web-dev-qa-db-ja.com

仮想関数のオーバーライドと非仮想関数の非表示の違いは何ですか?

次のコードフラグメントを考えると、関数呼び出しの違いは何ですか?機能隠蔽とは何ですか?関数のオーバーライドとは何ですか?それらは関数の過負荷とどのように関連していますか? 2つの違いは何ですか?これらの適切な説明を1か所で見つけることができなかったので、情報を統合できるようにここで質問しています。

class Parent {
  public:
    void doA() { cout << "doA in Parent" << endl; }
    virtual void doB() { cout << "doB in Parent" << endl; }
};

class Child : public Parent {
  public:
    void doA() { cout << "doA in Child" << endl; }
    void doB() { cout << "doB in Child" << endl; }
};

Parent* p1 = new Parent();
Parent* p2 = new Child();
Child* cp = new Child();

void testStuff() {
  p1->doA();
  p2->doA();
  cp->doA();

  p1->doB();
  p2->doB();
  cp->doB();
}
17
Jed Schaaf

機能隠蔽とは何ですか?

...は名前の隠蔽の一形態です。簡単な例:

void foo(int);
namespace X
{
    void foo();

    void bar()
    {
        foo(42); // will not find `::foo`
        // because `X::foo` hides it
    }
}

これは、基本クラスの名前ルックアップにも当てはまります。

class Base
{
public:
    void foo(int);
};

class Derived : public Base
{
public:
    void foo();
    void bar()
    {
        foo(42); // will not find `Base::foo`
        // because `Derived::foo` hides it
    }
};

関数のオーバーライドとは何ですか?

これは、仮想関数の概念にリンクされています。 [class.virtual]/2

仮想メンバー関数vfがクラスBaseおよびクラスDerivedで宣言されており、Baseから直接または間接的に派生している場合、同じ名前、parameter-type-list、cv-qualification、およびref-qualifier(または)を持つメンバー関数vf同じものがない場合)Base::vfが宣言されると、Derived::vfも仮想(宣言されているかどうかに関係なく)であり、オーバーライドBase::vfです。

class Base
{
private:
    virtual void vf(int) const &&;
    virtual void vf2(int);
    virtual Base* vf3(int);
};

class Derived : public Base
{
public: // accessibility doesn't matter!
    void vf(int) const &&; // overrides `Base::vf(int) const &&`
    void vf2(/*int*/);     // does NOT override `Base::vf2`
    Derived* vf3(int);     // DOES override `Base::vf3` (covariant return type)
};

仮想関数を呼び出すときに、最後のオーバーライドが関連します:[class.virtual]/2

クラスオブジェクトSの仮想メンバー関数C::vfは、Sが基本クラスサブオブジェクト(存在する場合)である最も派生したクラスがvfをオーバーライドする別のメンバー関数を宣言または継承しない限り、最終的なオーバーライドです。

つまりタイプSのオブジェクトがある場合、最後のオーバーライドは、Sのクラス階層をトラバースしてその基本クラスに戻るときに表示される最初のオーバーライドです。重要な点は、関数呼び出し式の動的タイプが最終的なオーバーライドを決定するために使用されることです。

Base* p = new Derived;
p -> vf();    // dynamic type of `*p` is `Derived`

Base& b = *p;
b  . vf();    // dynamic type of `b` is `Derived`

オーバーライドと非表示の違いは何ですか?

基本的に、基本クラスの関数は、派生クラスの同じ名前の関数によって常に隠されています。派生クラスの関数が基本クラスの仮想関数をオーバーライドするかどうかに関係なく、次のようになります。

class Base
{
private:
    virtual void vf(int);
    virtual void vf2(int);
};

class Derived : public Base
{
public:
    void vf();     // doesn't override, but hides `Base::vf(int)`
    void vf2(int); // overrides and hides `Base::vf2(int)`
};

関数名を見つけるために、式の静的型が使用されます。

Derived d;
d.vf(42);   // `vf` is found as `Derived::vf()`, this call is ill-formed
            // (too many arguments)

それらは関数の過負荷とどのように関連していますか?

