第2章 第4節 クラスのコピー


← 前に戻る | 次に進む →

この節では、クラスのオブジェクトをコピーする方法について解説します。しかし、コピーする方法だけを言えば、A = Bなどとするだけです。では、何が問題になるかといえば、そのコピーをするための準備が少々厄介なのです。

まずは、コピーについて詳しく説明する前に、初期化と代入の違いについて理解しなければなりません。下の例をご覧ください。

List 2.12 初期化と代入の例

class Foo {
private:
    int i;

public:
    Foo() { i = 0; }
};

void f(Foo e)
{
}

int main()
{
    Foo a;
    Foo b = a;  // bはaによって初期化.
    Foo c(a);   // cはaによって初期化.
    Foo d;
    d = a;      // dはaを代入.
    f(a);       // 関数fの引数eはaによって初期化.

    return 0;
}

リスト中のコメントでも示しているように、Foo b = aFoo c(a)は初期化になります。そして、この二つはまったく同じことをしています。初期化は、既に存在するオブジェクトを初期値として、新たにオブジェクトを作成することです。よって、関数にオブジェクトを値として渡す場合も、渡された側の関数の引数は初期化されたことになります。一方、d = aは代入になります。代入は、既にあるオブジェクト、つまり初期設定されているオブジェクトに対して、別のオブジェクトと同じ状態にするために行います。このように、初期化と代入はまったく違うものだと認識してください。

さて、まずは初期化のための準備についてお話します。初期化を行うためにはコピーコンストラクタという特別なコンストラクタが必要になります。しかし、List 2.12では何も特別なことはしていません。実は、この例では何も準備しなくても、問題なく実行できるのです。というのも、C++が自動的にコピーコンストラクタを用意してくれるからです。用意されたコピーコンストラクタは、メンバ対メンバのコピーを行います。

そこで皆さんは、だったらコピーコンストラクタなどいらないじゃないか、と思うことでしょう。では、前節のList 2.11のmain関数のreturn 0;の前にDiary diary2(diary);を追加して、実行してみてください。どうです? エラーになったでしょう。

つまり、ポインタなどを使ってなければメンバ対メンバのコピーで特に問題が起こることも少ないのですが、メンバにポインタを使用して、newなどで領域を割り当てていたりすると、簡単にトラブルを引き起こしてしまいます。そのために、コピーコンストラクタを用意して、その処理を行うのです。このコピーコンストラクタは初期化のみで代入は行いませんが、その点については、この節の後半で述べることにします。

では早速、前節のList2.8のノートクラスにコピーコンストラクタを追加してみましょう。コピーコンストラクタは、コンストラクタにそのクラスの参照を引数としたものになります。例えばノートクラスならNotebook(const Notebook&);とします。

それでは、コピーコンストラクタを加えたノートクラスを下に示します。

List 2.13 ノートクラス コピーコンストラクタ追加版

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

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

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

// ノートクラスのコピーコンストラクタ.
Notebook::Notebook(const Notebook& nb)
{
    if (nb.pages > 0) {  // ページが存在するとき.
        note = new string[pages=nb.pages];  // 初期化するページ分だけnew
        // ページをコピー.
        for (int i = 0; i < pages; i++) note[i] = nb.note[i];
    } else {  // ページ数が0のとき.
        pages = 0;
        note = NULL;
    }
}

ノートクラスの中で、コピーコンストラクタNotebook(Notebook&);の宣言をしています。それでは、その定義を見て行きましょう。

まず最初に、if (nb.pages > 0)となっていますが、これはコピー元のノートクラスのオブジェクトが、ページ領域を持っているかどうかの確認をしています。もし、ノートにページが存在しなければページ数を0として、noteにNULLを代入してコピーコンストラクタを抜けます。ページが存在するなら、コピー先のオブジェクトのnoteに対し、コピー元のページ数分だけの領域を確保し、その領域にコピー元のnoteの内容をそっくり複製します。これで、別々の領域を確保してコピーされたことになります。

よし、これでクラスのコピーに関しては完璧だ! と思うのは早計です。確かに初期化に関しては、これでよいのですが、代入に対してはまだ何も改善されていないからです。

しかし、どのように代入を行えばよいのでしょう。C++で代入をするために必要なものと言えば、代入演算子=が頭に浮かぶと思いますが、実は、この代入演算子も、コピーコンストラクタと同じく、我々が何もしなくても、C++によって自動的に用意されているのです。しかし、コピーコンストラクタと同じ理由で、そのままでは使用できないことが往々にしてあります。そこで、この演算子のオーバーロードを行って、自前の代入演算子を用意することになります。オーバーロードとは、既に存在する関数を上書きすることですが、ここでは代入演算子のオーバーロードのやり方について説明します。

C++では、当たり前のように使用している代入演算子ですが、メンバ関数の形で書くと、X& X::operator=(const X&)のようになります。そこで、これをノートクラスに組み入れると下のようになります。

List 2.14 ノートクラス コピー追加版

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

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

    // ノートクラスの代入演算子のオーバーロード.
    Notebook& operator=(const Notebook&);

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

