M.Hiroi's Home Page

Linux Programming

お気楽C++プログラミング超入門

[ PrevPage | C++ | NextPage ]

継承

2 回にわたってC++の基本的なオブジェクト指向機能について簡単に説明しました。今回はオブジェクト指向機能の目玉ともいえる「継承」について取り上げます。まず最初に、一般的なオブジェクト指向で使われている継承について簡単に説明します。

●継承とは?

「継承 (inheritance : インヘリタンス)」は簡単に言うとクラスに「親子関係」を持たせる機能です。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたインスタンス変数やメソッドになります。プログラムを作る場合、今まで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能になります。

クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++の場合は、元になるクラスを「基底クラス」といい、継承するクラスを「派生クラス」といいます。

たとえば、クラス Foo1 を継承してクラス Foo2 を定義しましょう。クラス Foo1 にはメソッド bar が定義されています。クラス Foo2 にメソッド bar は定義されていませんが、Foo2 のオブジェクトに対して bar を呼び出すと、スーパークラス Foo1 のメソッド bar が実行されるのです。

メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar が定義されているか調べます。ところが、Foo2 には bar が定義されていないので、スーパークラスである Foo1 に bar が定義されているか調べます。ここでメソッド bar が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーになります。

継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これを「オーバーライド (over ride)」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。

●単一継承

継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただ一つのクラスからしか機能を継承することができません。したがって、クラスの階層は下図のような木構造 [*1] で表すことができます。


  図 : 単一継承におけるクラスの階層

継承は何段階に渡って行われてもかまいません。たとえばクラス E の場合、スーパークラスが B で、B のスーパークラスが A に設定されています。サブクラスは複数あってもかまいません。たとえば、A のサブクラスは B, C, D の 3 つがあり、B のサブクラスは E, F の 2 つがあります。上図では、クラス A のスーパークラスはありませんが、ほかのクラスはただ一つのスーパークラスを持っています。オブジェクト指向言語の中では Smalltalk, Java, Ruby が単一継承です。

-- note --------
[*1] 木 (tree) は階層的な関係を表すためのデータ構造です。身近な例ではディレクトリ (フォルダ) の階層構造が木にあたります。

●多重継承

これに対し、多重継承は複数のクラスを継承することができます。このため、クラスの階層は木構造ではなく、下図のようなグラフ [*2] で表すことができます。


  図 : 多重継承におけるクラスの階層

クラス E に注目してください。スーパークラスには B と C の 2 つがあります。多重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつ、スーパークラスも複数持つことができるのです。

C++は多重継承をサポートしています。スクリプト言語では Perl や Python が多重継承です。Common Lisp には CLOS (Common Lisp Object System) というオブジェクト指向機能がありますが、CLOS も多重継承です。

実をいうと、M.Hiroi は多重継承に対してあまりいいイメージを持っていません。私見ですが、多重継承はメリットよりもプログラムを複雑にするデメリットの方が大きいのではないか、と思っています。とくに、上図のクラス A, B, C, E のような菱形の関係をC++でプログラムする場合、とても複雑な問題を引き起こすことが知られています。この問題は多重継承を説明する回で取り上げる予定です。

-- note --------
[*2] グラフは木をより一般化したデータ構造です。数学のグラフ理論では、いくつかの点とそれを結ぶ線でできた図形を「グラフ」といいます。

●継承の仕組み

一般的なオブジェクト指向言語の場合、継承によって引き継がれる性質は定義されたインスタンス変数やメソッドになります。下図を見てください。


          図 : 一般的な継承

クラス Foo にはインスタンス変数 a, b とリーダーメソッド get_a, get_b が定義されています。次にクラス Bar を定義します。Bar は Foo を継承し、Bar 固有のインスタンス変数 c とリーダーメソッド get_c を定義します。Foo と Bar のインスタンスを生成すると、上図に示したように Bar のインスタンスにはクラス Foo で定義された変数 a, b も含まれます。このように、Foo のインスタンス変数が Bar に継承されます。

Foo のインスタンスを生成すると、もちろん変数 a, b は含まれていますが、Bar のインスタンスとメモリを共有することはありません。クラスはオブジェクトの設計図です。設計に共通な部分があったとしても、それから生み出されるインスタンスは別々の実体で、インスタンス変数を共有することはないのです。

