C++において、仮想関数はオブジェクト指向プログラミングの重要な概念です。
この記事では、C++の仮想関数について詳しく解説します。
C++の仮想関数とは何か?
C++の仮想関数とは、オブジェクト指向プログラミングにおいて、ポリモーフィズム(多様性)を実現するための重要な概念です。
通常の関数は、コンパイル時に呼び出す関数が決定されます。しかし、仮想関数は実行時に呼び出す関数が決定されるため、オブジェクト指向プログラミングにおいて非常に重要な役割を果たします。
例えば、以下のような継承関係があった場合を考えてみましょう。
class Animal {
public:
virtual void speak() {
std::cout << "Animal speaks" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Dog barks" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Cat meows" << std::endl;
}
};
この場合、Animal
クラスを継承したDog
クラスとCat
クラスでspeak()
メソッドをオーバーライドしています。そして、以下のようにそれぞれのインスタンスを生成してspeak()
メソッドを呼び出してみます。
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->speak(); // "Dog barks"
animal2->speak(); // "Cat meows"
delete animal1;
delete animal2;
return 0;
}
この結果「Dog barks
とCat meows
という文字列が表示されます。これは、仮想関数によって実行時に適切なメソッドが呼び出されたからです。つまり、ポリモーフィズムが実現されていることがわかります。
仮想関数の基本的な使い方
C++において、仮想関数はポリモーフィズム(多様性)を実現するための重要な概念です。
仮想関数は、基底クラスで定義された関数を派生クラスで再定義することができます。このようにして、同じ名前の関数でも異なる動作をするようになります。
以下は、仮想関数の基本的な使い方の例です。
#include <iostream>
class Animal {
public:
virtual void speak() {
std::cout << "Animal speaks!" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
int main() {
Animal* animal = new Animal();
animal->speak();
Dog* dog = new Dog();
dog->speak();
Animal* animal2 = new Dog();
animal2->speak();
delete animal;
delete dog;
delete animal2;
}
上記のコードでは、Animal
クラスと Dog
クラスを定義しています。Animal
クラスには speak()
という仮想関数があります。この関数は、Animal speaks!
という文字列を出力します。
一方、Dog
クラスでは speak()
関数をオーバーライドし、Woof!
という文字列を出力するようにしています。
そして、main 関数内で Animal
クラスと Dog
クラスのインスタンスを生成し、それぞれ speak()
関数を呼び出しています。また、animal2
変数に Dog
クラスのインスタンスを代入し、その後も speak()
関数を呼び出しています。
実行結果は以下の通りです。
Animal speaks!
Woof!
Woof!
このように、仮想関数を使用することでポリモーフィズムが実現されており、同じ名前の関数でも異なる動作が可能になっています。
仮想関数の実装方法
C++の仮想関数は、クラス内で定義された関数にvirtualキーワードを付けることで実現できます。
class Base {
public:
virtual void func() {
std::cout << "Base::func()" << std::endl;
}
};
class Derived : public Base {
public:
void func() override {
std::cout << "Derived::func()" << std::endl;
}
};
上記の例では、Base
クラスとDerived
クラスが定義されています。Base
クラスのfunc()
関数にvirtual
キーワードが付けられており、これによってDerived
クラスでオーバーライドすることが可能になっています。
また、Derived
クラスでオーバーライドする場合は、override
キーワードを付けることが推奨されています。これによって、誤ってオーバーライドしていない場合やシグネチャが異なる場合にコンパイルエラーを発生させることが出来ます。
int main() {
Base* b = new Derived();
b->func(); // "Derived::func()" が出力される
}
上記の例では、ポリモーフィズムを利用してBase
型のポインタ変数b
にDerived
型のインスタンスを代入し、そのまま呼び出しています。
この場合、実行時にb
が指すオブジェクトのメソッドが呼び出されるため、Derived::func()
が出力されます。
仮想関数の注意点
仮想関数を使用する際には、以下の注意点について理解しておく必要があります。
1. 仮想関数はポインター経由で呼び出す
仮想関数は、オブジェクトのポインターを介して呼び出されるため、通常のメンバー関数とは異なります。そのため、オブジェクトのポインターを介して呼び出すことが必要です。
class Base {
public:
virtual void func() { cout << "Base::func()" << endl; }
};
class Derived : public Base {
public:
virtual void func() { cout << "Derived::func()" << endl; }
};
int main() {
Base* b = new Derived();
b->func(); // Derived::func()
}
上記の例では、Derived
クラスが Base
クラスを継承し、Base
クラスに定義された func()
関数をオーバーライドしています。
そして、Derived
クラスのオブジェクトを Base
クラスのポインターで指し示し、そのポインター経由で func()
関数を呼び出しています。この場合、実行結果は "Derived::func()"
となります。
2. 仮想関数は遅い
仮想関数は動的ディスパッチ(dynamic dispatch)によって実行されるため、通常のメンバー関数よりも遅くなります。
また、仮想関数テーブル(virtual function table)という特別な構造体が作成されるため、メモリ使用量も増加します。そのため、プログラム全体で多用する場合や高速性が求められる場合には注意が必要です。
3. コンストラクタやデストラクタでは仮想関数を使わない
コンストラクタやデストラクタでは仮想関数を使用しない方が良いです。
これらの特殊なメソッドではまだオブジェクトが完全に構築されておらず(コンストラクタ)または既に破棄されている(デストラクタ)可能性があるためです。
そのため、コンストラクタやデストラクタ内で呼び出した場合でも動的ディスパッチは行われません。
class Base {
public:
virtual void func() { cout << "Base::func()" << endl; }
Base() { func(); } // コンストラクタ内で仮想関数を呼ぶ
};
class Derived : public Base {
public:
virtual void func() { cout << "Derived::func()" << endl; }
Derived() {}
};
int main() {
Derived d;
d.func();
}
Base::func()
Derived::func()
上記の例では、Derivedクラスのオブジェクトを生成するタイミングで
が表示されます。Base::func()
これは、Derived
オブジェクトが作成される前にBase
オブジェクトが作成されるため、func()
関数が呼ばれるタイミングではDerived
オブジェクト自体はまだ存在しません。
そのため、Base
コンストラクタ内からfunc()
関数を呼ぶとBase
側のメソッドを参照することになるため、このような挙動になります。