書籍転載
文法からはじめるプログラミング言語Microsoft Visual C++入門

C++でオブジェクト指向プログラミング
―― 第10章 クラス〜オブジェクト指向プログラミング(後編) ――

WINGSプロジェクト 矢吹 太朗(監修 山田 祥寛)
2010/06/16
Page1 Page2

10.2.3 仮想関数

 図10-7のようにStudentのメソッドshow()をPersonのものとは別のものにすることができます。このように、基底クラスのメソッドを派生クラスで再実装することをオーバーライドと呼びます。

図10-7 基底クラスのメソッドを派生クラスでオーバーライドする

 オーバーライドを実現するためには、次の2つの作業が必要です。

  • 基底クラスのメソッドを仮想関数にする
  • 派生クラスでメソッドを再定義する

 オーバーライドは、派生クラスの定義で勝手にできるものではありません。オーバーライドされるメソッドは、基底クラスの定義において仮想関数にしておかなければなりません。メソッドを仮想関数にするのは簡単で、宣言の前にvirtualと書くだけです。クラスPersonの定義を次のように変更します。

class Person
{
protected:
  string name;
  int age;
public:
  Person(string name, int age) : name(name), age(age) {}
  void eat() { cout<<"eat()\n"; }
  virtual void show() { cout<<name<<" ("<<age<<")\n"; } //仮想関数
  virtual ~Person() {} //仮想デストラクタ
};
[サンプル]10-virtual1.cpp

 次のようにStudentでメソッドshow()を再定義します。

class Student : public Person
{
  int id;
public:
  Student(string name, int age, int id) : Person(name, age), id(id) {}
  void study() { cout<<name<<" (id:"<<id<<"): study()\n"; }
  //メソッドshow()の定義
  void show() { cout<<id<<": "<<name<<" ("<<age<<")\n"; }
};
[サンプル]10-virtual1.cpp

 オーバーライドの結果を確認しましょう。

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

(Personの定義)

(Studentの定義)

int main()
{
  Person p("Taro", 32);
  p.show();//出力値: Taro (32)

  Student s("Hanako", 22, 1);
  s.show();//出力値: 1: Hanako (22)

  //Person*の配列に2つのオブジェクトのアドレスを格納する
  Person* people[2]={&p,&s};
  people[0]->show();//出力値: Taro (32)
  people[1]->show();//出力値: 1: Hanako (22)
}
[サンプル]10-virtual1.cpp

 PeronオブジェクトとStudentオブジェクトで、show()の結果が違っていることが確認できます。重要なのは、Person*の配列で、PeronとStudentの両方のオブジェクトを管理していることです。型がPerson*であるにもかかわらず、->show()の結果は適切なものになっています(Studentだけでeat()を再実装してもこうはなりません)。これが継承による多態の実現です。

 この例では必要ありませんが、10.1.7項のような独自のデストラクタを利用する場合には、デストラクタを仮想関数にすることを忘れないでください。デストラクタを仮想関数にしないでおくと、派生クラスを多態的に利用したときに、派生クラスのデストラクタではなく基底クラスのデストラクタだけが呼び出されることになります。

10.2.4 純粋仮想関数

 1.1.4項の図1-12のような関係を実装することを考えます(編集注:本サイト上では該当部分は掲載されていません)。円(Circle)と長方形(Rectangle)を図形(Shape)から継承させることによって、両者を統一的に扱うことができるようにします。PersonとStudentの場合と違うのは、基底クラスであるShapeの描画メソッドdraw()を定義することはできないことです。図形は円や長方形などを一般化したもので、描き方を明示することはできないからです。このようなときには、純粋仮想関数を使います。純粋仮想関数は次のようにキーワードvirtualを使って宣言します。

virtual 戻り値の型 メソッド名(引数リスト)=0;
[構文]純粋仮想関数

 純粋仮想関数を利用してShapeとCircle、Rectangleを実装すると次のようになります。

#include <iostream>
using namespace std;

class Shape
{
public:
  virtual void draw()=0; //純粋仮想関数
};

class Circle : public Shape
{
public:
  void draw() { cout<<"○"<<endl; }
};

class Rectangle : public Shape
{
public:
  void draw() { cout<<"□"<<endl; }
};