クラス Bar にはメソッド get_c しか定義されていませんが、クラス Foo を継承することにより、メソッド get_a と get_b を呼び出すことができます。Bar のインスタンスに対して get_a を呼び出すと、クラス Bar には get_a が定義されていないのでスーパークラス Foo を調べ、そこで定義されている get_a が呼び出されます。もちろん、取り出される値は Bar のインスタンスにある変数 a の値です。このように、Foo のメソッドが Bar に継承されます。

●単一継承の使い方

それでは、具体的にC++の継承を説明しましょう。基底クラスは class 文で指定します。クラス名の後ろにコロン (:) を付けて、その後ろに継承修飾子 (public, protected, private) と基底クラス名を指定します。

継承修飾子ですが、通常の継承では public を指定してください。C++の場合、基底クラスが一つであれば単一継承になり、複数あれば多重継承にになります。継承に必要な設定はこれだけです。

簡単な例として、上図のクラスを実際にプログラムしてみましょう。次のリストを見てください。

リスト : 単一継承 (sample130.cpp)

#include <iostream>
using namespace std;

class Foo {
  int a, b;
public:
  // コンストラクタ
  Foo() : a(0), b(0) {
    cout << "Called Foo()\n";
  }
  Foo(int x, int y) : a(x), b(y) { }
  // メンバ関数
  int get_a() const { return a; }
  int get_b() const { return b; }
  void put_a(int n) { a = n; }
  void put_b(int n) { b = n; }
};

class Bar : public Foo {
  int c;
public:
  // コンストラクタ
  Bar () : c(0) {
    cout << "Called Bar()\n";
  }
  Bar(int x, int y, int z) : Foo(x, y), c(z) { }
  // メンバ関数
  int get_c() const { return c; }
  void put_c(int n) { c = n; }
};

int main()
{
  Foo a;
  Bar b;
  Bar c(1, 2, 3);
  cout << b.get_a() << endl;
  cout << b.get_b() << endl;
  cout << b.get_c() << endl;
  cout << c.get_a() << endl;
  cout << c.get_b() << endl;
  cout << c.get_c() << endl;
  // cout << a.get_c() << endl; コンパイルエラー
}

クラス Foo のメンバ変数 a, b はコンストラクタで初期化します。メソッド get_a と get_b の定義は簡単です。与えられたインスタンスから値を取り出すだけです。次にクラス Bar を定義します。コロンの後ろに public Foo を指定します。これで Foo のメンバ変数とメンバ関数を継承することができます。

Bar のコンストラクタではフィールド変数 a, b, c を初期化します。変数 a, b の初期化は Foo のコンストラクタで行っていますね。このコンストラクタを呼び出すことができれば、わざわざ Bar のコンストラクタで a, b の初期化を行う必要はありません。いいかえれば、基底クラスのコンストラクタと同じプログラムを書かなくてもよいわけです。

実をいうと、C++の派生クラスでは、コンストラクタの初期化リストで基底クラスのメンバ変数を初期化することはできません。また、コンストラクタの中で基底クラスの private なメンバ変数にアクセスすることもできません。ようするに、C++で基底クラスのメンバ変数を初期化するには、基底クラスのコンストラクタを呼び出すしかないのです。

基底クラスのコンストラクタは、派生クラスの初期化リストで呼び出すことができます。コンストラクタの呼び出しが省略された場合は、デフォルトのコンストラクタが呼び出されます。また、コンストラクタによる初期化は基底クラスから派生クラスの順番で行われます。逆に、デストラクタの実行は派生クラスから基底クラスの順番で行われます。

クラス Bar のデフォルトコンストラクタは、初期化リストで基底クラスのコンストラクタを明示的に呼び出していないので、この場合はデフォルトのコンストラクタ Foo() が呼び出され、メンバ変数 a, b は 0 に初期化されます。3 引数のコンストラクタ Bar は基底クラスのコンストラクタ Foo(x, y) を呼び出しているので、メンバ変数 a, b の値は引数 x, y の値に初期化されます。そのあとで、Bar のメンバ変数 c が引数 z の値に初期化されます。

それでは実行してみましょう。

$ clang++ sample130.cpp
$ ./a.out
Called Foo()
Called Foo()
Called Bar()
0
0
0
1
2
3

Foo a はデフォルトコンストラクタ Foo() が呼び出されるので、最初に Called Foo() が表示されます。次の Bar b は、コンストラクタ Bar() から自動的に Foo() が呼び出されるので、Called Foo() が表示されてから Called Bar() が表示されます。

