派生クラス | Programming Place Plus C++編【言語解説】 第26章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


関連する話題が、以下のページにあります。

継承

この章からしばらく、継承という、オブジェクト指向プログラミングにおける重要概念について取り上げます。

クラスは、他のクラスのメンバを引き継いで作り出すことができ、このようにしてクラスを定義することを派生と呼びます。また、メンバを引き継ぐという部分を指して、継承と呼びます。

また、派生元になるクラスを基底クラス(親クラス、スーパークラス)と呼び、派生によって作られたクラスを派生クラス(子クラス、サブクラス、導出クラス)と呼びます。

「派生クラスは、基底クラスを継承する」というような言い方をすることもあり、この場合、「派生」と「継承」がまったく同じ感覚で使われています。用語には混乱が見られますが、C++編では、基底クラス、派生クラスという用語を用い、派生クラスを定義することを継承と呼ぶことで統一します。

派生クラス

派生クラスは、次のように定義します。

class クラス名 : public 基底クラス名 {
};

派生クラスの名前の直後に「: public 基底クラス名」というように続けることで、継承を表現できます

public のようなアクセス指定子が登場することに違和感があるかもしれませんが、ここで public を使うことで、基底クラスのすべてのメンバを、そのまま派生クラスに引き継ぐのだという意思表示になっています。最初のうちは、publicキーワードを書き忘れることがあるかもしれませんが、public がなくてもコンパイルエラーにはなるとは限らず、意味だけが変わってしまうので注意してください。

publicキーワードを使わない継承については、第28章で取り上げます。

なお、派生クラスの定義を書く位置から、基底クラスの定義が見えていなければならず、クラスの前方宣言(第25章)では対応できません。

また、別の名前空間にある基底クラスから継承することも可能です。もちろん、名前空間名で修飾したり、using を使ったりして、曖昧でないようにする必要があります。

ちなみに、継承を行ったからといって、基底クラスを単体で使用できないわけではありません。

class Base {
};

class Derived : public Base {
};

Base b;       // Baseクラスのメンバだけが使用できる
Derived d;    // Baseクラスのメンバ+Derivedクラスのメンバが使用できる


公開継承

これまで、アクセス指定における public を「公開」と表現してきた(第12章)のと同様に、public を使った継承を「公開継承」と呼ぶことがあります。

単に「継承」といった場合、「公開継承」のことを指すのが普通ですが、C++ には、他にもいくつかの継承の形があるため、区別が必要な場面では「公開継承」と書きます。

公開継承の場合、基底クラスのメンバは、そのアクセス指定についてもそのまま引き継ぎます。

class Base {
public:
    void f1() {}

private:
    void f2() {}
};

class Derived : public Base {
};


int main()
{
    Derived d;

    d.f1();  // OK
    d.f2();  // エラー。f2 は「非公開」
}

基底クラス側で「非公開」なメンバは、派生クラスにも公開されていないことに注意してください。たとえば、

class Base {
public:
    void f1() {}

private:
    void f2() {}
};

class Derived : public Base {
public:
    void g()
    {
        f2();  // エラー。f2 は「非公開」
    }
};

Derivedクラスの gメンバ関数は、基底クラスの f2メンバ関数を呼び出そうとしていますが、f2 は「非公開」であるため、コンパイルエラーになります。(以降、クラスA の fメンバ関数のことを、A::fメンバ関数と表記します)。

基底クラス側にしてみれば、いつ誰が継承を行い、新たなクラスを作り出すか分かりません。サンプルプログラムの Baseクラスと Derivedクラスを観察してみてください。Baseクラス側には、継承に関するコードは何1つ含まれていないのです。

そのため、クラスが自分自身で使うだけのメンバを、継承先で勝手に使用されないように身を守るために、「非公開」なメンバが、派生クラス側からアクセスできないという事実は重要です。

もう少し、実践的な例を挙げてみます。

class Pen {
public:
    // 色を表す型
    // RGB(赤・緑・青)をそれぞれ 16進数2桁 (0x00~0xff) で表し、
    // 0xff8000 のように指定する(この場合、R=0xff, G=0x80, B=0x00)
    typedef unsigned int Color_t;

public:
    explicit Pen(Color_t color) :
        mColor(color)
    {}

    void DrawLine(int x1, int y1, int x2, int y2)
    {
        // mColor の色を使って、(x1,y1) から (x2,y2) に向かって直線を描く
    }

private:
    Color_t  mColor;
};

class TwoColorPen : public Pen {
public:
    TwoColorPen(Color_t color1, Color_t color2) :
        Pen(color1), mAnotherColor(color2)
    {}