int main()
{
  //Shape s; //エラー(抽象クラスのオブジェクトを作ることはできない)
  Circle c;
  c.draw(); //出力値:○
  Rectangle r;
  r.draw(); //出力値:□

  Shape* shapes[]={&c,&r};
  shapes[0]->draw(); //出力値:○
  shapes[1]->draw(); //出力値:□
}
[サンプル]10-virtual2.cpp

 純粋仮想関数を持つようなクラス(この例ではShape)のオブジェクトを作ることはできません。このようなクラスは抽象クラスと呼ばれ、派生クラスのメンバの仕様や有効範囲等のインターフェイスを規定するために使います。抽象クラスの派生クラス(この例ではCircleとRectangle)では、純粋仮想関数(この例ではdraw())を実装しなければなりません。

 非仮想関数と仮想関数、純粋仮想関数の役割をまとめると次のようになります。

  • 非仮想関数:派生クラスはインターフェイスと実装の両方を継承する。実装を変更してはいけない
  • 仮想関数:派生クラスはインターフェイスと実装を継承するが、実装は変更してもよい
  • 純粋仮想関数:派生クラスはインターフェイスだけを継承する

10.2.5 テンプレートによる暗黙的なインターフェイス指定

 継承を使わなくても、CircleクラスとRectangleクラスにメソッドdraw()を持つことを強制することができます。次のコードでは、引数として与えられたオブジェクトのメソッドdraw()を呼び出すテンプレート関数makeDraw()を定義し、CircleオブジェクトとRectangleオブジェクトをその関数に与えています。

#include <iostream>
using namespace std;

struct Circle
{
  void draw() { cout<<"○"<<endl; }
};

struct Rectangle
{
  void draw() { cout<<"□"<<endl; }
};

template <typename T>
void makeDraw(T t)
{
  t.draw();
}

int main()
{
  Circle c;
  makeDraw(c); //出力値:○
  Rectangle r;
  makeDraw(r); //出力値:□
}
[サンプル]10-generic-programming.cpp

 このコードをビルドするためには、CircleとRectangleはメソッドdraw()を持っていなければなりません。つまり、メソッドdraw()を派生クラスに実装させるという純粋仮想関数の役割の1つを、テンプレート関数makeDraw()でも果たせているのです。

 純粋仮想関数を使って、明示的にインターフェイスを指定しなくても、テンプレートを使って暗黙的にインターフェイスを指定できるのです。実は、このような考え方は、本書では既に登場しています。9.1.2項で反復子を紹介した際に、反復子とポインタの両方を引数に取れる関数を作成しました(編集注:本サイト上では該当部分は掲載されていません)。反復子とポインタの間には、何の継承関係もないにもかかわらずそのようなことが可能なのは、テンプレート関数があるからです。9.3.5項で紹介した関数ポインタを引数にする関数が(編集注:本サイト上では該当部分は掲載されていません)、10.1.13項のコラムで紹介した関数オブジェクトを引数に取れるのも同じ理由です。

 このように、型に依存しない形でプログラムを書くことを、ジェネリックプログラミングと言います。オブジェクト指向プログラミングがプログラミングのパラダイムの1つであるように、ジェネリックプログラミングもプログラミングのパラダイムの1つです。C++は、オブジェクト指向プログラミングを強力にサポートしているのと同じように、ジェネリックプログラミングも強力にサポートしています。実際、STLのような標準ライブラリは、オブジェクト指向プログラミングとジェネリックプログラミングのテクニックを、徹底的に利用しています。このことからも、C++はオブジェクト指向プログラミング言語ではなくマルチパラダイム言語と呼ぶべきだということがわかります。

 書籍『書籍転載:文法からはじめるプログラミング言語Microsoft Visual C++入門』の部分転載は今回で最終回です。「第10章 クラス〜オブジェクト指向プログラミング」は、この後、「■10.3 C++/CLIにおけるオブジェクト指向プログラミング」という節が続きますが、今回の転載ではカットしました。そのほかの章の内容は、「目次情報ページ」を参照してください。End of Article

 

 INDEX
  [書籍転載]文法からはじめるプログラミング言語Microsoft Visual C++入門
  C++でオブジェクト指向プログラミング
    1.継承/アクセス制御
  2.仮想関数/純粋仮想関数/テンプレートによる暗黙的なインターフェイス指定

インデックス・ページヘ 「文法からはじめるプログラミング言語Microsoft Visual C++入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間