継承
継承とは
クラスの重要な機能のひとつに継承があります。
これはあるクラスの機能を受け継いで、新しいクラスを作り出す機能です。
言葉にすると単純な機能ですが、継承関係の機能はやや複雑です。
継承はオブジェクト指向プログラミングの要素である多様性(ポリモーフィズム)に深くかかわります。
上手く使えば便利ですが、下手に使用するとコードが複雑になるだけでメリットが感じられないことも多い機能です。
このあたりからC++(というかオブジェクト指向の話)は中々ややこしくなってきます。
最初のうちは無理して使うことはないので、こういう機能もある、という程度に読んでください。
継承を使ったシンプルなコード
まずは簡単に継承機能を使ってみます。
#include <iostream>
//基底クラス
//(継承されるクラス)
class BaseClass
{
public:
void print()
{
std::cout << "BassClass\n";
}
};
//派生クラス
//(継承するクラス)
class DerivedClass : public BaseClass
{
public:
//新しい機能の追加
void printNew()
{
std::cout << "DerivedClass\n";
}
};
int main()
{
//派生クラスのインスタンスを生成
DerivedClass dc;
//派生クラスのインスタンスから
//基底クラスのメンバ関数を呼び出す
dc.print();
//派生クラスで定義したメンバ関数
dc.printNew();
std::cin.get();
}
BaseClass DerivedClass
まず、継承の元となるクラスであるBaseClass
を定義しています。
このクラスは基底クラス(スーパークラス、親クラス)と呼ばれます。
サンプルでは、メンバ関数ひとつだけの単純なクラスです。
次に、BaseClass
を元として新しいクラスであるDerivedClass
を定義します。
このクラスは派生クラス(サブクラス、子クラス)と呼ばれます。
こちらもメンバ関数ひとつだけの単純なクラスですが、基底クラスの機能を受け継いでいます。
main関数では、派生クラスのインスタンスのみを生成しています。
派生クラスで定義したメンバ関数を呼び出せるのは当然ですが、基底クラスのメンバ関数も呼び出すことができます。
(33行目)
このような機能が継承です。
継承の仕方
継承の元となる基底クラスは、なんの変哲もない普通のクラスです。
派生クラスでは、クラス名の記述の後に: public
を記述してから、継承したいクラス名を指定します。
class BaseClass
{};
class DerivedClass : public BaseClass
{};
基底クラス名を記述するのは問題ないと思います。
「public」というキーワードにももちろん意味はありますが、これはひとまず横に置いておきます。
継承は基本的に派生クラス名 : public 基底クラス名
という形で行う、と考えてください。
なお、派生クラスを定義する前に基底クラスがすでに定義されている必要があります。
サンプルコードの基底クラス、派生クラスを記述する順番を入れ替えるとエラーになります。
//エラー、BaseClassがまだ定義されていない
class DerivedClass : public BaseClass
{
};
class BaseClass
{
};
privateメンバにはアクセスできない
派生クラスから使用できるのは、基底クラスでpublic
もしくはprotected
で宣言されたメンバです。
private
メンバにはアクセスすることはできません。
class BaseClass
{
private:
int privateVal;
void privateFunc() {}
protected:
int protectedVal;
void protectedFunc() {}
public:
int publicVal;
void publicFunc() {}
};
class DerivedClass : public BaseClass
{
public:
void func()
{
//privateVal = 0; エラー
protectedVal = 0;
publicVal = 0;
//privateFunc(); //エラー
protectedFunc();
publicFunc();
}
};
合成(包含)
継承に似た概念に合成(包含)というものがあります。
これは「あるクラスに、別のクラス(のインスタンス)が含まれる」というものです。
class TestClass
{
int number;
std::string name;
public:
TestClass(int n = 0, const char *s = "")
{
number = n;
name = s;
}
};
TestClassのメンバ変数nameはstringクラスのインスタンスです。
この時点でTestClassはstringクラスを含んでいることになり、合成になっています。
合成とはこれだけのことで、今までも普通にやってきたことです。
合成は、別のクラスを含むのでそのクラスの設計に依存しますが、継承よりも依存度が低い関係性になります。
(含むクラスの仕様が変更されても影響が少ない)
is-a関係とhas-a関係
継承も合成も「BはAを含む」という点では同じです。
しかしその関係は少し異なります。
継承は「BはAである」(BはAの一種である)という関係が成り立ちます。
これをis-a関係といいます。
例えば「犬は動物である」「車は乗り物である」という関係です。
このような時は、動物クラスを継承して犬クラスを作る、乗り物クラスを継承して車クラスを作る、という設計が有効です。
合成は「BはAを持っている」という関係が成り立ちます。
これをhas-a関係といいます。
例えば「犬は尻尾を持っている」「車はハンドルを持っている」という関係です。
この時、「犬は尻尾である」「車はハンドルである」という関係は成り立ちません。
BにAが含まれるのは同じですが、こういうものは継承ではなく合成で設計したほうが上手くいくことが多いでしょう。
継承とコンストラクタ
呼ばれる順番
派生クラスのインスタンスを生成すると派生クラスのコンストラクタが呼び出されますが、実はその前に基底クラスのコンストラクタも呼び出されます。
呼び出される順番は「基底クラスのコンストラクタ」→「派生クラスのコンストラクタ」→「インスタンス生成」となります。
インスタンスを破棄する際はその逆で、「派生クラスのデストラクタ」→「基底クラスのデストラクタ」→「インスタンス破棄」という順番になります。
#include <iostream>
class BaseClass
{
public:
BaseClass()
{
std::cout << "BassClassコンストラクタ" << std::endl;
}
~BaseClass()
{
std::cout << "BassClassデストラクタ" << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
DerivedClass()
{
std::cout << "DerivedClassコンストラクタ" << std::endl;
}
~DerivedClass()
{
std::cout << "DerivedClassデストラクタ" << std::endl;
}
};
int main()
{
DerivedClass *dc = new DerivedClass();
delete dc;
std::cin.get();
}
BassClassコンストラクタ DerivedClassコンストラクタ DerivedClassデストラクタ BassClassデストラクタ
委譲コンストラクタ
派生クラスのインスタンスを生成する際に、基底クラスの引数付きコンストラクタを呼び出してメンバを初期化するには以下のようにします。
#include <iostream>
#include <string>
class BaseClass
{
public:
int number;
std::string name;
BaseClass(int n = 0, const char* s = "")
: number(n), name(s) {}
void print()
{
std::cout << number << std::endl;
std::cout << name << std::endl;
}
};
class DerivedClass : public BaseClass
{
public:
//基底クラスのコンストラクタ呼び出し
DerivedClass(int n = 0, const char* s = "")
: BaseClass(n, s) {}
};
int main()
{
DerivedClass dc(1, "John");
dc.print();
std::cin.get();
}
派生クラスのコンストラクタの引数定義の後ろに: 基底クラス名(引数...)
という形で記述すると、基底クラスのコンストラクタを呼び出すことができます。
この時、派生クラスのコンストラクタの引数に指定された値を使用することができます。
処理の順番は変わらず、「基底クラスコンストラクタ」→「派生クラスコンストラクタ」という順になります。
つまり、派生クラスのコンストラクタ内に処理が移った時点ではすでに基底クラスのコンストラクタは処理が完了しているということです。
デフォルトコンストラクタを持たないクラスの初期化
デフォルトコンストラクタを持たないクラスのインスタンスは、必ず宣言と同時に初期化が必要となります。
しかし、そのクラスを別のクラスのメンバ変数として持つ場合、宣言と同時に初期化することができません。
別クラスのコンストラクタが呼ばれた時では遅く、それよりも前での初期化が必要となります。
class ClassA
{
int x;
public:
//デフォルトコンストラクタあり
ClassA() { x = 1; }
};
class ClassB
{
int x;
public:
//デフォルトコンストラクタなし
ClassB(int n) { x = n; }
};
class ClassC
{
public:
ClassA a;
ClassB b;
int n;
ClassC()
{
//この時点ですでにaとbの初期化処理は終わっている
//ClassAはデフォルトコンストラクタが呼ばれているが
//ClassBにはデフォルトコントラスタがないのでコンストラクタを実行できず
//コンパイルエラーになる
//以下のようにコンストラクタを実行しようとしてもダメ
//この時点ですでにインスタンスが生成されていなければならないので
//改めてコンストラクタは呼べない
//a();
//b(3);
//これは初期化ではなくただの代入
n = 0;
}
};
このような場合はメンバイニシャライザを使用して初期化します。
メンバイニシャライザは名前の通りメンバの初期化に使用できます。
class ClassA
{
int x;
public:
//デフォルトコンストラクタあり
ClassA() { x = 1; }
};
class ClassB
{
int x;
public:
//デフォルトコンストラクタなし
ClassB(int n) { x = n; }
};
class ClassC
{
public:
ClassA a;
ClassB b;
ClassC()
: a(), b(3) {}
};
なお、インスタンスをポインタで持つ場合はnew
演算子で任意のタイミングでコンストラクタを呼ぶことができます。
class ClassA
{
int x;
public:
ClassA() { x = 1; }
};
class ClassB
{
int x;
public:
ClassB(int n) { x = n; }
};
class ClassC
{
public:
ClassA *a;
ClassB *b = new ClassB(3);
ClassC()
{
a = new ClassA();
}
};
継承の禁止
クラスの継承を禁止したい場合はクラス定義にfinal
キーワードを指定します。
class BaseClass
{
};
//これ以上の継承は禁止
class DerivedClass final : public BaseClass
{
};
//継承できない
//class DerivedClass2 : public DerivedClass
//{
//};
継承は継承元クラスに機能を追加したり動作を変更したりできるのですが、クラスが複雑になってくると全体を把握して正しく機能変更することが困難になり、誤った実装をしてしまう危険性があります。
継承そのものを禁止してしまうことでコードの安全性を高めるメリットがあります。
その反面、自由度は低下します。
継承の禁止はコードをライブラリ等にして他人に使ってもらう際に使用を検討すると良いでしょう。