    void DrawAnotherColorLine(int x1, int y1, int x2, int y2)
    {
        // mAnotherColor の色を使って、(x1,y1) から (x2,y2) に向かって直線を描く
    }

private:
    Color_t  mAnotherColor;
};


int main()
{
    Pen pen1(0x000000);                            // 黒いペン
    TwoColorPen pen2(0x000000, 0xff0000);          // 黒と赤の2色ペン

    pen1.DrawLine(100, 100, 200, 200);             // 黒い線を描く
    pen2.DrawLine(150, 100, 250, 200);             // 黒い線を描く
    pen2.DrawAnotherColorLine(150, 100, 250, 200); // 赤い線を描く
}

Penクラスは1色だけのペンで、DrawLineメンバ関数を使って、直線を描くことを想定しています。一方、TwoColorPenクラスは、2色を同時に保持でき、別々の色で直線を描くことを想定しています。

1色だけを使った処理は Penクラスにあるので、TwoColorPenクラスは、Penクラスの機能を使わせてもらうために、公開継承して定義しています。

まず、今回の例では、メンバ関数だけなく、メンバ変数やコンストラクタ、typedef も引き継いでいることに注目してください。このように、継承で引き継げるメンバは、文字どおり「すべてのメンバ」です。他にも、デストラクタ、演算子オーバーロード、列挙型、入れ子クラス、staticメンバといったものも含みます。

TwoColorPenクラスのコンストラクタにおいて、メンバイニシャライザのところに、基底クラス名が含まれていますが、これは、基底クラスのコンストラクタが引数を持つ場合に、それを呼び出すための構文です。基底クラスがデフォルトコンストラクタを持っていない場合、必ずメンバイニシャライザを使って、明示的に呼び出さなくてはなりません

公開継承では、基底クラス型を要求している箇所で、派生クラス型のオブジェクトをそのまま使えます。これは、実体でもポインタや参照でも同様です。

void DrawLine(Pen& pen, int x1, int y1, int x2, int y2)
{
    pen.DrawLine(x1, y1, x2, y2);
}

int main()
{
    Pen pen1(0x000000);                     // 黒いペン
    TwoColorPen pen2(0x000000, 0xff0000);   // 黒と赤の2色ペン

    DrawLine(pen1, 100, 100, 200, 200);     // 黒い線を描く
    DrawLine(pen2, 100, 100, 200, 200);     // 黒い線を描く
}

この例では、DrawLine関数の第1引数は Pen型の参照ですから、Penクラスのオブジェクトを指定できることはもちろんのこと、Penクラスを継承した TwoColorPenクラスのオブジェクトを指定できます。

このように、基底クラス型で扱うことで、オブジェクトが本当に基底クラスからインスタンス化されたものなのか、派生クラスからインスタンス化されたものなのかを気にすることなく、共通のコードを利用できます。ただし、型が基底クラスのものになっているため、派生クラス側で宣言されたメンバを使用できません。

基底クラス型を指すポインタや参照から、派生クラス側のメンバを使う方法として、いったん、派生クラス型(のポインタや参照)へキャストする手があります。このようなキャストは、継承構造の下位のクラスへのキャストなので、ダウンキャストと呼ばれます。この辺りの詳細は、第30章で扱います。

is-a関係

オブジェクト指向プログラミングにおける継承という概念は、その使い道によっていくつか分類できます。ここまでに取り上げた公開継承の例は、基底クラスの機能はそのまま使い、派生クラス側でプラスアルファの機能を付け加えるという形で、これは「拡張のための継承」であるといえます。

拡張のための継承は、すでにあるクラスが持っている機能(コード)をもう1回書き直すことを避け、新たに必要な部分だけを追加で書き足すという発想に基づいています。

このようなプログラミング手法は、差分プログラミングと呼ばれ、かつては、継承のもっとも有用な使い道と考えられることもあったようですが、現在では、使い方に注意が必要であることが分かっています。

適切な公開継承の使い方は、基底クラスと派生クラスとの間に is-a関係が構築できる場合だという、一種のガイドラインのようなものがあります。つまり、「Derived is a Base(Derived は Base である)」という文が成立するかどうかがポイントになります。

たとえば、前に挙げた例は、「TwoColorPen is a Pen(2色ペンはペンである)」の関係になるので、適切です。is-a関係は、つねに適切だと言い切れるわけではありませんが、非常に有名で有用なガイドラインとなっています。

is-a関係を考える上で有名な問題として、「ペンギンは飛べなくても鳥なのか?」「正方形は長方形の一種とみなして良いのか?」といったものがあります。前者は、鳥クラスが Flyメンバ関数を持つことが不適切な可能性を示唆しており、後者は、長方形クラスに SetWidth や SetHeight のような、幅や高さの一方だけを変更するような機能を付けられないことを示唆しています。このような問題は、結局のところ、どこかで妥協点を持つしかありませんが、なかなか悩ましい設計になります。

