メモ帳ブログ @ wiki
クラスの継承と仮想関数
最終更新:
nina_a
-
view
クラスの継承と仮想関数
クラスの継承
クラスの継承とは他のクラスを内包するようなクラスを作ることである。内包されているクラスを基本クラス、内包しているクラスを派生クラスという。

図:クラスの継承の概要
クラスの継承は以下のように記述する。
class Dimension2 : public Dimension1{};
クラスのメンバのアクセス制御と同様に、クラスの継承にもprivateな継承、protectedな継承、publicな継承がある。上記の継承はpublicな継承である。privateな継承は以下のように記述する。
class Dimension2 : private Dimension1{};
派生クラスのメンバ関数からは、基本クラスのprotectedなメンバ、およびpublicなメンバにアクセスできる。これは継承の種類によらない。 一方、派生クラス外からの基本クラスのメンバ関数へのアクセスは、継承の種類によってさらに制限される。

図:privateな継承

図:protectedな継承

図:publicな継承
クラスの継承とコンストラクタ、デストラクタ
クラスを継承すれば、当然コンストラクタやデストラクタも継承される。つまり、派生クラスのオブジェクトを生成すれば、派生クラスのコンストラクタと基本クラスのコンストラクタの両方が実行される。デストラクタも同様である。
そのコンストラクタの実行順序は「基本クラス→派生クラス」の順である。派生クラスのコンストラクタは基本クラスの初期化も含んでいるため、基本クラスの後に実行される。何も記述しなくても基本クラスのコンストラクタは自動実行されるが、基本クラスのコンストラクタに引数を与えたい場合には、初期化リストの部分に記述する。
そのコンストラクタの実行順序は「基本クラス→派生クラス」の順である。派生クラスのコンストラクタは基本クラスの初期化も含んでいるため、基本クラスの後に実行される。何も記述しなくても基本クラスのコンストラクタは自動実行されるが、基本クラスのコンストラクタに引数を与えたい場合には、初期化リストの部分に記述する。
class Dimension2 : public Dimension1{
public:
Dimension2() : Dimension1() { // ここで基本クラスのコンストラクタ呼び出し
}
}
一方、デストラクタの実行順序は「派生クラス→基本クラス」の順に実行される。これも、派生クラスの後処理が基本クラスのメンバに依存することがあることを考えれば自然。
オブジェクトの代入互換性
ある変数AとBがあり、A=BおよびB=Aが実行できるとき、変数AとBには代入互換性があるという。
オブジェクトの代入互換性とは基本型へのポインタに派生型のオブジェクトのアドレスを代入しても良いということである。つまり、以下のコードが実行可能である。
オブジェクトの代入互換性とは基本型へのポインタに派生型のオブジェクトのアドレスを代入しても良いということである。つまり、以下のコードが実行可能である。
Dimension1* p;
Dimension2 d2;
p = &d2;基本型への回帰
以下のようなクラスを考える。なお、本筋でない関数についてはここでは省略してある。
class Dimension1{
protected:
int x;
public:
void print(void){
std::cout << x << std::endl;
}
};
class Dimension2 : public Dimension1{
protected:
int y;
public:
void print(void){
std::cout << x << "," << y << std::endl;
}
};
オブジェクトの代入互換性から以下のコードは実行可能である。
Dimension2 d2(1,4);
Dimension1 *p = &d2; // 基本型へのポインタに派生型へのアドレスを代入している
p->print();
この場合、4行目のp->print();で実行されるのは基本型のprint()である(派生型のオブジェクトのアドレスにも関わらずである)。これが基本型への回帰である。しかし、派生型にprint()が用意されているにも関わらず、基本型のprint()が実行されるのはやはり不便である。どうしても派生型のprint()を実行したいときで、ポインタが派生型のオブジェクトを指していることが確実である場合には、キャストによって実現できる。
((Dimension2*) p)->print();
この場合、pがDimension2のオブジェクトを指していない場合、予期せぬ動作が起こるため、この方法はスマートではない。実際には以下の仮想関数を用いるのが普通である。
仮想関数
仮想関数とはvirtualというキーワードがつけられたメンバー関数の事である。なおvirtualをつけるのは宣言のみで、定義の所につける必要はない。
classs Dimension1{
virtual void print(void); // printは仮想関数
}
void Dimension1::print(void){ // ここにはvirtualはつけない
:
}
仮想関数を普通に使う分には何の意味もない。仮想関数はクラスを派生し、オーバーライドすることによって初めて役に立つ。
Dimension1のprint関数を仮想関数に変更する。
Dimension1のprint関数を仮想関数に変更する。
class Dimension1{
protected:
int x;
public:
void virtual print(void){
std::cout << x << std::endl;
}
};
そして、以下のコードを実行する。
Dimension1 d1(3);
Dimension2 d2(1,3);
Dimension1 *p = &d1;
p->print(); // Dimension1のprintが実行される
p = &d2;
p->print(); // Dimension2のprintが実行される
すると、ポインタの指す先がDimension1のオブジェクトである場合は、Dimension1のprintが実行され、Dimension2のオブジェクトである場合にはDimension2のprintが実行される。つまり、「基本型への回帰」を回避することが出来るのである。
純粋仮想関数
クラスに仮想関数を作成する場合に、関数の中身が空と言うことがある。つまり、以下のようなコードである。
virtual void show(void){}
これは、クラスが派生によりshow関数がオーバーライドされることを前提としているのである。このような場合には純粋仮想関数を用いた方がより安全であり、ファイルサイズを節約することにもつながる。純粋仮想関数を宣言するには=0をつける。
virtual void show(void) = 0; 抽象クラス
純粋仮想関数をメンバーに持つクラスを抽象クラスという。抽象クラスは定義されていない関数を持つ不完全なクラスである。そのため、抽象クラスのオブジェクトを作ることは出来ない。また、派生クラスが純粋仮想関数を定義していない場合は、その派生クラスも抽象クラスとなる。
dynamic_cast
基本クラスのポインタから派生クラス固有の関数を利用するにはキャストをしてやる必要があることは前述の通りである。継承関係があり、仮想関数が存在しているならdynamic_castを用いて安全にキャストすることが出来る。
ここでは例としてDimension2クラスにdistance関数を追加した。
ここでは例としてDimension2クラスにdistance関数を追加した。
double Dimension2::distance(void){
return sqrt(x*x+y*y);
}
以下にdynamic_castを用いてキャストする例を示す。
Dimension1 d1(3);
Dimension2 d2(1,3);
Dimension1* p = &d2;
Dimension2* p2 = dynamic_cast<Dimension2*>(p); // pはDimension2のオブジェクトを指している
if(p2 != 0){
std::cout << p2->distance() << std::endl;
}else{
std::cerr << p << " : Not Dimension2.\n";
}
p = &d1;
p2 = dynamic_cast<Dimension2*>(p); // pはDimension1のオブジェクトを指している
if(p2 != 0){
std::cout << p2->distance() << std::endl;
}else{
std::cerr << p << " : Not Dimension2.\n";
}
【実行結果】
3.16228
0x60000fffffffaa00 : Not Dimension2.
カテゴリ:C/C++
