私は以下の継承に関する小さなプログラムについて混乱しています:
_#include<iostream>
using namespace std;
struct B {
virtual int f() { return 1; }
}; // f is public in B
class D : public B {
int f() { return 2; }
}; // f is private in D
int main()
{
D d;
B& b = d;
cout<<b.f()<<endl; // OK: B::f() is public, D::f() is invoked even though it's private
cout<<d.f()<<endl; // error: D::f() is private
}
_
D::f()
がプライベートである理由がわかりません。D
はB
からpublic継承であるため、f
のパブリック関数B
D
でも公開されています(継承がない場合、メンバーのアクセスはデフォルトで非公開です)f
はB
の仮想関数であるため、b.f()
を呼び出すと、実際にはD::f()
を呼び出しますが、前述の図のように、なぜD::f()
を呼び出すことができます。プライベートなのに呼び出されますか?誰かが単純な継承の問題を詳細に説明できますか?
これは、仮想ディスパッチがランタイムの概念であるということと関係があります。クラスB
は、どのクラスがそれを拡張するかを気にせず、それがプライベートであるかパブリックであるかを知らないため、気にしません。
D :: f()がプライベートで、DがBからパブリックに継承されている理由がわかりません。したがって、Bのパブリック関数fもDでパブリックです(継承がない場合、メンバーアクセスはデフォルトでプライベートです)
D::f()
は、プライベートにしたためプライベートです。このルールは、継承や仮想ディスパッチの影響を受けません。
fはBの仮想関数なので、b.f()を呼び出すと、実際にはD :: f()が呼び出されますが、図で説明したように、プライベートであってもD :: f()を呼び出すことができるのはなぜですか?
実際には、b.f()
を呼び出すとき、コンパイラはどの関数が実際に呼び出されるかを認識していません。関数f()
を呼び出すだけで、B::f
は仮想であるため、呼び出される関数はランタイムで選択されます。ランタイムプログラムには、どの関数がプライベートまたは保護されているかに関する情報がありません。それは機能しか知りません。
実行時に関数が選択された場合、コンパイラーはコンパイル時にどの関数が呼び出されるかを知ることができず、アクセス指定子も知ることができません。実際、コンパイラーは、呼び出された関数がプライベートであるかどうかをチェックしようとさえしません。アクセス指定子は、コンパイラーがまだ認識していないコードに含まれている可能性があります。
これまでに経験したように、D::f
に直接電話をかけることはできません。これはまさにprivateが行うことです。メンバーへの直接アクセスを禁止します。ただし、ポインタまたは参照を介して間接的にアクセスできます。使用する仮想ディスパッチは、内部でそれを行います。
アクセス指定子は関数名にのみ適用され、他の方法で関数を呼び出す方法やタイミングを制限するものではありません。プライベート関数は、その名前以外の手段(関数ポインターなど)で使用できるようになっている場合、クラスの外部で呼び出すことができます。
class
キーワードで宣言されたクラスの場合、デフォルトのアクセス指定子はprivate
です。あなたのコードは次のものと同じです:
_// ...
class D: public B
{
private:
int f() { return 2; }
};
_
ご覧のとおり、f
はD
でプライベートです。同じ名前のB
内の関数のアクセス指定子が何であったかは関係ありません。 B::f()
とD::f()
は2つの異なる関数であることを忘れないでください。
virtual
キーワードの効果は、スコープ修飾子のないf()
がB
オブジェクトを参照するD
参照で呼び出された場合、さらにB::f()
に解決されますが、実際にはD::f()
が代わりに呼び出されます。
このプロセスは引き続きB::f()
のアクセス指定子を使用します。アクセスはコンパイル時にチェックされます。ただし、どの関数が呼び出されるかは実行時の問題である可能性があります。
C++標準には、この正確な例があります。
11.5仮想関数へのアクセス[class.access.virt]
1仮想関数のアクセス規則(第11節)は、その宣言によって決定され、後でそれをオーバーライドする関数の規則の影響を受けません。 [例:
class B { public: virtual int f(); }; class D : public B { private: int f(); }; void f() { D d; B* pb = &d; D* pd = &d; pb->f(); // OK: B::f() is public, // D::f() is invoked pd->f(); // error: D::f() is private }
-終了例]
これ以上明確に説明することはできません。
与えられた答えは何が行われているのかを示していますが、基本クラスがprivate
仮想関数を呼び出す場合になぜこれを実行したいのでしょうか。
さて、派生クラスのプライベート仮想関数を呼び出す基本クラスを持つこの手法を使用する テンプレートメソッドパターン と呼ばれるデザインパターンがあります。
struct B
{
virtual ~B() {};
int do_some_algorithm()
{
do_step_1();
do_step_2();
do_step_3();
}
private:
virtual void do_step_1() {}
virtual void do_step_2() {}
virtual void do_step_3() {}
};
class D : public B
{
void do_step_1()
{
// custom implementation
}
void do_step_2()
{
// custom implementation
}
void do_step_3()
{
// custom implementation
}
};
int main()
{
D dInstance;
B * pB = &dInstance;
pB->do_some_algorithm();
}
これにより、クラスD
のカスタムステップをpublic
インターフェイスに公開することはできませんが、同時にB
はpublic
関数を使用してこれらの関数を呼び出すことができます。
これは実際には仮想ディスパッチとは関係がなく、アクセス指定子の意味と関係があります。
関数自体はprivate
ではありません;そのnameはです。
したがって、関数はクラスのスコープ外で名前を付けることはできません。 main
から。ただし、public
である名前(つまり、オーバーライドされるベースの仮想関数)を介して、またはprivate
修飾子にもかかわらず関数の名前にアクセスできるスコープから(たとえば、そのクラスのメンバー関数)。
それがまさにその仕組みです。
プライベートなのにD :: f()を呼び出せるのはなぜですか?
仮想関数のメカニズムを理解するには、それが通常どのように実装されているかを知っておくとよいでしょう。実行時の関数は、実際には、関数本体の実行可能コードが配置されているメモリ内のアドレスにすぎません。関数を呼び出すには、そのアドレス(ポインター)を知る必要があります。メモリ内に仮想関数表現を持つC++オブジェクトには、いわゆるvtable-仮想関数へのポインタの配列が含まれています。
重要な点は、派生クラスではvtableが基本クラスのvtableを繰り返す(そして拡張する可能性がある)が、仮想関数がオーバーライドされると、そのポインターが置き換えられるということです。派生オブジェクトのvtable。
基本クラスポインタを介して仮想関数呼び出しが実行されると、仮想関数のアドレスはvtable配列のオフセットとして計算されます。他のチェックは行われず、関数アドレスのみが取得されます。基本クラスオブジェクトの場合は、基本クラス関数のアドレスになります。派生クラスオブジェクトの場合は、派生クラス関数のアドレスになり、プライベートとして宣言されているかどうかは関係ありません。
これがどのように機能するかです。
struct
のメンバーはデフォルトでパブリックになり、class
のメンバーはデフォルトでプライベートになります。したがって、Bのf()
はパブリックであり、Dに派生すると、明示的にパブリックであると宣言しなかったため、派生のルールに従ってプライベートになりました。