「関数の非表示」は名前の非表示の一形態であるため、関数の名前が非表示になっていると、すべてのオーバーロードが影響を受けます。

class Base
{
private:
    virtual void vf(int);
    virtual void vf(double);
};

class Derived : public Base
{
public:
    void vf();     // hides `Base::vf(int)` and `Base::vf(double)`
};

関数のオーバーライドの場合、同じ引数を持つ基本クラスの関数のみがオーバーライドされます。もちろん、仮想関数をオーバーロードすることもできます。

class Base
{
private:
    virtual void vf(int);
    virtual void vf(double);
    void vf(char);  // will be hidden by overrides in a derived class
};

class Derived : public Base
{
public:
    void vf(int);    // overrides `Base::vf(int)`
    void vf(double); // overrides `Base::vf(double)`
};
26
dyp

仮想メンバー関数の呼び出しと非仮想メンバー関数の呼び出しの違いは、定義上、前者の場合、ターゲット関数は-に従って選択されることです。 dynamic呼び出しで使用されるオブジェクト式のタイプ。後者の場合、static typeが使用されます。

これですべてです。あなたの例は、p2->doA()p2->doB()の呼び出しによってこの違いを明確に示しています。 _*p2_式の静的型はParentですが、同じ式の動的型はChildです。これが、p2->doA()が_Parent::doA_を呼び出し、p2->doB()が_Child::doB_を呼び出す理由です。

その違いが重要な状況では、名前の非表示はまったく関係ありません。

7
AnT

それらすべてのb/wが異なるはるかに簡単な例。

class Base {
public:
    virtual int fcn();
};

class D1 : public Base {
public:  
    // D1 inherits the definition of Base::fcn()
    int fcn(int);  // parameter list differs from fcn in Base
    virtual void f2(); // new virtual function that does not exist in Base
};

class D2 : public D1 {
public:
    int fcn(int); // nonvirtual function hides D1::fcn(int)
    int fcn();  // overrides virtual fcn from Base
    void f2();  // overrides virtual f2 from D1
}
2
AbhimanyuAryan

簡単なものから始めましょう。

p1Parentポインターであるため、常にParentのメンバー関数を呼び出します。

cpChildへのポインターであるため、常にChildのメンバー関数を呼び出します。

今、より難しいもの。 p2Parentポインターですが、タイプChildのオブジェクトを指しているため、一致するChild関数が仮想であるか、関数がParent内にのみ存在し、Childに存在しない場合は常に、Parentの関数を呼び出します。つまり、Childは、Parent::doA()を独自のdoA()で非表示にしますが、Parent::doB()をオーバーライドします。同じ名前の関数には異なる実装が与えられるため、関数の非表示は関数のオーバーロードの一形態と見なされることがあります。非表示関数は非表示関数とは異なるクラスにあるため、署名が異なり、どちらを使用するかが明確になります。

testStuff()の出力は次のようになります

doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child

いずれの場合も、Parent::doA()Parent::doB()は、関数の「仮想性」に関係なく、名前解決を使用してChild内で呼び出すことができます。関数

void Child::doX() {
  doA();
  doB();
  Parent::doA();
  Parent::doB();
  cout << "doX in Child" << endl;
}

cp->doX()によって呼び出されたときに、出力してこれを示します

doA in Child
doB in Child
doA in Parent
doB in Parent
doX in Child

さらに、cp->Parent::doA()ParentのバージョンのdoA()を呼び出します。

p2Parent*であり、ParentChildの内容を認識していないため、doX()を参照できません。ただし、p2は1つとして初期化されているため、Child*にキャストできます。その後、doX()を呼び出すために使用できます。

2
Jed Schaaf

質問に記述されているサンプルコードは、基本的に、実行時に答えを提供します。

非仮想関数を呼び出すと、オブジェクトが実際に他の派生型として作成されたかどうかに関係なく、ポインタ型と同じクラスの関数が使用されます。一方、仮想関数を呼び出すと、使用しているポインタの種類に関係なく、元の割り当てられたオブジェクトタイプの関数が使用されます。

したがって、この場合のプログラムの出力は次のようになります。

doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child
1
karadoc