多重継承
今までの継承は、すべて基底クラスがひとつだけの継承で説明してきました。
これを単一継承といい、良く行われる継承です。
これに対して、基底クラスを複数指定して継承を行うこともできます。
これを多重継承と言います。
シンプルな多重継承のコード
#include <iostream>
class BaseA
{
public:
int num;
};
class BaseB
{
public:
char str[100];
};
class DerivedClass : public BaseA, private BaseB
{
void Print()
{
std::cout << num << str << std::endl;
}
};
サンプルコードのDerivedClassクラスは、BaseAクラスとBaseBクラスを基底クラスとする派生クラスです。
その中身は当然、それぞれの基底クラスを合わせた内容となります。
多重継承の文法は、引数の複数指定のようにコンマで区切るだけです。
その際、継承のアクセス指定子(public、protected、private)はそれぞれで異なっても構いません。
必要ならば、基底クラスを三つ、四つと増やすこともできます。
多重継承の注意点
継承関係の複雑化
多重継承は、ただでさえ複雑になりがちな継承の中でも特に複雑になります。
そのため、クラス設計をしっかりとして全体を把握しないと自分でもよくわからないコードとなり、バグを招きます。
不用意に多重継承を行うのは避けたほうが良いでしょう。
ただしインターフェイスクラスを含む多重継承はそれなりに実用性が高いです。
インターフェイスクラスには実装がなく、派生クラス側ですべてオーバーライドにより定義する必要があります。
基底クラスへの依存度が少なく、オーバーライドのし忘れによるバグもありません。
クラスを使う側からはそのクラスが「できること」が明示されるので、使いやすさも上がります。
名前の衝突
複数のクラスを基底クラスとするので、それぞれの基底クラスに同じ名前のメンバが存在すると、名前の衝突が起こります。
#include <iostream>
class BaseA
{
public:
int num;
void func() {}
};
class BaseB
{
public:
int num;
void func() {}
};
class DerivedClass : public BaseA, public BaseB
{
void Print()
{
//エラー: 名前が曖昧
std::cout << num << std::endl;
func();
}
};
これはスコープ解決演算子(::)を使用することで解決できます。
#include <iostream>
class BaseA
{
public:
int num;
void func() {}
};
class BaseB
{
public:
int num;
void func() {}
};
class DerivedClass : public BaseA, private BaseB
{
void Print()
{
std::cout << BaseA::num << std::endl;
BaseB::func();
}
};
ダイヤモンド継承(菱形継承)
C++では、派生クラスからさらに派生クラスを作ることができます。
派生クラスの派生クラスを、ここでは孫クラスと呼ぶことにします。
(親クラス(基底クラス)、子クラス(派生クラス)という呼び方から)
継承の継承を用いれば、以下のようにある基底クラスを継承するクラスを二つ作り、さらにそれら二つのクラスを多重継承した孫クラスを作ることが可能です。
class Base
{
};
class DerivedA : public Base
{
};
class DerivedB : public Base
{
};
//派生クラスを多重継承
class DerivedX : public DerivedA, public DerivedB
{
};
これを図で表すとこのようになります。
このような形になることから、これはダイヤモンド継承(菱形継承)と呼ばれます。
ダイヤモンド継承にはいくつかの注意点があります。
基底クラスの扱い
以下はメンバ変数とメンバ関数がそれぞれひとつだけの単純なクラスをダイヤモンド継承した例です。
派生クラスではメンバは定義せず、そのまま継承しただけです。
#include <iostream>
class Base
{
private:
int num;
public:
Base(int n) : num(n) {}
void Print()
{
std::cout << num << std::endl;
}
};
class DerivedA : public Base
{
public:
DerivedA(int n) : Base(n) {}
};
class DerivedB : public Base
{
public:
DerivedB(int n) : Base(n) {}
};
class DerivedX : public DerivedA, public DerivedB
{
public:
DerivedX(int n) : DerivedA(n), DerivedB(n) {}
};
int main()
{
DerivedX dx(10);
//エラー
dx.Print();
std::cin.get();
}
孫クラスDerivedXのインスタンスからメンバ関数Printを呼び出そうとしてもエラーになります。
メンバ関数Printの呼び出しはBaseクラスから直接呼び出せず、DerivedAかDerivedBかのどちらかを通して呼び出さねばなりません。
そのため、関数名だけではどちらを経由して呼び出して良いのかが曖昧になるのです。
これは今まで通りスコープ解決演算子を使用することで解決します。
それも併せて、上のコードを少しだけ変更してみます。
#include <iostream>
class Base
{
private:
int num;
public:
Base(int n) : num(n) {}
void Print()
{
std::cout << "Base: " << num << std::endl;
}
};
class DerivedA : public Base
{
public:
DerivedA(int n) : Base(n) {}
};
class DerivedB : public Base
{
public:
DerivedB(int n) : Base(n) {}
};
class DerivedX : public DerivedA, public DerivedB
{
public:
//引数を変更
DerivedX(int x, int y) : DerivedA(x), DerivedB(y) {}
};
int main()
{
DerivedX dx(10, 20);
//スコープ解決演算子を使用
dx.DerivedA::Print();
dx.DerivedB::Print();
std::cin.get();
}
孫クラスDerivedXのコンストラクタの引数を二つに増やし、DerivedAとDerivedBにそれぞれ別の引数を与えています。
Baseクラス、DerivedA、DerivedBクラスの定義は最初と同じです。
このコードの実行結果は以下となります。
Base: 10 Base: 20
Print関数をDerivedAから呼び出した時とDerivedBから呼び出したときとでは値が異なります。
つまり、C++でダイヤモンド継承をすると基底クラスの実体はふたつ存在することになります。
図にすると以下のようになります。
DerivedAが継承したBaseクラスと、DerivedBが継承したBaseクラスとではそれぞれ実体が異なり、メンバは別物として扱われています。
以下のようにBaseクラスにセッターを用意して値を書き換えてみると良くわかるでしょう。
#include <iostream>
class Base
{
private:
int num;
public:
void Set(int n) { num = n; }
void Print()
{
std::cout << "Base: " << num << std::endl;
}
};
class DerivedA : public Base
{
};
class DerivedB : public Base
{
};
class DerivedX : public DerivedA, public DerivedB
{
};
int main()
{
DerivedX dx;
dx.DerivedA::Set(10);
dx.DerivedB::Set(20);
dx.DerivedA::Print();
dx.DerivedB::Print();
std::cin.get();
}
実行結果は同じです。
Base: 10 Base: 20
仮想継承
C++では仮想継承という方法を用いると、基底クラスの実体をひとつにすることができます。
#include <iostream>
class Base
{
private:
int num;
public:
void Set(int n) { num = n; }
void Print()
{
std::cout << "Base: " << num << std::endl;
}
};
//仮想継承
class DerivedA : public virtual Base
{
};
//仮想継承
class DerivedB : public virtual Base
{
};
//多重継承
class DerivedX : public DerivedA, public DerivedB
{
};
int main()
{
DerivedX dx;
dx.DerivedA::Set(10);
dx.DerivedB::Set(20);
dx.DerivedA::Print();
dx.DerivedB::Print();
std::cin.get();
}
DerivedAとDerivedBの継承時に、virtualというキーワードを付けただけで、後は同じです。
実行結果は以下になります。
Base: 20 Base: 20
Print関数をDerivedAから呼び出しても、DerivedBから呼び出しても同じ値となります。
これはセッター(Set関数)でも同様で、どちらを通して値を書き換えても結局同じ実体の値を書き換えていることになります。
これは最初のイメージ図通りの動作と言えます。
仮想継承時のコンストラクタと基底クラスのメンバへのアクセス
仮想継承を用いると基底クラスの実体はひとつになりますから、曖昧さがなくなります。
そのため、孫クラスのインスタンスから基底クラスのメンバに直接アクセスが可能となります。
#include <iostream>
class Base
{
private:
int num;
public:
Base(int n) : num(n)
{
std::cout << "Base" << std::endl;
}
void Print()
{
std::cout << "Base: " << num << std::endl;
}
};
//仮想継承
class DerivedA : public virtual Base
{
public:
DerivedA() : Base(1)
{
std::cout << "DerivedA" << std::endl;
}
};
//仮想継承
class DerivedB : public virtual Base
{
public:
DerivedB() : Base(2)
{
std::cout << "DerivedB" << std::endl;
}
};
//多重継承
class DerivedX : public DerivedA, public DerivedB
{
public:
DerivedX(int n) : Base(n) //基底クラスのコンストラクタ呼び出し
{
std::cout << "DerivedX" << std::endl;
}
};
int main()
{
DerivedX dx(10);
dx.DerivedA::Print();
dx.DerivedB::Print();
dx.Print(); //Baseクラスのメンバに直接アクセス
std::cin.get();
}
それぞれのクラスのコンストラクタで自分のクラス名を出力するように変更しています。
そして、Baseクラスのコンストラクタ呼び出しには引数が必要です。
(デフォルトコンストラクタがない)
実行結果は以下になります。
Base DerivedA DerivedB DerivedX Base: 10 Base: 10 Base: 10
基底クラスから順にコンストラクタが呼び出されるのは通常の継承と同じです。
孫クラスDerivedXからは直接基底クラスBaseのコンストラクタを呼び出します。
DerivedA、DerivedBのコンストラクタには引数がないので今回は指定していませんが、必要ならばこれらの派生クラスのコンストラクタも孫クラスから呼び出します。
(今回はデフォルトコンストラクタが呼び出されています)
Baseクラスのコンストラクタは一回だけしか呼び出されていないこと、メンバ変数は孫クラスで指定された値になっていることに注目してください。
ダイヤモンド継承で孫クラスのインスタンスを生成する場合、基底クラスのコンストラクタ呼び出しは孫クラスから行われ、派生クラスDerivedAとDerivedBで定義している基底クラスのコンストラクタ呼び出しは使われません。
ただし、派生クラスのインスタンスを生成する場合には必要ですから、コンストラクタ呼び出しの記述を省略することはできません。
(デフォルトコンストラクタがあれば省略可能です)
同じように、孫クラスDerivedXのインスタンスからは、基底クラスBaseのメンバに直接アクセスできます。
スコープ解決演算子を使用すれば、明示的にDerivedAやDerivedBで継承されたメンバにアクセスすることもできます。
これはBaseのメンバ関数をオーバーライドし、動作を変更している場合などに意味のある呼び出し方です。