拡張のための継承は、単に、コードの記述を楽にすることだけが目的の差分プログラミングのために使うと、is-a関係とはまったく無関係な構造を作りがちです。

たとえば、「生徒は直線を描くことがある」と考え、すでに「Penクラスが直線を描く機能を持っている」ことに気づき、Penクラスから Studentクラスを派生させたとします。これは、「Student is a Pen(生徒はペンである)」が成り立たないことから、誤った関係性であると言えます。

この例の場合、Studentクラスのメンバ変数として、Penクラスのオブジェクトを持たせる方が設計として適切です。これは has-a関係と呼ばれ(Student has a Pen(生徒はペンを持っている)」と考えられます。第28章であらためて取り上げます。


継承とデストラクタ

派生クラスをインスタンス化すると、まず基底クラスのコンストラクタが呼び出され、その後で派生クラスのコンストラクタが呼び出されます。であれば、デストラクタはその逆順で呼び出されることが期待されます。実際、オブジェクトが実体であれば、先に派生クラスのデストラクタが呼び出され、その後で基底クラスのデストラクタが呼び出されます

しかし、次のような使い方をすると、派生クラスのデストラクタが呼び出されません。

class Base {
public:
    ~Base() {}
};

class Derived : public Base {
public:
    ~Derived() {}
};


int main()
{
    Base* p = new Derived();

    delete p;  // ~Derived() が呼び出されない
}

この場合、インスタンス化されたのは、派生クラスである Derivedクラスです。これを、基底クラスである Baseクラスのポインタ型の変数で受け取っています。

その後、基底クラスのポインタ変数である p に対して、delete演算子を適用していますが、この場合、delete の対象は基底クラスのオブジェクトになるため、基底クラス側のデストラクタだけしか呼び出されないのです

このような事情があるため、この章で解説しているような継承の使い方をするときは、派生クラスのインスタンスを new演算子で生成する使い方は避ける必要があります。詳しい解説は第27章で行いますが、実はこのような挙動は、デストラクタの定義の仕方を変更することで修正できます。

なお、ここで挙げた問題は、標準ライブラリに含まれているクラスを継承する場合にも発生することに注意してください。特によくありがちなのは、vector(【標準ライブラリ】第5章)のような、STLコンテナに便利な追加機能を付けようと思って継承を用いることです。

隠蔽

基底クラスにあるメンバ関数と同じ名前のメンバ関数を、派生クラス側で定義すると、基底クラス側のメンバ関数は隠されてしまいます。これを、隠蔽と呼びます。

class Base {
public:
    void f(int n) {}
};

class Derived : public Base {
public:
    void f(const char* s) {}
};

このサンプルでは、Base::fメンバ関数が隠蔽されます。

基底クラスのメンバが派生クラスに引き継がれることを考えると、次のようになるので、オーバーロードとみなされるような感じもしますが、そうはなりません。

class Derived : public Base {
public:
    // void f(int n) {}  // Base::fメンバ関数はここにある?
    void f(const char* s) {}
};

Base::fメンバ関数が隠蔽されると、次のように、fメンバ関数を呼び出すコードの意味に影響を与えます。

int main()
{
    Base b;
    Derived d;

    b.f(10);     // OK。Base::fメンバ関数を呼び出す
    d.f("xyz");  // OK。Derived::fメンバ関数を呼び出す
    d.f(10);     // エラー。Base::fメンバ関数は隠蔽されている
}

また、Derivedクラスの他のメンバ関数から呼び出す場合も、普通に書くと、自分自身が持っている Derived::fメンバ関数の方を呼び出そうとします。ただ、こちらの場合は回避策もあり、「Base::f();」のように、スコープ解決演算子を使って明示すれば、隠蔽されたメンバ関数を呼び出せます

class Derived : public Base {
public:
    void f(const char* s) {}

    void g()
    {
        f("xyz");     // OK。Derived::fメンバ関数を呼び出す
        f(10);        // エラー。Base::fメンバ関数は隠蔽されている
        Base::f(10);  // OK。Base::fメンバ関数を呼び出す
    }
};

メンバ関数が隠蔽されてしまうと、基底クラスと派生クラスとで、関数の挙動を大きく変更されてしまう可能性があります。こうなると、is-a関係を成立させることができないため、一般的に隠蔽は避けるべきです

公開継承を使うことが適切であるのにも関わらず、どうしても隠蔽が起きてしまうという場合には、using宣言を使用できます。using宣言を使うと、基底クラス側で定義されている名前を、using宣言を書いた位置のスコープに取り込めます。これは、名前空間に関する章(第3章)で取り上げた using宣言とまったく同じだと分かるでしょう。

class Derived : public Base {
public:
    using Base::f;  // Base::f をこのスコープに取り込む

    void f(const char* s) {}
};

こうすると、Base::fメンバ関数と、Derived::fメンバ関数とが、同じスコープに存在することになるので、fメンバ関数がオーバーロードされているのと同等になります。そのため以下のように、すべての呼び出しが有効になります。

class Derived : public Base {
public:
    using Base::f;  // Base::f をこのスコープに取り込む

    void f(const char* s) {}

    void g()
    {
        f("xyz");     // OK。Derived::fメンバ関数を呼び出す
        f(10);        // OK。Base::fメンバ関数を呼び出す
        Base::f(10);  // OK。Base::fメンバ関数を呼び出す
    }
};

int main()
{
    Base b;
    Derived d;

    b.f(10);     // OK。Base::fメンバ関数を呼び出す
    d.f("xyz");  // OK。Derived::fメンバ関数を呼び出す
    d.f(10);     // OK。Base::fメンバ関数を呼び出す
}

ただし、間違ってはいけないのは、隠蔽が起きてしまうときに、いつも using宣言を使えばいいというわけではないということです。そもそも、公開継承を使うこと自体が間違っていないかどうか考えることが先決です。その際のガイドラインは、is-a関係が構築できているかどうかです。is-a関係が成立しないようならば、別の手段を考える必要がありますが、この辺りの詳細は、第28章で取り上げます。

スライシング

基底クラスの方が上位にあり、派生クラスが下位にあるようにイメージすると、派生クラスの方が小さいように勘違いしそうになりますが、実際には逆であることに注意してください。つまり、派生クラスは、基底クラス+αなのですから、追加のメンバ変数を持っている可能性があり、sizeof(Base) <= sizeof(Derived) になります。ここに問題が潜んでおり、次のようなコピー操作を行うと、派生クラスに固有な情報が失われてしまいます

Derived d;
Base b = d;  // Derived に固有な情報を失う

このような現象を、スライシングと呼びます。スライシングは、次のような関数呼び出しのときにも起こります。

void f(Base b)
{
}

Derived d;
f(d);  // スライシング

スライシングは大抵の場合、バグですので、避けるべきです。結局のところ、基底クラス型のオブジェクトへ実体をコピーしていることが問題なので、次のように、参照(あるいはポインタ)を使えば解決できます。

void f(Base& b)
{
}

Derived d;
f(d);  // OK

あるいは、クラス側でコピー操作を禁止にしておくのも有効な手段です(第17章)。オブジェクトをコピーすることに意味が見出せないクラスの場合は、これは設計として適切でもあります。

継承とテンプレート

継承とテンプレートは組み合わせることができます。

たとえば、クラステンプレートから派生クラスを定義することが可能です。これは、STLコンテナを継承しようとすると起こります。以下の例では、list(【標準ライブラリ】第6章)を継承しています。

#include <list>

class IntList : public std::list<int> {
};

この場合、継承元になるクラステンプレートのテンプレート実引数も確定させていますが、クラステンプレートのままで派生させることもできます。

#include <list>

template <typename T>
class MyList : public std::list<T> {
};

逆に、クラスから派生させて、クラステンプレートを定義することも可能です。

class Base {};

template <typename T>
class Derived : public Base {};

C++11 (final指定子)

C++11

C++11 には、final指定子が追加されており、これを使うと、クラスの継承を禁止できます。

final指定子にはもう1つ、オーバーライドを禁止するという使い方もあります。これについては、第27章で取り上げます。

class Base final {
};

class Derived : public Base {  // エラー
};

C++11 (継承コンストラクタ)

C++11

C++11 では、派生クラス側で、基底クラスのコンストラクタを使えます。これは、継承コンストラクタと呼ばれる機能で、次のように使用します。

class Base {
public:
    Base(int n) {}
};

class Derived : public Base {
public:
    using Base::Base;
};

int main()
{
    Derived d(10);  // OK。Baseクラスのコンストラクタを使用
}

このように、派生クラス側で、基底クラスのコンストラクタ(基底クラス名::基底クラス名)を using宣言することで表現します。


練習問題

問題① 標準ライブラリの bitset(【標準ライブラリ】第13章)には、すべてのビットが 1 になっているかどうかを判定するメンバ関数がありません(C++11 で allメンバ関数が追加されました)。このメンバ関数を、bitset を継承して追加してください。


解答ページはこちら

参考リンク


更新履歴

’2018/7/21 final を指定子と表記するように修正。

’2018/7/13 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

’2018/4/5 VisualStudio 2013 の対応終了。

’2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

’2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

≪さらに古い更新履歴を展開する≫



前の章へ (第25章 フレンド)

次の章へ (第27章 仮想関数)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る