Menu
Coddy logo textTech

仮想関数と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() {}
};
challenge icon

チャレンジ

簡単

virtual 関数と実行時ポリモーフィズムの力を示すメディアプレーヤーシステムを構築しましょう。それぞれが異なる方法で再生されるメディア型の階層を作成し、base class ポインター経由でアクセスした場合でも、virtual 関数によって正しい動作が可能になることを確認します。

コードを3つのファイルに整理します。

  • Media.h:再生可能なあらゆるメディアを表す base Media class を Define します。
    • protected な std::string title member
    • title を受け取り、それを保存する constructor
    • virtual play() メソッド。次を出力します:Playing media: <title>
    • 次を出力するvirtual destructorMedia [<title>] destroyed
  • AudioTrack.hMedia を public 継承する AudioTrack class を Define します。
    • private な std::string artist member
    • title と artist を受け取り、title を base class に渡して artist を保存する constructor
    • override keyword を使用して play() を Override し、次を出力します:Playing audio: <title> by <artist>
    • 次を出力する destructor:AudioTrack [<title>] destroyed
  • main.cpp:3つの入力を読み取ります(それぞれ別の行)。
    1. Audio track title(string)
    2. Artist name(string)
    3. Video title(string)

    Media を public 継承する VideoClip class を main.cpp 内で直接 Define します。

    • private な int duration member(秒単位)
    • 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 RhapsodyQueenNature 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;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: C++オンラインコンパイラ