継承
クラスの継承
既存のクラスを基にして新しいクラスを作ることを,クラスの継承 (inheritance) といいます。
次のプログラムは,Clock クラスを継承した ChimeClock クラスを定義したものです。
#include <cstdio>
// 時計を表すクラス
class Clock
{
protected:
int hour;
int minute;
public:
Clock(int h, int m) : hour(h), minute(m) { }
virtual void print();
};
// チャイム付きの時計を表すクラス
class ChimeClock : public Clock // Clock クラスを継承
{
public:
ChimeClock(int h, int m) : Clock(h, m) { }
void print(); // print 関数をオーバーライド
};
void Clock::print()
{
printf("%02d:%02d\n", this->hour, this->minute);
fflush(stdout);
}
void ChimeClock::print()
{
Clock::print();
if (this->minute == 0) printf("ring!\n");
fflush(stdout);
}
int main()
{
Clock c1(5, 0);
c1.print(); // 出力: 05:00
ChimeClock c2(6, 0);
c2.print(); // 出力: 06:00, ring!
}
ChimeClock クラスは Clock クラスを継承しています。
ここで,Clock クラスは基底クラス (base class),ChimeClock クラスは派生クラス (derived class) と呼ばれます。
一般に,クラスの継承は次のように書きます。
class クラス名 : アクセス修飾子 基底クラス名
{
メンバの宣言
};
オーバーライド
基底クラスで既に定義されているメンバ関数を派生クラスで定義し直すことを,関数のオーバーライド (overriding) といいます。
オーバーライドを可能にするには,基底クラス側の関数宣言に virtual 指定子を付ける必要があります。
virtual 指定子の付いた関数は仮想関数 (virtual function) と呼ばれます。
ポリモーフィズム (多態性)
基底クラス型へのポインタまたは参照は,派生クラスのインスタンスを参照することができます。
先の Clock クラス,ChimeClock クラスに対して次のプログラムを実行する時,p->print() および r.print() で呼び出されるのは Clock::print() でしょうか,ChimeClock::print() でしょうか。
int main()
{
ChimeClock c(6, 0);
Clock* p = &c;
p->print(); // 出力: 06:00, ring!
Clock& r = c;
r.print(); // 出力: 06:00, ring!
}
この結果から,ポインタまたは参照の被参照型には関係なく,インスタンスの型に基づいて呼び出される関数が選択されていることがわかります。
このように,ポインタまたは参照からの関数呼び出しがインスタンスによって異なる振る舞いを示すことを,ポリモーフィズム (polymorphism) または多態性といいます。
コンストラクタ/デストラクタの呼び出し
基底クラスのコンストラクタが引数を持つ場合,今回の : Clock(h, m) のようにして,基底クラスのコンストラクタに引数を渡すことができます。
基底クラスのコンストラクタを明示的に呼び出さなかった場合,基底クラスのデフォルトコンストラクタが暗黙的に呼び出されます。
派生クラスのインスタンスを生成したとき,派生クラスのコンストラクタの前に,必ず基底クラスのコンストラクタが呼び出されます。
また,デストラクタについては,コンストラクタの逆の順序で呼び出されます。
#include <iostream>
class Base
{
public:
Base() { std::cout << "Base() called" << std::endl; }
~Base() { std::cout << "~Base() called" << std::endl; }
};
class Derived : public Base
{
public:
Derived() { std::cout << "Derived() called" << std::endl; }
~Derived() { std::cout << "~Derived() called" << std::endl; }
};
int main()
{
Derived d;;
}
<b>[出力]</b>
Base() called
Derived() called
~Derived() called
~Base() called
仮想デストラクタ
先のプログラムの main 関数を次のように変更すると,派生クラスのデストラクタが呼び出されなくなってしまいます。
int main()
{
Base* p = new Derived();
delete p;
}
<b>[出力]</b>
Base() called
Derived() called
~Base() called
この問題は,Base クラスのデストラクタを仮想デストラクタにすることで解消できます。
class Base
{
public:
virtual ~Base() { ... } // 仮想デストラクタ
};
アクセスレベルの継承
継承時に指定するアクセス修飾子によって,基底クラスメンバのアクセスレベルは,派生クラスでは次のように置換されて扱われます。
元のレベル | public 継承 | protected 継承 | private 継承 |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | private | private | private |
次のプログラムは,クラスの継承に private 継承を適用した例です。
class Base
{
public:
void func1() { }
};
class Derived : private Base
{
public:
void func2() { func1(); }
};
int main()
{
Derived d;
d.func1(); // 不可
d.func2(); // 可
}
純粋仮想関数
仮想関数の引数リストの後に = 0 を付けたものは純粋仮想関数と呼ばれ,関数の実装を派生クラスに強制させることができます。
純粋仮想関数を含むクラスは抽象クラス (abstract class) と呼ばれます。
抽象クラスは継承専用のクラスであり,インスタンス化できません。
class Base
{
public:
virtual void func1() = 0; // 純粋仮想関数
};
class Derived : public Base
{
public:
void func1() { ... } // 関数を実装
};
int main()
{
Base b; // エラー: 純粋仮想関数が含まれる
}
多重継承
C++ では,複数の基底クラスを継承した派生クラスを作る,多重継承が認められています。
class Derived : public Base1, public Base2 { ... };
仮想継承
図のような菱型継承を何も考えずに行うと,クラス ios のメンバにアクセスできません。
class ios { public: void func1() { } };
class istream : public ios { };
class ostream : public ios { };
class iostream : public istream, public ostream { };
int main()
{
iostream s;
s.func1(); // エラー: func1 が曖昧
}
C++ のコンパイラは,istream と ostream が継承する ios を独立したクラスと認識するため,メンバ名の衝突が起こります。
この問題を解消するには,次のようにして ios の継承に仮想継承を適用すればいいです。
class ios { public: void func1() { } };
class istream : public virtual ios { }; // 仮想継承
class ostream : public virtual ios { }; // 仮想継承
class iostream : public istream, public ostream { };