基底クラスのメンバ関数は Bar のインスタンスからでも呼び出すことができます。b.get_a() は 0 に、c.get_a() は 1 になりますし、自クラスのメンバ関数 b.get_c() は 0 に、c.get_c() は 3 になります。なお、Foo のインスタンス a から派生クラスのメンバ関数 get_c は呼び出すことができません。コンパイルエラーになります。

●オーバーライド

継承はクラスに新しい機能を追加するだけではなく、メンバ関数をオーバーライドすることで機能を変更することができます。なお、オーバーライドと多重定義 (オーバーロード) はまったく異なる機能です。混同しないように注意してください。

それでは、簡単な例を示しましょう。メンバ変数の合計値を求めるメンバ関数 sum() を定義します。次のリストを見てください。

リスト : メンバ関数のオーバーライド (sample131.cpp)

#include <iostream>
using namespace std;

class Foo {
  int a, b;
public:
  // コンストラクタ
  Foo() : a(0), b(0) {
    cout << "Called Foo()\n";
  }
  Foo(int x, int y) : a(x), b(y) { }
  // メンバ関数
  int get_a() const { return a; }
  int get_b() const { return b; }
  void put_a(int n) { a = n; }
  void put_b(int n) { b = n; }
  int sum() { return a + b; }
};

class Bar : public Foo {
  int c;
public:
  // コンストラクタ
  Bar() : c(0) {
    cout << "Called Bar()\n";
  }
  Bar(int x, int y, int z) : Foo(x, y), c(z) { }
  // メンバ関数
  int get_c(){ return c; }
  void put_c(int n){ c = n; }
  int sum() { return Foo::sum() + c; }
};

int main()
{
  Foo a(1, 2);
  Bar b(10, 20, 30);
  cout << a.sum() << endl;
  cout << b.sum() << endl;
  cout << b.Foo::sum() << endl; 
}

クラス Foo でメンバ関数 sum() を定義します。そして、クラス Bar でメンバ関数 sum() をオーバーライドします。オーバーライドは基底クラスにあるメンバ関数と同じ名前のメンバ関数を定義するだけです。オーバーライドするときは、メソッド名を間違わないように注意してください。

Bar のメソッド sum は、メンバ変数 a, b, c の値の合計を求めます。このとき、基底クラスのメンバ関数 sum を呼び出すことができると便利です。C++の場合、スコープ解決演算子 :: を使って基底クラスのメンバ関数 [*3] を呼び出すことができます。つまり、Foo::sum() とすれば、基底クラス Foo のメンバ関数を呼び出すことができます。その結果に変数 c の値を足し算します。また、Bar のインスタンス b から基底クラス Foo のメンバ関数 sum を呼び出すこともできます。その場合は b.Foo::sum() とクラス名を指定してください。

実行結果は次のようになります。

$ clang++ sample131.cpp
mhiroi@DESKTOP-FQK6237:~/cpp$ ./a.out
3
60
30

正常に動作していますね。

-- note --------
[*3] 他のオブジェクト指向言語、たとえば Java では super というキーワードを使ってスーパークラスのメソッドを呼び出すことができます。C++には super というキーワードはありません。スコープ解決演算子を使って、呼び出すメンバ関数のクラスを指定する必要があります。

●データ型の継承

一般的なオブジェクト指向言語の場合、クラス名はデータ型を表す識別子として利用することができます。継承はフィールド変数やメソッドに作用するだけではなく、データ型にも作用します。サブクラスに属するインスタンスはデータ型も継承されるため、スーパークラスのデータ型として取り扱うことができます。

たとえば、次のような継承を考えてみましょう。

リスト : データ型の継承

class Foo { }

class Bar : public Foo { }

class Baz : public Bar { }

一般的なオブジェクト指向言語の場合、サブクラスのインスタンスはスーパークラスの変数に代入することができます。具体的には、Foo 型の変数には Bar と Baz のインスタンスを、Bar 型の変数には Baz のインスタンスをセットすることができるのです。ただし、C++にはちょっとした制限があります。これはあとで説明します。

このように、クラスを単一継承してサブクラスを作ると、サブクラスはスーパークラスの部分集合として考えることができます。下図を見てください。


  図 : クラスとサブクラスの関係

サブクラス Baz は Bar や Foo に含まれているので、そのインスタンスに Bar や Foo のメソッドを適用することができるわけです。

●仮想関数とポリモーフィズム

C++の場合、派生クラスのインスタンスを基底クラスの変数に代入すると、そのインスタンスは基底クラスとして扱われます。つまり、派生クラスでオーバーライドしたメンバ関数があっても、基底クラスのメンバ関数が呼び出されます。また、派生クラスで定義したメンバ関数を呼び出すことはできなくなります。

