今日、友人グループとチャットしていたときに、非常に紛らわしい質問を見つけたので、それを共有したいと思います。
1. 質問を読んでください。
まずはタイトルを見てみましょう
struct Dad
{
public:
Dad(){
echo();}
~Dad(){
echo();}
virtual void echo() {
cout << "DAD ";
}
};
struct Son:Dad
{
public:
void echo() const override {
cout << "SON ";
}
};
Son ss;
これの出力は何でしょうか?
A "DAD DAD "
B "DAD SON "
C "SON DAD "
D "SON SON "
E 编译出错
F 运行出错
答えは E、コンパイル エラーです。
2. 関連する知識ポイント
2.1 知識ポイント
まず、この質問にどのような知識ポイントが含まれるかについて話しましょう。
- 多態性呼び出し。
- ポリモーフィックな書き換え関数ではどのような条件を満たす必要があるか。
- クラス内に関数を追加する機能
const
。 - クラス内に関数を追加する機能
override
。 - 早期バインディングと遅延バインディングとは何ですか
一つ一つ見直していきましょう!
- ポリモーフィック呼び出しとは、親クラスのポインター/参照がサブクラスを指している場合、仮想関数を呼び出すとサブクラスの書き換えられたバージョンが呼び出されることを意味します。
- 関数を多態的に書き換える条件: 関数名/パラメータ/戻り値が同じである必要があります (共分散があることに注意してください)
- クラス内の関数の後に変更されるのは
const
オブジェクトのポインタでありthis
、変更された関数内でクラス内のメンバ変数を変更することはできません。 - クラスの後に関数を追加すると、
override
コンパイラはそれがオーバーロードを構成するかどうかを厳密にチェックできます。 - 早期バインディング: 静的バインディング、後期バインディング: 動的バインディング (詳細については、CPP 多態性ブログを参照してください)
2.2 分析の質問
親クラスとサブクラスのecho()
これら 2 つの関数の違いに注意してください
virtual void echo(){
}//父类
void echo() const override {
}//子类
まず注意すべきことは、サブクラス関数ではvirtual
キーワードを省略できるが、省略した場合でも関数は仮想関数であることに変わりはありません。
ここではサブクラスの関数にさらに変更がありconst
、この const 変更はthis
関数内の暗黙的なポインターです。この時点で、サブクラス内の関数のパラメーターがecho()
変更されています。
virtual void echo(Son* this) {
} // 不加const
virtual void echo(const Son* this) {
} // 加const
ここでの this ポインタは const で変更されているため、サブクラスのエコーと親クラスのエコーのパラメーターの型が異なり、仮想関数の書き換えにはなりません。厳密なキーワード チェックと組み合わせるとoverride
、コンパイル中にエラーが直接報告されます。
正しい書き方は、サブクラスの echo 内の const を削除するか、親クラスの echo 関数に const を追加することです。
3. もう一度質問を見てみましょう
さて、難しい部分を読み終えたので、「通常の」質問を見てみましょう。これは、上記の質問をコンパイルして合格できる質問に変更することです。このとき誰を選ぶべきですか?
struct Dad
{
public:
Dad(){
echo();}
~Dad(){
echo();}
virtual void echo() const{
cout << "DAD ";
}
};
struct Son:Dad
{
public:
void echo() const override {
cout << "SON ";
}
};
Son ss;
A "DAD DAD "
B "DAD SON "
C "SON DAD "
D "SON SON "
コンパイルして実行すると、結果としてDAD DAD
A が選択されることがわかります。
3.1 分析
Son
クラスのコンストラクターおよびデストラクターを定義する場合、親クラスの対応するコンストラクターおよびデストラクターを呼び出す仕様はありません。したがって、Son
オブジェクトが作成されると、クラスのコンストラクターとデストラクターがss
デフォルトで呼び出されます。Dad
Dad
クラス内のコンストラクターとデストラクターは仮想関数を呼び出しecho()
、この仮想関数はサブクラスでオーバーライドされるため、Son
オブジェクトの種類に応じて、対応するオーバーライドされた関数が呼び出されます。ただし、コンストラクターとデストラクターでは、仮想関数メカニズムが期待どおりに機能しません。
仮想関数がコンストラクターで呼び出される場合、動的バインディング メカニズムは無視され、親クラスの関数バージョンが直接呼び出されます。したがって、Dad
のコンストラクターでの呼び出しは、実際にはクラス内のオーバーライドされたバージョンではなく、クラス内の関数echo()
を呼び出します。Dad
echo()
Son
同様に、動的バインディング メカニズムはデストラクターで無視され、親クラスの関数バージョンが直接呼び出されます。したがって、Dad
デストラクターで呼び出された場合でも、クラス内の関数echo()
が呼び出されます。Dad
echo()
したがって、Son
オブジェクトが作成されて出力が出力されると、ss
最初にDad
クラスのコンストラクターが呼び出されて出力され"DAD "
、次にDad
クラスのデストラクターが呼び出されて再度出力されます"DAD "
。
3.2 結論
親クラスの構築と破棄では、オブジェクトのバージョンが親クラスのバージョンであると判断され、サブクラスの書き換えられた関数の代わりに親クラス独自の関数を呼び出すために早期バインディングが使用されます。
単純な記憶: 親クラスの構築と破棄に仮想関数が出現した場合、親クラス独自の関数のみが呼び出されます。
これは、コンパイラが構築と破棄の正しい順序を保証する必要があるためで、親クラスの破棄時にサブクラスの仮想関数が呼び出される場合、次のシナリオが発生する可能性があります。
struct Dad
{
public:
Dad(){
echo();}
~Dad(){
echo();}
virtual void echo() const{
cout << "DAD ";
}
};
struct Son:Dad
{
public:
Son() {
_a = new int(3);
}
~Son() {
delete _a;
}
void echo() const override {
cout << "SON ";
delete _a;
}
private:
int _a;
};
Son ss;
親クラスのデストラクターがecho()
サブクラスによって書き換えられた関数を呼び出すと、サブクラスが破棄されたように見えます (サブクラスのデストラクターが親クラスのデストラクターよりも早い)、同じ空間で 2 回破棄されます_a
。 .エラーが報告されます。delete
delete
したがって、この状況を回避するために、親クラスのデストラクターで早期バインディングが使用され、サブクラスによってオーバーライドされた仮想関数は有効になりません。
この動作は、オブジェクトの構築および破棄中に各クラスのコンストラクターとデストラクターが正しい順序で呼び出されることを保証し、オブジェクトが不完全に初期化または部分的に破棄された状態にあるときにサブクラスの関数の呼び出しを回避することを目的としています。
親クラスの構造もこのように理解できますが、親クラスの構造内でサブクラスの仮想関数を呼び出すことができる場合、サブクラスのオブジェクトに新しい領域が 2 回使用される可能性があり、メモリ リークが発生します。
ただし、コンストラクターは仮想関数テーブルの初期化にも関係します。この時点では、仮想関数テーブルは完全に初期化されておらず、サブクラス オブジェクトもまだ構築されておらず、ポリモーフィック呼び出しの条件もないため、書き換えられたサブクラスの仮想関数を呼び出すことができません。