仮想関数
継承は、あるクラスの機能を受け継いだ新しいクラスを定義することです。
前ページではごく単純に、ある基底クラスに新しい関数を追加しただけのシンプルな派生クラスを紹介しました。
これだけでもある程度便利に使うことはできますが、継承で重要なのは多様性(ポリモーフィズム)を実現できる点です。
そのためには仮想関数という機能が必要となります。
仮想関数のサンプル
まずはサンプルコードを見てみましょう。
#include <iostream>
class Human
{
public:
//仮想関数
virtual void speak() const
{
std::cout << "Hello." << std::endl;
}
};
class Japanese : public Human
{
public:
//仮想関数をオーバーライド
void speak() const override
{
std::cout << "こんにちは。" << std::endl;
}
};
//引数を基底クラスの参照で受け取る
void action(const Human &human)
{
human.speak();
}
int main()
{
Human john; //基底クラス
Japanese taro; //派生クラス
action(john);
action(taro);
std::cin.get();
}
Human
クラスは挨拶を返す関数speak
を持つシンプルなクラスです。
Japanese
クラスはHuman
クラスを継承し、日本語で挨拶をするクラスにしています。
ここで注目すべきは、どちらも同じ「speak」という名前で関数が定義されている点です。
さらに、Human
クラスのspeak関数には、先頭にvirtual
というキーワードが追加されてます。
そして、Japanese
クラスのspeak
関数の後ろにはoverride
というキーワードが追加されています。
実はこのコードはis-a関係になっていません。
is-a関係を忠実に守りたい場合は、後に説明する純粋仮想関数という機能を使います。
virtual
基底クラスの関数にvirtual
キーワードを付けると、その関数は仮想関数というものになります。
仮想関数は基本的に普通の関数と同じですが、そのクラスを継承したとき、派生クラス側で機能が上書きされる可能性がある関数、という意味になります。
あくまでも可能性であって、必ず上書きされるわけではありませんし、必ず上書きして使わなければならないという意味ではありません。
上書きしなければそのまま普通の関数として使用できます。
基底クラスのインスタンスを生成して使う場合(継承機能を使わない場合)も同様です。
override
派生クラスの関数にoverride
キーワードを付けると、その関数は基底クラスの関数を上書きする関数という意味になります。
(キーワードを付ける位置に注意)
基底クラスの関数を上書きすることをオーバーライドといいます。
名前がオーバーロードと似ていますが別の機能なので注意してください。
オーバーロードは関数の引数が異っている必要がありますが、オーバーライドは引数や戻り値、constの有無などの関数定義(シグネチャという)がすべて一致していなければなりません。
(オーバーロードについては関数の新機能参照)
なお、override
は比較的新しく追加されたキーワードです。
(C++11、2011年)
古いコンパイラでは基底クラスと同じくvirtual
を使用します。
(virtual
を記述する位置も基底クラスと同じです)
ちなみに、基底クラスで仮想関数に指定されていない関数をオーバーライドしようとするとエラーになります。
実は派生クラスではoverride
やvirtual
を記述しなくてもオーバーライドは可能です。
しかしこれらのキーワードがないと、シグネチャ(引数などの定義)が異なる場合にオーバーライドにならず、新しい関数を定義したことになってしまいます。
これらのキーワードを付けていれば記述ミスなどがあればきちんとエラーになってくれますから、バグの混入を防ぐことができます。
多様性の実現
アップキャスト
さて、上記のサンプルコードではもう一つ重要な点があります。
それは「関数action
の引数はHuman
クラスの参照型」という点です。
//引数を基底クラスの参照で受け取る
void action(const Human &human)
{
human.speak();
}
int main()
{
Human john; //基底クラス
Japanese taro; //派生クラス
action(john);
action(taro);
}
main関数ではHuman
クラスとJapanese
クラスのインスタンスをひとつずつ生成しています。
そしてそれを関数action
にそのまま渡しています。
関数action
の引数はHuman
クラスの参照型なのに、派生クラスであるJapanese
クラスのインスタンスを受け取ることができます。
そして、その結果としてそれぞれのクラスで定義されている通りのメンバ関数speak
を呼び出すことができます。
上のサンプルコードの実行結果は以下になります。
Hello. こんにちは。
基底クラスと派生クラスではデータ型が異なるのでエラーになりそうなものですが、基底クラスは派生クラスのインスタンスを受け取れるという特徴があります。
派生クラスは、基底クラスを含むクラスです。
基底クラスに存在する関数は派生クラスにも存在することが保証されているため、基底クラスの型情報から(基底クラスにも存在する)派生クラスの関数を呼び出すことができるのです。
(基底クラスに存在しない、派生クラスで新しく定義した関数は呼び出せません)
派生クラスのインスタンスを基底クラスで受け取ることをアップキャストと言います。
(「子」から「親」へのキャスト)
基底クラスのインスタンスも、基底クラスにアップキャストしたインスタンスも「見た目」は同じ(Human
クラスのインスタンス)で、どちらも基底クラスとして使用できます。
しかしアップキャストしたインスタンスの実体は派生クラスです。
あるメンバ関数を呼び出すとき、アップキャストしたインスタンスではそのメンバ関数が派生クラス側でオーバーライドしていればそちらが呼び出されます。
オーバーロードしていなければ基底クラスのメンバ関数が呼び出されます。
基底クラスのインスタンスでは通常通り基底クラスで定義された関数が呼び出されます。
このように、同じ操作(名前)なのに異なる結果が得られるのが多様性の肝となる機能です。
アップキャストは参照やポインタの時のみ可能
ただし、上記のような使い方ができるのはインスタンスを参照またはポインタとして受け取る場合のみです。
引数をインスタンスのコピーを受け取った場合は、基底クラスの関数が呼び出されてしまい、多様性は実現できません。
//引数をインスタンスのコピーで受け取る
void action(const Human human)
{
human.speak();
}
int main()
{
Human john;
Japanese taro;
action(john);
action(taro);
}
Hello. Hello.
これは、派生クラスのインスタンスを基底クラスとしてコピーした時、派生クラスで新たに追加したメンバ情報が失われてしまうためです。
これはスライシングといい、避けるべきとされています。
隠蔽
見た目がオーバーライドに似ているものに隠蔽(いんぺい)というものがあります。
#include <iostream>
class BaseClass
{
public:
void print(int n) const
{
std::cout << n << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
//関数printを隠蔽
void print(const char *s) const
{
std::cout << s << std::endl;
}
};
int main()
{
BaseClass bc;
DerivedClass dc;
bc.print(10);
//dc.print(10); //エラー
dc.print("abc");
std::cin.get();
}
派生クラスでは、基底クラスに存在するメンバ関数と同じ名前のメンバ関数を定義しています。
なお、説明のためにあえて引数を変えていますが、引数などの定義を揃えても同じです。
このように記述すると、派生クラスのインスタンスからは基底クラスにある同名関数の存在が見えなくなり、アクセスができなくなります。
引数の種類を変えても同名関数のオーバーロードをしたことにはなりません。
ポインタなどを使用してもアクセスできず、多様性はありません。
メンバ関数の隠蔽は、基底クラスの関数をそっくり置き換えてしまうことになります。
意図して隠蔽するのならば構いませんが、大抵は単なるミスです。
せっかく機能を継承しているのに多様性の発揮できない形で別物で置き換えるのは効率が悪く、設計自体を見直した方がいいかもしれません。
ちなみに、派生クラスのインスタンスからは隠蔽された基底クラスのメンバ関数は使用できませんが、派生クラスのメンバ関数内からは基底クラス名::メンバ関数
という形で使用することはできます。
class BaseClass
{
public:
void print(int n) const
{
std::cout << n << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
//関数printを隠蔽
void print(const char *s) const
{
//隠蔽されたメンバ変数にアクセス
BaseClass::print(10);
std::cout << s << std::endl;
}
};
仮想関数とnew演算子
基底クラスは、派生クラスのインスタンスをポインタまたは参照で扱うことができます。
(アップキャスト)
そのため、以下のコードも有効です。
#include <iostream>
class BaseClass
{
};
class DerivedClass : public BaseClass
{
};
int main()
{
BaseClass *dc = new DerivedClass();
delete dc;
}
new演算子でインスタンスを生成すると、そのインスタンスのポインタが返ってきます。
ポインタですから、受け取る側の型が基底クラスであっても派生クラスのように扱うことができます。
ただし、この使い方は注意点があります。
派生クラスのメンバにアクセスできない
アップキャストしたポインタ変数の実体は派生クラスのインスタンスですが、基底クラスとして受け取っているため、基底クラスのメンバにしかアクセスできません。
#include <iostream>
class BaseClass
{
public:
void print()
{
std::cout << "BaseClass" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
void printDerive()
{
std::cout << "DerivedClass" << std::endl;
}
};
int main()
{
BaseClass *dc = new DerivedClass();
//dc->printDerive(); //エラー
delete dc;
std::cin.get();
}
アップキャストしたインスタンスは、ダウンキャストという方法で元の(派生クラスの)インスタンスに戻すことができます。
そのためこのような使い方に意味がないわけではないのですが、派生クラスのメンバを使いたい場合は素直に派生クラスとして受け取りましょう。
ダウンキャストについてはキャストの項で改めて説明します。
仮想デストラクタ
new演算子で派生クラスを基底クラスとして保存する場合、派生クラスのデストラクタが呼ばれなくなります。
#include <iostream>
class BaseClass
{
public:
BaseClass()
{
std::cout << "BaseClassコンストラクタ" << std::endl;
}
~BaseClass()
{
std::cout << "BaseClassデストラクタ" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
DerivedClass()
{
std::cout << "DerivedClassコンストラクタ" << std::endl;
}
~DerivedClass()
{
std::cout << "DerivedClassデストラクタ" << std::endl;
}
};
int main()
{
BaseClass *dc = new DerivedClass();
delete dc;
std::cin.get();
}
BaseClassコンストラクタ DerivedClassコンストラクタ BaseClassデストラクタ
インスタンスの生成はnew DerivedClass()
が行うので、派生クラスのコンストラクタにはアクセスできます。
しかし、その後は基底クラスとして扱われるため、インスタンスの破棄時に派生クラスのデストラクタが呼ばれません。
これを解決するには、デストラクタも仮想関数にします。
#include <iostream>
class BaseClass
{
public:
BaseClass()
{
std::cout << "BaseClassコンストラクタ" << std::endl;
}
virtual ~BaseClass() //仮想デストラクタ
{
std::cout << "BaseClassデストラクタ" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
DerivedClass()
{
std::cout << "DerivedClassコンストラクタ" << std::endl;
}
~DerivedClass() override //デストラクタのオーバーライド
{
std::cout << "DerivedClassデストラクタ" << std::endl;
}.
};
int main()
{
BaseClass *dc = new DerivedClass();
delete dc;
std::cin.get();
}
BaseClassコンストラクタ DerivedClassコンストラクタ DerivedClassデストラクタ BaseClassデストラクタ
仮想関数にされたデストラクタを仮想デストラクタと言います。
デストラクタが呼ばれないと、コンストラクタで確保したリソース(メモリ領域など)が正しく破棄されないため、継承して使用するクラスのデストラクタはvirtual
を付けておくことが推奨されます。
逆に言えば、デストラクタにvirtual
が付いていないクラスは安易に継承すべきではないかもしれません。
なお、デストラクタは戻り値も引数もありませんから、派生クラス側ではoverride
やvirtual
などの修飾子を書かなくても問題は起こりません。
書いても問題ないので、オーバーライドしていることを明確にしておきたい場合には書いておきましょう。
オーバーライドの禁止
仮想関数のオーバーライドを明示的に禁止したい場合はfinal
キーワードを指定します。
class BaseClass
{
public:
virtual void print() final //オーバーライドの禁止
{}
};
class DerivedClass : public BaseClass
{
public:
//オーバライドできない
//void print() override {}
};
例えばクラスのベースとなる処理を派生クラス側で変更してしまうなどの間違いを防ぐことができます。
派生クラス側では同じ名前の関数を定義できなくなるので、基底クラスのメンバ関数を誤って隠蔽してしまうミスを防ぐこともできます。