簡単な例を示しましょう。

リスト : 基底クラスへの代入 (sample132.cpp)

#include <iostream>
using namespace std;

class Foo {
public:
  void print() const { cout << "Foo\n"; }
};

class Bar : public Foo {
public:
  void print() const { cout << "Bar\n"; }  
};

class Baz : public Bar {
public:
  void print() const { cout << "Baz\n"; }
};

int main()
{
  Foo a;
  Bar b;
  Baz c;
  a.print();
  b.print();
  c.print();
  a = b;
  a.print();
  b = c;
  c.print();
  Foo* d = new Foo;
  Foo* e = new Bar;
  Foo* f = new Baz;
  d->print();
  e->print();
  f->print();
}
$ clang++ sample132.cpp
$ ./a.out
Foo
Bar
Baz
Foo
Baz
Foo
Foo
Foo

変数 a, b, c に Foo, Bar, Baz のインスタンスをセットします。メンバ関数 print を呼び出すと、各クラスで定義された print が呼び出されます。次に、b のインスタンスを a にセットします。b の値はクラス Bar のインスタンスですが、a に代入すると Foo のインスタンスとして扱われるので、a.print() を呼び出すと Foo のメンバ関数が呼び出されます。同様に、b に c のインスタンスを代入して b.print() を呼び出すと Bar のメンバ関数が呼び出されます。

ポインタ変数の場合も同じです。Foo* 型の変数には new Foo, new Bar, new Baz で生成したインスタンスをセットすることはできますが、いづれの場合もクラス Foo のインスタンスとして扱われます。

このように、基底クラスの変数に派生クラスのインスタンスをセットすることができても、このままではオーバーライドしたメンバ関数を呼び出すことができません。つまり、プログラムの実行時にポリモーフィズムが働いていないのです。C++でポリモーフィズムを機能させるには「仮想関数」を使います。仮想関数はメンバ関数の先頭にキーワード virtual を付けて表します。

リスト : 仮想関数

class Foo {
public:
  virtual void print() const { cout << "Foo\n"; }
};

基底クラス Foo で print の前に virtual を付けます。これで、Foo を継承した派生クラスでも print は仮想関数として扱われます。

C++の場合、もう一つ制限があって、仮想関数が機能するのはポインタ変数 (または参照型変数) に格納されたインスタンスだけです。先ほどのプログラムで、Foo の print に virtual をつけ、プログラムを実行してみましょう。結果は次のようになります。

$ clang++ sample132.cpp
$ ./a.out
Foo
Bar
Baz
Foo
Baz
Foo
Bar
Baz

ポインタ変数 d, e, f に格納されたインスタンスで print を呼び出すと、オーバーライドした print が呼び出されていて、ポリモーフィズムが働いていることがわかります。それ以外の変数は前と同じ結果で、ポリモーフィズムは機能しないことがわかります。

●仮想デストラクタ

派生クラスのインスタンスを基底クラスの変数にセットする場合、デストラクタを仮想関数にしないと、派生クラスのデストラクタが呼び出されません。次のリストを見てください。

リスト : 仮想デストラクタが必要な場合 (sample133.cpp)

#include <iostream>
using namespace std;

class Foo {
  int x;
public:
  Foo() : x(0) { }
  Foo(int n) : x(n) { }
  ~Foo() { cout << "Foo destroy\n"; }  // virtual が必要
  virtual void print() const { cout << "Foo " << x << endl; }
};

class Bar : public Foo {
  int y;
public:
  Bar() : Foo(0), y(0) { }
  Bar(int n, int m) : Foo(n), y(m) { }
  ~Bar() { cout << "Bar destroy\n"; }
  void print() const { cout << "Bar " << y << endl; }
};

int main()
{
  Foo* a = new Foo;
  Bar* b = new Bar;
  Foo* c = new Bar(1, 2);
  a->print();
  b->print();
  c->print();
  delete a;
  delete b;
  delete c;
}

クラス Bar はクラス Foo を継承しています。Bar のインスタンスを廃棄すると、Bar のデストラクタが呼び出されたあと、Foo のデストラクタが呼び出されます。実際に実行すると、次のような結果になりました。

$ clang++ sample133.cpp
$ ./a.out
Foo 0
Bar 0
Bar 2
Foo destroy
Bar destroy
Foo destroy
Foo destroy

