C++編で扱っている C++ は 2003年に登場した C++03
という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14
-> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++
の学習を始めるのなら、C++11
よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい
C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。
この章の概要です。
これまでの章では、1つのクラスから継承を行って、派生クラスを定義していましたが、2つ以上のクラスから継承することも可能です。このような、複数のクラスから継承を行うことを、多重継承と呼びます。また、1つのクラスからの継承を、多重継承と対比させて、単一継承と呼びます。
多重継承を行うと、複数ある基底クラスのそれぞれのメンバが、派生クラスに引き継がれます。
多重継承の構文は、以下のようになります。
class クラス名 : アクセス指定子 基底クラス名, アクセス指定子 基底クラス名 {};
このように、「,」で区切って、複数の基底クラスを指定します。それぞれの継承に指定するアクセス指定子は、異なる種類のものが混在しても問題ありません。
class Base1 {};
class Base2 {};
class Derived : public Base1, private Base2 {};
この場合、Base1 を「公開継承」、Base2 を「非公開継承」します。
多重継承を使って、次のような構造を作ると、厄介な問題を引き起こすことがあります。
class Ancestor {};
class Base1 : public Ancestor {};
class Base2 : public Ancestor {};
class Derived : public Base1, public Base2 {};
つまり、多重継承の基底になる2つのクラス(3つ以上でも同様)が、1つの共通クラスから派生している構造です。この継承構造を図にすると、次のようになります。
ポイントは、Ancestor が2カ所にあることです。
このような構造になるということは、Derivedクラスをインスタンス化すると、Ancestor は2つできるということであり、ここに問題が潜んでいます。つまり、Derivedクラスのオブジェクトから、Ancestorクラスのメンバへアクセスしようとすると、「どの Ancestor なのか?」が曖昧になってしまう訳です。
どのような形で曖昧さが問題になるのか、見ていきましょう。まず、Ancestorクラスにメンバ関数がある場合を考えてみます。
class Ancestor {
public:
void f1() {}
virtual void f2() {}
};
class Base1 : public Ancestor {
public:
virtual void f2() {}
};
class Base2 : public Ancestor {};
class Derived : public Base1, public Base2 {};
int main()
{
* d = new Derived();
Derived->f1();
d->f2();
ddelete d;
}
このプログラムはコンパイルに失敗します。問題なのは、f1、f2 の呼び出しがともに曖昧であることです。 f1 は Ancestorクラスでしか定義されていませんが、Derived から見ると、Base1 の基底としての Ancestor と、Base2 の基底としての Ancestor があるため、どちらの f1 を呼び出そうとしているのか判断できません。
曖昧さを無くすには、Ancestorクラスのメンバへのアクセスが、Base1経由で行うべきものなのか、Base2経由で行うべきものなのかを明示する必要があります。そのための方法としては、以下の2つがあります。
static_cast<Base1*>(d)->f1();
->Base2::f1(); d
1つは、キャストを使って、ポインタ(あるいは参照)を基底クラスを指すように変換することです。この方法で解決できるということは、いったん、基底クラスのポインタ型(あるいは参照型)の変数を経由させるのでも構いません。基底クラス型への変換は暗黙的に行えるので、この方法ならば static_cast も不要です。
もう1つの方法は、スコープ解決演算子 ::
を使って、基底クラスの名前を明示することです。やや見慣れない構文ですが、これも有効です。
f1 に関していえば、Ancestorクラスでしか定義されていないので、曖昧さの解決を求められることは少々不服に感じますが、f2 は仮想関数であり、Base2 だけでオーバーライドされていますから、曖昧さの解決は重大な意味を持ちます。
今度は、Ancestorクラスがメンバ変数を持っている場合を考えてみましょう。
静的でないメンバ変数は、オブジェクト1つごとにメモリ領域を確保しなければなりませんから、Ancestor を2つ含む形になると、メモリも2カ所に確保されることになります。メモリの使用量が増えることが問題というわけではなく、Base1経由で見たメンバ変数と、Base2経由で見たメンバ変数が別物であるという点が問題になり得ます。
staticメンバ変数の場合は、クラスごとに1つのメモリ領域を取るので、重複することはありません。
確認してみましょう。
#include <iostream>
class Ancestor {
public:
() : mValue(0)
Ancestor{}
inline void Print() const
{
std::cout << mValue << std::endl;
}
protected:
inline void SetValue(int value)
{
= value;
mValue }
private:
int mValue;
};
class Base1 : public Ancestor {
public:
inline void Set(int value)
{
(value);
SetValue}
};
class Base2 : public Ancestor {
public:
inline void Set(int value)
{
(value);
SetValue}
};
class Derived : public Base1, public Base2 {};
int main()
{
* d = new Derived();
Derived->Base1::Set(100);
d->Base2::Set(200);
d->Base1::Print();
d->Base2::Print();
ddelete d;
}
実行結果:
100
200
Base1 経由で設定した値と、Base2 経由で設定した値とが、別物として扱われていることが分かると思います。
多重継承の際に、共通の基底クラスを、本当にただ1つの実体として持ちたい場合には、仮想継承を用います。仮想継承を行うには、基底クラスを指定する際に virtual指定子を付加します。
class Ancestor {};
class Base1 : public virtual Ancestor {};
class Base2 : public virtual Ancestor {};
class Derived : public Base1, public Base2 {};
こうすると、継承構造は次の図のように、菱形(ダイアモンド形)になります。
このように、Base1 経由でみた Ancestor と、Base2 経由でみた Ancestor は同一のものになりました。なおこのとき、Ancestor は、仮想基底クラスと呼ばれます。前の項で試したプログラムを、仮想継承に変更して再度実行して確認してみます。
#include <iostream>
class Ancestor {
public:
() : mValue(0)
Ancestor{}
inline void Print() const
{
std::cout << mValue << std::endl;
}
protected:
inline void SetValue(int value)
{
= value;
mValue }
private:
int mValue;
};
class Base1 : public virtual Ancestor {
public:
inline void Set(int value)
{
(value);
SetValue}
};
class Base2 : public virtual Ancestor {
public:
inline void Set(int value)
{
(value);
SetValue}
};
class Derived : public Base1, public Base2 {};
int main()
{
* d = new Derived();
Derived->Base1::Set(100);
d->Base2::Set(200);
d->Base1::Print();
d->Base2::Print();
ddelete d;
}
実行結果:
200
200
後から設定した方の値 200 しか、出力されていないことが分かります。
ところで、Ancestor が1つになったということは、Printメンバ関数の呼び出しについては、曖昧さが無くなったはずです。実際、次のように呼び出すことができます。
->Print(); d
問題① 「飛べるもの」を表す抽象クラス FlyObject と、「乗り物」を表す抽象クラス Vehicle があるとき、「飛行機」のクラスを多重継承を使って定義してください。FlyObject と Vehicle はそれぞれ、速度を表すメンバ変数 mSpeed と、それを返す GetSpeedメンバ関数を持つものとします。
問題② 問題①において、FlyObjectクラスを廃止して、代わりに「飛べるもの」を表すインターフェース IFly を導入したら、派生クラスはどう変化するでしょうか。IFlyインターフェースは、「飛ぶ」という動作にしか興味がないので、Fly という純粋仮想関数と仮想デストラクタのみを持ちます。
サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)
新規作成。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
RSS | 管理者情報 | プライバシーポリシー |