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

この章の概要

この章の概要です。

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

継承

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

クラスは、他のクラスのメンバを引き継いで作り出すことができ、 このようにしてクラスを定義することを派生と呼びます。 また、メンバを引き継ぐという部分を指して、継承と呼びます。
また、派生元になるクラスを基底クラス(親クラス、スーパークラス)と呼び、 派生によって作られたクラスを派生クラス(子クラス、サブクラス、導出クラス)と呼びます。

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

派生クラス

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

class Base {
};

class Derived : public Base {
};

この場合、Baseクラスが基底クラス、Derivedクラスが派生クラスです。 派生クラスの名前の直後に「: アクセス指定子 基底クラス名」というように続けることで、継承を表現できます

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 も引き継いでいることに注目して下さい。 このように、継承で引き継げるメンバは、文字通り「すべてのメンバ」です。 他にも、デストラクタ、演算子オーバーロード、列挙型、入れ子クラス、静的メンバといったものも含みます。

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章で行いますが、実はこのような挙動は、 デストラクタの定義の仕方を変更することで修正できます。 C++プログラミングのガイドラインの中には、この修正を常に施すべきだというものもあります。 確かに安全性を考えると、その案に同意できると思いますが、一応 C++ としては必須ではありません。

ここで挙げた問題は、標準ライブラリに含まれているクラスを継承する場合にも発生することに注意して下さい。 特によくありがちなのは、vector(【標準ライブラリ】第5章)のような、 STLコンテナに便利な追加機能を付けるために、継承を用いることです。 してはいけないということはありませんが、派生クラスのデストラクタが呼び出されない使い方をしないように注意が必要です。

とはいえ、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

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

継承とテンプレート

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

例えば、クラステンプレートから派生クラスを定義することが可能です。 これは、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 {  // エラー
};

finalキーワードは、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます

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宣言することで表現します。

この機能は、VisualC++ 2015/2017、clang 3.7 で使用できます。VisualC++ 2013 では対応していません


練習問題

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


解答ページはこちら

参考リンク

更新履歴

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2016/3/20 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