Bar* 型の変数 b を delete すると、Bar のデストラクタの次に Foo のデストラクタが呼び出されますが、Foo* 型の変数 c を delete とすると、Bar のインスタンスがセットされているにもかかわらず、Foo のデストラクタが呼び出されます。これはデストラクタが仮想関数ではないため、ポリモーフィズムが機能せず、変数の型 (クラス) のデストラクタだけが呼び出されるのです。

派生クラスのデストラクタを呼び出すには、デストラクタを仮想関数にします。これを「仮想デストラクタ」といいます。具体的には Foo のデストラクタの前に virtual を付けるだけです。これで Foo とそれを継承したクラスのデストラクタは仮想関数になります。

それではプログラムを修正して実行してみましょう。

$ clang++ sample133.cpp
$ ./a.out
Foo 0
Bar 0
Bar 2
Foo destroy
Bar destroy
Foo destroy
Bar destroy
Foo destroy

このように、変数 c を delete すると、派生クラス Bar のデストラクタが呼び出されるようになります。

●純粋仮想関数と抽象クラス

仮想関数を定義するとき、関数本体 { ... } のかわりに = 0; を記述することができます。これを「純粋仮想関数」といいます。そして、純粋仮想関数を持つクラスを「抽象クラス (abstract class)」といい、そのクラスのインスタンスを生成することはできません。

抽象クラスは継承されることを前提としたクラスで、純粋仮想関数は派生クラスにおいて具体的に定義されます。抽象クラスでは、派生クラス共通のメンバ関数を定義します。このとき、純粋仮想関数を呼び出してもかまいません。派生クラスのインスタンスが生成されるとき、そのクラスでは純粋仮想関数が具体化されているはずなので、実際には派生クラスのメンバ関数が呼び出されることになります。

それでは簡単な例題として、図形の面積を求めるプログラムを作ってみましょう。次のリストを見てください。

リスト : 図形のクラス (sample134.cpp)

#include <iostream>
#include <cmath>
using namespace std;

class Figure {
public:
  virtual string kind_of() const = 0;
  virtual double area() const = 0;
  virtual void print() {
    cout << kind_of() << ": area = " << area() << endl;
  }
  virtual ~Figure() { }
};

// 三角形
class Triangle : public Figure {
  double altitude, base;
public:
  Triangle(double a, double b) : altitude(a), base(b) { }
  string kind_of() const { return "Triangle"; }
  double area() const { return altitude * base / 2.0; }
};

// 四角形
class Rectangle : public Figure {
  double width, height;
public:
  Rectangle(double w, double h) : width(w), height(h) { }
  string kind_of() const { return "Rectangle"; }
  double area() const { return width * height; }
};

// 円
class Circle : public Figure {
  double radius;
public:
  Circle(double r) : radius(r) { }
  string kind_of() const { return "Circle"; }
  double area() const { return radius * radius * M_PI; }
};

int main()
{
  Triangle* a = new Triangle(2.0, 2.0);
  Rectangle* b = new Rectangle(2.0, 2.0);
  Circle* c = new Circle(2.0);
  a->print();
  b->print();
  c->print();
}

クラス Figure は抽象クラスです。メンバ関数 kind_of と area が純粋仮想関数で、kind_of は図形の種類を文字列で返し、area は図形の面積を計算して返します。kind_of と area の実体はサブクラスで定義します。

print は図形の種別と面積を表示するメンバ関数です。ここで、純粋仮想関数 kind_of と area を呼び出しています。実際には、print を呼び出したインスタンス (this) が属するクラスのメソッドが呼び出されます。つまり、ポリモーフィズムにより適切なメソッドが呼び出されるわけです。

クラス Triangle, Rectangle, Circle は Figure を継承します。サブクラス固有のメンバ関数 (コンストラクタ) も定義されていますが、どのクラスも純粋仮想関数 kind_of と area を具体化しています。なお、基底クラスの純粋仮想関数をすべて具体化しないと、その派生クラスも抽象クラスになるため、コンパイルでエラーになります。ご注意ください。

それでは実行例を示します。

$ clang++ sample134.cpp
$ ./a.out
Triangle: area = 2
Rectangle: area = 4
Circle: area = 12.5664

正常に動作していますね。

●キャストとポリモーフィズム

型変換のことを「キャスト」といいますが、派生クラスをスーパークラスに変換することを「アップキャスト」といい、基底クラスのインスタンスを派生クラスのデータ型に変換することを「ダウンキャスト」といいます。

