第2章 第3節 受け継がれた能力〜継承


← 前に戻る | 次に進む →

さて、この節では継承について勉強します。継承とは受け継ぐことを意味します。では、C++では何を受け継ぐのでしょうか。それは、クラスを受け継ぐのです。下の例を見てください。

List 2.7 図形クラスと円形クラス

// 基底クラス: 図形クラス.
class Shape {
protected:
    int x, y;  // 図の描かれる位置.

public:
    virtual void Draw(void);   // 図を描く.
    virtual void Rotate(int);  // 図を回転させる.
};

// 派生クラス: 円形クラス.
class Circle : public Shape {
protected:
    int radius;  // 円の半径.

public:
    void Draw(void);
    void Rotate(int) { }  // 円は回転しても変化しない.
};

この例では、図形クラス(Shape)が円形クラス(Circle)に継承されています。図形クラスは、図の位置を示すxとy、そして図を描画するDraw関数とそれを回転させるRotate関数を持っています。

さて、この中で今まで出てこなかったものがいくつかあります。一つはアクセス制御子の一つ、protected:です。これは、クラス外からメンバへのアクセスを禁じるprivate:と自由にアクセスできるpublic:の中間にあるキーワードで、自分自身のクラスと継承されたクラスからアクセスでき、それ以外からはできません。因みに、privateは継承されたクラスからもアクセスできないのです。

次に、virtualというキーワードがあるのに気が付いたでしょう。これは、継承後に再定義される関数であることを表しています。つまり、継承されたクラスで再び同じ名前の関数を定義することを意味します。ここでは、図形クラスのDraw関数とRotate関数が継承先の円形クラスでも使われます。

ところで、クラスを継承させるには、class 派生クラス : public 基底クラスのように記述します。List 2.7の例では、class Circle : public Shapeとして、図形クラスから円形クラスに継承しています[注1]。

継承時のアクセス制御

継承を行うときはすべて、class 派生クラス : public 基底クラスとしましたが、もちろんpublic以外のprivateやprotectedも指定できます。しかし、ほとんどの場合でそれは無意味なものとなってしまうかもしれません。

では、これらにはどのような意味があるのでしょうか。継承のアクセス制御をpublicにした場合は、基底クラスのprivate、protected、publicはそのまま派生クラスに伝わります。しかし、アクセス制御がprotectedの場合、privateとprotectedは基底クラスと派生クラスで同じですが、基底クラスのpublicは派生クラスでprotectedになってしまいます。アクセス制御をprivateにした場合は基底クラスのメンバすべてが派生クラスでprivateになってしまうのです。

そして、円形クラスにも図形クラスが持つDraw関数とRotate関数を持たせ、それぞれ定義を行います。ところで、このRotate関数はいくら回転しても変化のない円には必要ありません。そこで、void Rotate(int) { }として、何もしない関数になっています。

円形クラスは、円を描くための半径が必要なので、int radius;として、半径を持たせます。因みに、ここで出てくる数値はすべて整数で扱っています。

ここまでで、継承のやり方は大体おわかり頂けたと思いますので、次は、前節に出てきたノートクラスを基底クラスとして、日記クラスを作ることにしましょう。

その前に、前節のノートクラスに若干の修正を加えます。日記クラスで再定義できるようにvirtualキーワードを関数に付けています。

List 2.8 ノートクラス virtual版

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

// ノートクラス.
class Notebook {
protected:
    string* note;  // ノートの本文.
    int pages;     // ノートの総ページ数.

public:
    // ノートクラスのコンストラクタ.
    Notebook() { pages = 0;  note = NULL; }
    Notebook(int pp) { note = new string[pages=pp]; }
    // ノートクラスのデストラクタ   用意したノートを削除.
    virtual ‾Notebook() { delete [] note; }

    // ノートを読む 指定がなければ最初のページ(1ページ目)を読む.
    virtual string Read(int p = 1) const;
    // ノートに書く 指定がなければ最初のページ(1ページ目)に書く.
    virtual void Write(string, int p = 1);
};

では次に、ノートクラスを継承した日記クラスを示します。

List 2.9 日記クラス

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

// 日記クラス.
class Diary : public Notebook {
public:
    // 日記クラスのコンストラクタ 1年は365日とする.
    Diary() { note = new string[pages=365;]; }
    virtual ‾Diary() { }

