継承
継承とは
クラスの重要な機能のひとつに継承があります。
これはあるクラスの機能を受け継いで、新しいクラスを作り出す機能です。
言葉にすると単純な機能ですが、継承関係の機能はやや複雑です。
継承はオブジェクト指向プログラミングの要素である多様性(ポリモーフィズム)に深くかかわります。
上手く使えば便利ですが、下手に使用するとコードが複雑になるだけでメリットが感じられないことも多い機能です。
このあたりから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
{
public:
//デフォルトコンストラクタなし
ClassA(int n) {}
};
class ClassB
{
public:
ClassA a;
//デフォルトコンストラクタを持たないクラスのインスタンスは
//コンストラクタ内では初期化できない
//ClassB()
//{
// a(10);
//}
//ClassBのコンストラクタの実行前に
//ClassAの引数付きコンストラクタを呼び出して初期化する
ClassB() : a(10){}
};
ただし、new演算子を使用すれば以下のような初期化は可能です。
(非静的メンバ変数の初期化)
class ClassA
{
public:
//デフォルトコンストラクタなし
ClassA(int n) {}
};
class ClassB
{
public:
ClassA *a = new ClassA(10);
};
メンバイニシャライザ
基底クラスの引数付きコンストラクタの呼び出しはコンストラクタの項で紹介したメンバイニシャライザの書き方と同じです。
メンバイニシャライザはメンバ変数の効率の良い初期化方法である、と説明しました。
これは代入による初期化と、メンバイニシャライザによる初期化では変数に値を入れる「手数」が異なるためです。
class TestClass
{
int number;
std::string name;
//代入による初期化(本当は初期化とは呼ばない)
TestClass()
{
number = 0;
name = "no name";
}
//メンバイニシャライザ
//それぞれのクラスのコンストラクタを呼び出して初期化
TestClass(): number(0), name("no name")
{
//既にメンバ変数は初期化されているので何もしない
}
};
C++では、stringなどのクラスはもちろんint型などの組み込みのデータ型(プリミティブ型)もコンストラクタを持っています。
そして、クラスのメンバ変数は、そのクラスのコンストラクタが実行される直前にコンストラクタの呼び出しが完了しています。
つまり、コンストラクタ内でメンバに値を代入する初期化方法では、メンバ変数numberに0を代入するより前に「何もしない」デフォルトコンストラクタが呼び出されているのです。
その後に0を代入することになり、「二度手間」となります。
これに対してメンバイニシャライザは、最初にメンバ変数のコンストラクタが呼び出される時に、引数付きコンストラクタを呼び出すことができます。
TestClassのコンストラクタを実行する段階では既にメンバ変数の初期化は完了しているため、後から代入する処理が必要なくなるため効率が良いのです。
なお、メンバイニシャライザはクラスのメンバ変数を宣言した順番と同じ順番で記述してください。
クラスでメンバ変数を宣言した順番にそれぞれのコンストラクタが呼ばれるためです。