C++ の継承システムでよくある間違いに関する簡単な説明
現在、私の職歴に基づいて、C++ の継承システムで必ず間違える点が 2 つあります。
- 基本クラスでオーバーライドする必要があるメソッドを仮想メソッドとして設定しないでください。
- 基本クラスのデストラクターを仮想メソッドとして設定しないでください。
1点目ですが、基底クラスでオーバーライドする必要があるメソッドが仮想メソッドとして設定されていない場合はどうなるでしょうか。その結果、サブクラスがクラスを継承します。サブクラスが親クラスのメソッドをオーバーライドしたい場合、それは成功しません。動的バインディングが発生しても、親クラスは引き続き親クラスのメソッドを呼び出し、メソッドは呼び出しません。サブクラスのメソッド。実際の列を見てみましょう。
#include<iostream>
class Father {
public:
Father(){
}
~Father(){
}
void Show()
{
std::cout << "I'm your Father" << std::endl;
}
};
class Son : public Father {
public:
Son() {
}
~Son(){
}
void Show()
{
std::cout << "I'm your Son" << std::endl;
}
};
int main()
{
Father* father = new Son;
father->Show();
system("pause");
}
コードを読んだ後、まず出力が正確に何であるかを考えてみましょう。それは父の show() を呼び出しているのか、それとも息子の Show() を呼び出しているのでしょうか?
出力:
私はあなたの父親です。
続行するには任意のキーを押してください...
予想通り、父親は険しい顔で自分のメソッドを呼び出しました。私の息子はまったく栄養を摂取できません、可哀そうな赤ちゃん。
次に、親クラスの show() メソッドを仮想メソッド (virtual void Show();) に変更して、その効果を確認してみましょう。
#include<iostream>
class Father {
public:
virtual void Show()
{
std::cout << "I'm your Father" << std::endl;
}
};
class Son : public Father {
public:
void Show()
{
std::cout << "I'm your Son" << std::endl;
}
};
int main()
{
... // 与上面一致
}
変更された出力:
I'm your Son
続行するには任意のキーを押してください...
この種の継承の問題は非常に一般的です。カバーする必要があるメソッドは、仮想キーを使用して変更する必要があります。そうしないと、動的バインディングが発生した場合でも、サブクラスのメソッドが呼び出されず、予想とはかけ離れているか、エラーさえ発生します。実際、そのようなエラーを防ぐ良い方法があります。サブクラス メソッドにオーバーライドを追加するだけです。このキーワードは、サブクラスのメソッドが親クラスのメソッドをオーバーライドできるかどうか (親クラスのメソッドが仮想メソッドであるかどうか、サブクラスのメソッドが親クラスのメソッドと一貫性があるかどうかなど) を自動的に検出します。したがって、仕様書によれば、次のように記述されます。
#include<iostream>
class Father {
public:
virtual void Show()
... // 与上面一致
};
class Son : public Father {
public:
virtual void Show() override
...
};
int main()
{
...
}
ことわざにあるように、ルールのないルールは存在しません。ルールに従えば、間違いを犯す可能性は必ず減ります。
2 番目の間違いやすい点は、これも面接室でよく聞かれる質問ですが、継承システムでは、基本クラスのデストラクター メソッドを仮想メソッドに設定する必要があるという点です。もう一言、インタビュー ルームが常にこの質問をするのが好きなら、要点だけ話してもいいでしょう:
基本クラスのデストラクターを仮想メソッドとして設定することは、メモリ リークを防ぐことです。派生クラスが一部のメモリを適用し、それをデストラクターで解放する場合、基底クラス ポインターが削除されたときにサブクラスのデストラクターは呼び出されません。なぜそうではないのですか? 実験をしただけではないでしょうか? サブクラスが親クラスのメソッドを呼び出しても、それが仮想メソッドでなければ動的バインディングは発生しないので、当然親クラスのメソッドのみが呼び出され、サブクラスのメソッドは呼び出されず、メソッドも呼び出されません。メモリリークが発生するようにメモリを解放するにはどうすればよいですか。もちろん、サブクラス自体は解放されず、ワイルドポインタとなります。メモリリークの原因となります。
人々はあまり話しません。さあ、ただコードに行きましょう。コードは魂の目的地です。
#include<iostream>
class Base {
public:
Base()
{
}
// 故意不设置为虚方法
~Base()
{
std::cout << " Base 析构完成..." << std::endl;
}
virtual void DoSomething() = 0;
};
class Derive : public Base {
public:
Derive() :m_data(nullptr)
{
m_data = new char[m_lenth];
}
~Derive()
{
delete[] m_data;
std::cout << " Derive 析构完成..." << std::endl;
}
virtual void DoSomething() override
{
// ...
}
private:
// 既然是狠人,我们就new 500M内存
// 这儿为什么不直接写524288000呢? 其实多个嘴,
// c++有编译和运行阶段,像这种常量,编译阶段能计算出来的,
// 编译器早就优化好了,根本就等不到运行时才算,所以啊,
// 常量我们尽量写的直白点,像直接写524288000 这种魔鬼数字,鬼懂哦
const unsigned int m_lenth = 500 * 1024 * 1024;
char* m_data;
};
int main()
{
Base* base = new Derive;
base->DoSomething();
delete base;
while (true)
{
// 方便我们在任务管理器里看看内存到底有没有被回收
}
system("pause");
return 0;
}
タスク マネージャーの出力とメモリの状態を確認してください。
出力:
基本デストラクターが完了しました...
この文を出力しただけですが、サブクラスのデストラクターが実行されませんでした。それは難しい。
タスク マネージャー:
解放されていない 500M のメモリを見てください。このようなコードがサーバー上で実行されると、サーバーのわずか数百ギガバイトのメモリでは不十分です。数サイクル後にクラッシュします。
コードを変更して、親クラスのデストラクターを仮想メソッドに変更しましょう。
#include<iostream>
class Base {
public:
...
virtual ~Base()
{
std::cout << " Base 析构完成..." << std::endl;
}
virtual void DoSomething() = 0;
};
class Derive : public Base {
public:
... // 和上保持一致
private:
...
};
int main()
{
Base* base = new Derive;
base->DoSomething();
delete base;
while (true)
{
// 方便我们在任务管理器里看看内存到底有没有被回收
}
system("pause");
return 0;
}
実行中の効果を見てみましょう。
出力:
派生デストラクターが完了しました...
基本デストラクターが完了しました...
タスク マネージャー:
出力から、親クラスがサブクラスのデストラクターを呼び出したことがわかり、タスク マネージャーは次のように示しています。記憶が解放されたということ。実際的な観点から見ると、メモリリークが発生するため、親クラスのデストラクタを仮想メソッドに設定する必要があります。ローカルで使うときは大丈夫、メモリがなくなって再起動すれば大丈夫、サーバーがこんな感じだと動かない。その際、影響を受けるのは 1 人ではなく、何千人ものユーザーになります。この点に関する原則的な議論には立ち入りません。さらに読むことをお勧めします:
「入門 C++ 第 5 版」第 15 章
「効果的な C++」 07. ポリモーフィック基底クラス宣言の仮想デストラクター
最後に、いくつかの言葉を言います。サードパーティのライブラリからクラスを継承したい場合は、基本クラスに仮想デストラクターがあるかどうかを注意して確認する必要があります。たとえば、std::string を継承することは不可能ですが、これはまったくナンセンスです。それらのどれも仮想的な架空の機能を持っていません。したがって、すべてのことに注意する必要があります。