// ノートクラスのコピーコンストラクタ.
Notebook::Notebook(const Notebook& nb)
{
    if (nb.pages > 0) {  // ページが存在するとき.
        note = new string[pages=nb.pages];  // 初期化するページ分だけnew
        // ページをコピー.
        for (int i = 0; i < pages; i++) note[i] = nb.note[i];
    } else {  // ページ数が0のとき.
        pages = 0;
        note = NULL;
    }
}

// ノートクラスの代入演算子のオーバーロード.
Notebook& Notebook::operator=(const Notebook& nb)
{
    if (this != &nb && nb.pages > 0) {
        delete [] note;  // 現在のnoteをdelete
        note = new string[pages=nb.pages];  // 代入するページ分だけnew
        // ページを代入.
        for (int i = 0; i < pages; i++) note[i] = nb.note[i];
    } else if (nb.pages == 0) {  // ページ数が0のとき.
        pages = 0;
        note = NULL;
    }

    return *this;
}

代入演算子の中身は大部分がコピーコンストラクタと一致しますが、一部、違うところがあるのがわかるでしょう。まず、代入演算子には戻り値があります。この戻り値には、オブジェクト自身(*this)を返せばOKです。また最初の行の条件式の中で、this != &nbとありますが、これは、あるオブジェクトにそのオブジェクト自身が代入されることを考慮しているのです。もし、代入されたオブジェクトが自分自身であれば、即座に*thisを返すことになります。次の行では、delete [] note;とあります。これは、代入先のオブジェクトの持つ領域を解放して、それから、新たに必要な分の領域を確保するのです。

コピーの禁止

クラスによっては、コピーを禁止したいこともあります。そのようなときにはどうしたらよいのでしょうか?

例えば、この節で出てきたノートクラスをコピー禁止にする場合を考えてみましょう。クラスをコピーするためには、コピーコンストラクタと代入演算子を用いることは既に説明しました。そこで、その二つについて使用できなくすればコピーを禁止することができそうです。しかし、C++では自動的に用意されてしまうので、これらを定義しなければそれで済むということにはなりません。

そこで、アクセス指定子をprivateにして、その中でコピーコンストラクタと代入演算子の宣言を行うのです。定義の必要はありません。こうすることによって、クラスをコピーしようとしたときにエラーを出すことができます。

このコピー禁止のテクニックは、クラスの種類によっては非常に有用であるので、覚えておくと良いでしょう。

どうですか? これで、クラスのコピー、つまり初期化と代入についての準備を完了したことになります。では、実際に、これらを用いたプログラムを走らせてみましょう。

List 2.15 ノートクラス コピー追加版の例 (全ソースはリンク先を参照)

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

int main()
{
    stringstream sstr;     // 文字列ストリーム.
    int i;

    Notebook noteA(5);     // 5ページのノートA

    // ノートAに書き込む.
    for (i = 1; i <= 5; i++) {
        sstr << "Notebook - Page " << i << ends;
        noteA.Write(sstr.str(), i);
        sstr.seekp(0, ios_base::beg);
    }

    Notebook noteB(noteA);  // ノートAをコピーしたノートB
                            // Notebook noteB = noteA; と同じ.

    Notebook noteC(10);     // 10ページのノートC
    noteC = noteA;          // ノートCにノートAを代入.

    for (i = 1; i <= 5; i++)  cout << noteA.Read(i) << endl;
    for (i = 1; i <= 5; i++)  cout << noteB.Read(i) << endl;
    for (i = 1; i <= 10; i++) cout << noteC.Read(i) << endl;

    return 0;
}


Notebook - Page 1
Notebook - Page 2
Notebook - Page 3
Notebook - Page 4
Notebook - Page 5
Notebook - Page 1
Notebook - Page 2
Notebook - Page 3
Notebook - Page 4
Notebook - Page 5
Notebook - Page 1
Notebook - Page 2
Notebook - Page 3
Notebook - Page 4
Notebook - Page 5
Cannot read: out of pages.
Cannot read: out of pages.
Cannot read: out of pages.
Cannot read: out of pages.
Cannot read: out of pages.

この例では、まずNotebook noteA(5);として、5ページのノートを作成し、それぞれのページに対して、ページ数などを書き込んでいます。ところで、この書き込みの際、文字列ストリームstringstreamを使用していますが、これによって、文字列に対して、<<のような入出力ストリームを使用できるようになります。ストリームについては、別の章で詳しい説明をします。

次に、Notebook noteB(noteA);のように、noteAを用いてnoteBを初期化しています。そして、Notebook noteC(10); noteC = noteA;として、noteCにnoteAを代入しています。ここで、noteCは最初に10ページで初期化されていますが、noteAを代入することによって、noteCは5ページに再初期化されます。

このプログラムの出力結果を参考に、コピーの仕組みをきちんと理解しておきましょう。ここで作成した、コピー追加版ノートクラスは、前節のList 2.11にも適用できます。是非、試してみてください。


← 前に戻る | 次に進む →


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