C++の場合、派生クラスのインスタンスを基底クラスの変数 (または関数の引数) や配列などに代入するとき、自動的にアップキャストが行われます。コンパイルエラーは発生しません。これに対し、基底クラスのインスタンスを派生クラスの変数などに代入するとき、プログラマが明示的にキャストしないとコンパイルエラーになります。

簡単な例を示しましょう。

リスト : キャスト (sample135.cpp)

#include <iostream>
using namespace std;

class Foo {
public:
  virtual void display() const { cout << "Foo\n"; }
};

class Bar1 : public Foo {
public:
  void display() const { cout << "Bar1\n"; }
};

class Bar2 : public Foo {
public:
  void display() const { cout << "Bar2\n"; }
};

class Baz {
public:
  void display() const { cout << "Baz\n"; }
};

int main()
{
  Foo a;
  Bar1 b;
  Bar2 c;
  Baz  d;

  Foo* a1 = &b;     // アップキャスト
  Foo* a2 = &c;
  a.display();
  a1->display();        // ポリモーフィズム
  a2->display();
  Bar1* b1 = dynamic_cast<Bar1*>(a1);     // ダウンキャスト
  b1->display();
  // Bar1* b2 = a1;                       // キャストしないとコンパイルエラー
  // Foo* a3 = dynamic_cast<Foo*>(&d);    // キャストしてもコンパイルエラー
  // Bar2* b2 = dynamic_cast<Bar2*>(a1);  // 間違ったダウンキャスト
  // b2->display();                       // コアダンプ
}
$ clang++ sample135.cpp
$ ./a.out
Foo
Bar1
Bar2
Bar1

各クラスのインスタンスを生成して変数 a, b, c, d にセットします。Bar1, Bar2 は Foo のサブクラスなので、Foo の変数 a1 に b を、a2 に c を代入することができます。これがアップキャストです。そして、a からメソッド display() を呼び出すと Foo と表示されますが、a1, a2 から display() を呼び出すと、ポリモーフィズムが働いて Bar1, Bar2 の display() が呼び出され、Bar1 と Bar2 が表示されます。

逆に、Bar1 の変数 b1 に a1 を代入するには明示的にキャストする必要があります。これがダウンキャストです。仮想関数を持つクラスをダウンキャストする場合は static_cast ではなく dynamic_cast を使います。使い方は staic_cast と同じです。dynamic_cast<Bar1*>(a1) のように、型変換するデータ型を < > で囲み、その後ろのカッコの中に変換するデータ a1 を書きます。これで変数 b1 に a1 を代入することができます。そして、b1 からメンバ変数 display() を呼び出すと Bar1 と表示されます。

ところで、ダウンキャストをすると問題が発生する場合があります。たとえば、a1 を Bar2 に間違えてダウンキャストすると、コンパイルは成功しますがプログラムを実行するとコアダンプが発生します。それから、Foo と継承関係のない Baz のインスタンスは、キャストしても Foo や Bar の変数に代入することはできません。ダウンキャストはできるだけ使わないように注意してください。

次に、図形のオブジェクトを配列にまとめて格納することを考えてみましょう。Triangle, Rectangle, Circle は型が違うので、同じ配列に格納することはできません。この場合、基底クラス Figure に型変換すると同じ配列に格納することができます。

リスト : キャスト (sample136.ccp)

//
// クラス定義は省略
//

double sum_of_area(Figure* ary[], int n)
{
  double sum = 0;
  for (int i = 0; i < n; i++) {
    sum += ary[i]->area();
  }
  return sum;
}

int main()
{
  Figure* ary[] = {
    new Triangle(2.0, 2.0),
    new Rectangle(2.0, 2.0),
    new Circle(2.0)
  };
  cout << sum_of_area(ary, 3) << endl;
}
$ clang++ sample136.cpp
$ ./a.out
18.5664

関数 main で Figure* 型の配列 ary を用意します。ここで、派生クラスのインスタンスを生成して配列 ary を初期化していますが、アップキャストが行われるので正常にコンパイルできます。

関数 sum_of_figure は図形の面積の合計値を求めます。型変換した場合、派生クラスの情報は失われるため、派生クラス独自のメンバ関数を呼び出すことはできません。型変換した基底クラスのメンバ関数しか利用できませんが、ポリモーフィズムにより派生クラスのメンバ関数が呼び出されるため、図形の面積を正しく計算することができます。


初版 2015 年 9 月 12 日
改訂 2023 年 4 月 9 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | C++ | NextPage ]