    // 指定した日付を元日からの日数に変換.
    int DateToPage(int, int) const;
    // 指定したページの日記を読む.
    string Read(int) const;
    // 指定した日付の日記を読む.
    string Read(int, int) const;
    // 指定したページに日記を書く.
    void Write(string, int);
    // 指定した日付の日記を書く.
    void Write(int, int, string);
};

日記クラスでは、いくつかの関数が加えられ、コンストラクタにも修正が加わっています。コンストラクタの定義で、ページ数を一年の日数と同じ365ページにしています。この際、閏年は考慮していません。また、ページだけではなく、日付でも日記の読み書きができるようにそのための関数を加えています。日付をページに変換できるようにDateToPageという関数も追加しました。

ところで、コンストラクタでnote = new string[pages];としているのに、どこにもdeleteが見当たらないのに気が付いたでしょうか。このままだとメモリリークが起こるのではないかと思う方もいるでしょう。しかし、その心配は要りません。というのも、ノートクラスのデストラクタがnoteをdeleteしているからです。派生クラスのオブジェクトが作られるときには、まず基底クラスのコンストラクタが呼ばれ、次に派生クラスのコンストラクタが呼ばれます。そして、オブジェクトが破棄されるときには、まず派生クラスのデストラクタが呼ばれて、次に基底クラスのデストラクタが呼ばれるのです。ですから、日記クラスのデストラクタにdelete [] note;を入れると、noteに対してdeleteが二度呼ばれるのでエラーとなってしまいます。

次に、日記クラスの関数を以下に示します。

List 2.10 日記クラスのメンバ関数の定義

// 月の日数.
static const int days_of_month[12] =
    { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

// 指定した日付を元旦からの日数に変換.
int Diary::DateToPage(int m, int d) const
{
    // 正しい日付か判定.
    if (m < 1 || m > 12 || d < 1 || d > days_of_month[m-1]) {
        cerr << "Error of date: " << m << "/" << d << endl;
        return -1;
    }

    // 日数を計算.
    int days = 0;
    for (int i = 0; i < m - 1; i++) days += days_of_month[i];

    return days + d;
}

// 指定したページの日記を読む.
string Diary::Read(int p) const
{
    return Notebook::Read(p);
}

// 指定した日付の日記を読む.
string Diary::Read(int m, int d) const
{
    return Read(DateToPage(m, d));
}

// 指定したページに日記を書く.
void Diary::Write(string str, int p)
{
    Notebook::Write(str, p);
}

// 指定した日付の日記を書く.
void Diary::Write(int m, int d, string str)
{
    Write(str, DateToPage(m, d));
}

指定の日付の日記を読み込む関数と書き込む関数を追加しましたが、どちらも日付からページを求めて、それを指定したページの日記を読み込む関数、書き込む関数に渡しているだけです。そして、指定したページからの読み書きの関数では、ノートクラスの関数をそのまま使っています。その際、ReadとWriteの関数にNotebook::を付けることで、ノートクラスの関数であることを表しています。

指定した日付からページを求める関数では、月の日数のテーブルを用いて、ページ数を割り出しています。これは、そんなに難しくはないでしょう。

以下に、日記クラスを用いた例を示します。日記クラス全体はタイトルにリンクされています。

List 2.11 日記クラスの例 (全ソースはリンク先を参照)

int main()
{
    // 日記クラスのオブジェクト.
    Diary diary;

    // 日記を書く.
    diary.Write(5, 5,  "Today is the holiday.");    // 5月5日.
    diary.Write(7, 10, "Today is my birthday :D");  // 7月10日.
    diary.Write("This page is 3.", 3);  // 3ページ目つまり1月3日.

    // 日記を読む.
    cout << diary.Read(1, 3)  << endl;  // 1月3日.
    cout << diary.Read(125)   << endl;  // 125ページ目は5月5日.
    cout << diary.Read(7, 10) << endl;  // 7月10日.
    cout << diary.Read(8, 31) << endl;  // 8月31日...この日はまだ書いてない.
    cout << diary.Read(2, 31) << endl;  // 2月31日...?! 2月31日は存在しないよ!

    return 0;
}


This page is 3.
Today is the holiday.
Today is my birthday :D
This page is empty.
Error of date: 2/31
Cannot read: out of pages.

これで、継承についての基本的なことはおわかり頂けたと思います。継承はオブジェクト指向には不可欠なものなので、しっかり身に付けるようにしてください。


← 前に戻る | 次に進む →


Copyright (C) Noriyuki Futatsugi/Foota Software, Japan.
All rights reserved.
d2VibWFzdGVyQGZ1dGF0c3VnaS5uZXQ=