仮想関数とVTable
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 52/104。
virtual キーワードは、前のレッスンで見た問題を解決します。メソッドを virtual として宣言すると、C++ はポインター型ではなく、実行時の実際のオブジェクト型に基づいて、どのバージョンを呼び出すかを決定します。
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // 出力: Woof!これで正しいメソッドが呼び出されます!overrideキーワードは任意ですが、推奨されます。これは、仮想関数をオーバーライドする意図があることをコンパイラに伝え、シグネチャが一致しない場合にエラーを検出します。
これはどのように機能しますか? クラスに仮想関数がある場合、コンパイラーは仮想テーブル(vtable)を作成します。これは、クラスの仮想関数へのポインターを含む隠しルックアップテーブルです。仮想関数を持つ各オブジェクトは、そのクラスのvtableへの隠しポインター(vptr)を格納します。
ポインタを介して仮想関数を呼び出すと、プログラムは実行時に vtable で正しい関数アドレスを検索します。これは動的ディスパッチと呼ばれます。通常の関数呼び出しと比べてわずかなオーバーヘッドが生じますが、強力なポリモーフィックな振る舞いを可能にします。
重要なルールが1つあります。class に virtual 関数があり、base class として使用される場合、その destructor も virtual にする必要があります。これにより、base ポインタを通じて derived オブジェクトを削除する際に、適切なクリーンアップが保証されます。
class Animal {
public:
virtual ~Animal() {} // 仮想デストラクタ
virtual void speak() {}
};チャレンジ
簡単virtual 関数と実行時ポリモーフィズムの力を示すメディアプレーヤーシステムを構築しましょう。それぞれが異なる方法で再生されるメディア型の階層を作成し、base class ポインター経由でアクセスした場合でも、virtual 関数によって正しい動作が可能になることを確認します。
コードを3つのファイルに整理します。
Media.h:再生可能なあらゆるメディアを表す baseMediaclass を Define します。- protected な
std::string titlemember - title を受け取り、それを保存する constructor
- virtual
play()メソッド。次を出力します:Playing media: <title> - 次を出力するvirtual destructor:
Media [<title>] destroyed
- protected な
AudioTrack.h:Mediaを public 継承するAudioTrackclass を Define します。- private な
std::string artistmember - title と artist を受け取り、title を base class に渡して artist を保存する constructor
overridekeyword を使用してplay()を Override し、次を出力します:Playing audio: <title> by <artist>- 次を出力する destructor:
AudioTrack [<title>] destroyed
- private な
main.cpp:3つの入力を読み取ります(それぞれ別の行)。- Audio track title(string)
- Artist name(string)
- Video title(string)
Mediaを public 継承するVideoClipclass を main.cpp 内で直接 Define します。- private な
int durationmember(秒単位) - title と duration を受け取る constructor(duration の default は 120)
play()を Override して、次を出力します:Playing video: <title> (<duration>s)- 次を出力する destructor:
VideoClip [<title>] destroyed
block scope 内に3つの
Media*ポインターの array を Create します。Dynamically allocate するものは次のとおりです。- title が "Generic Media" の base
Mediaオブジェクト - 入力された title と artist を持つ
AudioTrackオブジェクト - 入力された video title を持つ
VideoClipオブジェクト
array を Loop して、各ポインターで
play()を call します。その後、すべてのオブジェクトを逆順に Delete します。block の後で、次を出力します:Playback complete!
たとえば、入力が Bohemian Rhapsody、Queen、Nature Documentary の場合:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Media* ポインター経由で play() を call すると、virtual 関数のおかげで正しい derived class のメソッドが呼び出されることに注目してください。また、virtual destructor によって適切な cleanup が保証されることも確認してください。base ポインター経由で Delete すると、derived と base の両方の destructor が実行されます。
自分で試してみよう
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Mediaから公開継承するVideoClipクラスをここに定義する
// - プライベートなint durationメンバー(秒単位)
// - コンストラクタはtitleとdurationを取る(durationのデフォルトは120)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: コンストラクタを実装する
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: durationを格納する
}
// TODO: play()メソッドをオーバーライドする
void play() override {
// TODO: このメソッドを実装する
}
// TODO: デストラクタを実装する
~VideoClip() {
// TODO: このデストラクタを実装する
}
};
int main() {
// 入力を読み取る
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: 波括弧でブロックスコープを作成する
// ブロック内:
// 1. 3つのMedia*ポインタの配列を作成する
// 2. 動的に割り当てる:
// - A base Media object with title "Generic Media"
// - 入力のタイトルとアーティストを持つAudioTrackオブジェクト
// - 入力のビデオタイトルを持つVideoClipオブジェクト
// 3. 配列をループして各ポインタでplay()を呼び出す
// 4. すべてのオブジェクトを逆順に削除する
// TODO: After the block, print: Playback complete!
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタパラメータ付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ徹底解説Rule of Three / Five / Zero復習 - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () の演算子オーバーロード型変換演算子復習 - Matrixクラス自分で練習してみよう: C++オンラインコンパイラ