第2章 第2節 最初と最後が肝心


← 前に戻る | 次に進む →

前節では、クラスを使ってノートを作ってみました。しかし、そのノートはたったの1ページ(もっと言えば1行(^-^;;)しか書き込めないものでした。今回はそれを改良して自分の指定したページ数だけノートに書き込めるようにしてみましょう。

それにはまず、どうすればよいのかを考えてみます。複数ページを表現できるノートクラスには、総ページ数を保存しておく機能とそのページ数分の領域が必要です。そして、指定したページを読み書きする関数もいりますね。

早速、これらの機能を加えて拡張したノートクラスを下に示してみました。

List 2.4 ノートクラス 複数ページ版

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

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

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

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

このクラスを見てみると、まずint型の変数pagesが付け加わっていることがわかります。これがノートの総ページ数を表します。次に、以前のクラスではstring note;と宣言されていた本文の領域がstring* noteとポインタ型で表されていることに気付くでしょう。こうすることで、newを使って任意のstringの領域を確保することができます。つまり、使いたいページ数分だけのスペースを用意できるのです。

では、いつ、その領域を確保すればよいのでしょう? ノートクラスのオブジェクトを用意した後、初期化のための関数を用意しなければならないのでしょうか? そうすれば確かに領域を確保できますが、初期化することはわかりきっているのに、いちいちそのための関数を用意するのは効率が悪いですよね。普通ならオブジェクトを作成すると同時に初期化を行いたいと考えるでしょう。

そのために、クラスにはコンストラクタという機能が備わっています。これは、オブジェクトを構築するときに自動的に実行される初期化を行うのための関数です。このコンストラクタはクラス名と同じ名前の関数として表されます。例えば、List 2.4で示されるノートクラスならば、Notebookという関数が用意されるのです。

そして、このノートクラスのコンストラクタは、仮引数ppに総ページ数をもらってそれをpagesに代入し、それと同時に、そのページ数分の領域をnote = new string [pages=pp];として確保しています。これで、指定したページ数のノートを作ることができますね。もし、引数に何も与えられないな時は、ページ数が0のノートを作ります。つまり、このノートには何も書き込めず、また、読み込むこともできません。

ここまでで、あれ? newで確保した領域は、いつ、どうやって解放するの? という疑問を持った方もいるかもしれません。もちろん、きちんと解放する必要があります。そこで、コンストラクタと対を成す、デストラクタという関数もクラスには備わっているのです。簡単に言えば、後片付けをする関数で、オブジェクトを使い終わったときに呼ばれるものです。今回の場合は、プログラムが終了すると同時にオブジェクトが破棄され、そしてデストラクタが呼ばれます。

デストラクタはクラス名の先頭にに‾(チルダ)を付けたもので、ノートクラスでは‾Notebookとなります。そして、その中でdelete [] note;のようにnoteに割り当てられた領域を解放しているのがわかるでしょう。因みに、コンストラクタに引数が渡されないとき、noteにNULLが代入されますが、このときnoteをdeleteしても何もしません。

デフォルトコンストラクタ

実は、C++のクラスには必ずコンストラクタは存在します。だからコンストラクタがないとエラーとして扱われるはずです。えっ、コンストラクタを書かないこともあるじゃない!? と思われるかもしれませんが、実際にはC++が勝手に用意しているのです。そして、デフォルトコンストラクタはnew初期化子によりデータメンバのビット列を0で初期化します。これは、Cとの互換性の意味もあります。

もし、コンストラクタを自分で用意すればもちろんデフォルトコンストラクタは用意されません。だから、引数を持っているコンストラクタを用意した場合、クラスのオブジェクトを作る際に引数なしでオブジェクトを宣言したりするとエラーとなるのです。

そこで、引数を持つコンストラクタを用意した場合で、引数なしで宣言したいときは自分でデフォルトコンストラクタを用意しなければなりません。この章のList 2.4でもデフォルトコンストラクタを用意していますよね。

class Foo
{
    Foo();
    Foo(int x);
};

まあ、上記のように単にFoo();のように宣言するだけでOKなので、それほどの手間にはならないでしょう。また、Foo(int x = 0)のようなデフォルトの引数を用意することで解決することもできます。

次に、ノートの読み書きのための関数ですが、これにはページ数を指定するための仮引数pを追加するだけです。ここでは、特に指定がない場合は最初のページである1ページ目が読み書きされるように、仮引数をint p = 1のようにしました。

それでは、Read関数とWrite関数の中身を見ていきましょう。

List 2.5 ノートクラスのメンバ関数の定義 複数ページ版

// ノートを読む.
string Notebook::Read(int p) const
{
    p--;  // ノートは1ページ目が最初のページ.
    if (p >= 0 && p < pages) {
        if (note[p].empty()) return "This page is empty.";
        return note[p];  // noteの内容を返す.
    } else return "Cannot read: out of pages.";  // ページ外.
}

// ノートに書く.
void Notebook::Write(string str, int p)
{
    p--;  // ノートは1ページ目が最初のページ.
    if (p >= 0 && p < pages) note[p] = str;  // noteに代入する.
    else cerr << "Cannot write: out of pages." << endl;  // ページ外.
}

まずは、Read関数ですが、最初にp--;とpから1を引いています。これは、ノートのページを数えるときに普通は1ページ目から数えますが、newで確保した領域は0から数えるのでその調整をしているのです。だから1ページ目を指定するために1をpに渡せば、note[0]を見ることができるようになります。

次に、if (p >= 0 && p < pages)というif文があります。これによって、pに渡されたページが確保したページ内であるかをチェックしています。そして、そのページ範囲内であれば、note[p]の中身をチェックして、空なら"This page is empty."と表示して、そうでなければ、その内容を返すわけです。

Write関数も基本的にはRead関数に加えた変更とほぼ同じですが、範囲外のページがpに渡されたときの動作が若干異なります。Read関数では、"Cannnot read: out of pages."という文字列を返すのに対し、Write関数では、cerrに"Cannnot write: out of pages."と表示させています。因みにこのcerrは標準エラー出力を表し、エラーメッセージなどをディスプレイなどの標準出力に表示させたいときに使用します。

それでは、実際にこれらのプログラムを実行させて見ましょう。全体のプログラムを以下に示します。

List 2.6 ノートクラス全体 複数ページ版

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

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

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

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

// ノートを読む.
string Notebook::Read(int p) const
{
    p--;  // ノートは1ページ目が最初のページ.
    if (p >= 0 && p < pages) {
        if (note[p].empty()) return "This page is empty.";
        return note[p];  // noteの内容を返す.
    } else return "Cannot read: out of pages.";  // ページ外.
}

// ノートに書く.
void Notebook::Write(string str, int p)
{
    p--;  // ノートは1ページ目が最初のページ.
    if (p >= 0 && p < pages) note[p] = str;  // noteに代入する.
    else cerr << "Cannot write: out of pages." << endl;  // ページ外.
}

int main()
{
    // ノートクラスのオブジェクト 10ページ分を用意.
    Notebook notebook(10);

    // 最初のページ(1ページ目)に書き込む.
    notebook.Write("This is a notebook.");
    // 2ページ目に書き込む.
    notebook.Write("This notebook is good.", 2);

    // 最初のページ(1ページ目)を読む.
    cout << notebook.Read() << endl;
    // 2ページ目を読む.
    cout << notebook.Read(2) << endl;
    // 3ページ目を読む 何も書いてないけどね.
    cout << notebook.Read(3) << endl;

    // 0ページ目に書き込む...えっ! 0ページなんてないよ!
    notebook.Write("This page is bad.", 0);
    // 0ページ目を読む...0ページってどこだい?
    cout << notebook.Read(0) << endl;

    return 0;
}


This is a notebook.
This notebook is good.
This page is empty.
Cannot write: out of pages.
Cannot read: out of pages.

最初のページ(1ページ目)を読み込むと"This is a notebook."と出力され、2ページ目では"This notebook is good."と表示されます。書き込みされていない3ページ目を読み込もうとすると"This page is empty."となります。そして、存在しないはずの0ページ目に対して書き込みや読み込みを行うと、それぞれ、"Cannot write: out of pages."、"Cannot read: out of pages."と出ます。

この章では、複数ページに対応したノートクラスを作り、そこに出てきたコンストラクタとデストラクタを中心に解説を行いました。このコンストラクタ・デストラクタは、クラスを用いる際に非常に重要な役割を果たすものなのでしっかりと覚えておきましょう。


← 前に戻る | 次に進む →


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