次のコードフラグメントを考えると、関数呼び出しの違いは何ですか?機能隠蔽とは何ですか?関数のオーバーライドとは何ですか?それらは関数の過負荷とどのように関連していますか? 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();
}
...は名前の隠蔽の一形態です。簡単な例:
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)`
};
仮想メンバー関数の呼び出しと非仮想メンバー関数の呼び出しの違いは、定義上、前者の場合、ターゲット関数は-に従って選択されることです。 dynamic呼び出しで使用されるオブジェクト式のタイプ。後者の場合、static typeが使用されます。
これですべてです。あなたの例は、p2->doA()
とp2->doB()
の呼び出しによってこの違いを明確に示しています。 _*p2
_式の静的型はParent
ですが、同じ式の動的型はChild
です。これが、p2->doA()
が_Parent::doA
_を呼び出し、p2->doB()
が_Child::doB
_を呼び出す理由です。
その違いが重要な状況では、名前の非表示はまったく関係ありません。
それらすべての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
}
簡単なものから始めましょう。
p1
はParent
ポインターであるため、常にParent
のメンバー関数を呼び出します。
cp
はChild
へのポインターであるため、常にChild
のメンバー関数を呼び出します。
今、より難しいもの。 p2
はParent
ポインターですが、タイプ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()
を呼び出します。
p2
はParent*
であり、Parent
はChild
の内容を認識していないため、doX()
を参照できません。ただし、p2
は1つとして初期化されているため、Child*
にキャストできます。その後、doX()
を呼び出すために使用できます。
質問に記述されているサンプルコードは、基本的に、実行時に答えを提供します。
非仮想関数を呼び出すと、オブジェクトが実際に他の派生型として作成されたかどうかに関係なく、ポインタ型と同じクラスの関数が使用されます。一方、仮想関数を呼び出すと、使用しているポインタの種類に関係なく、元の割り当てられたオブジェクトタイプの関数が使用されます。
したがって、この場合のプログラムの出力は次のようになります。
doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child