C++によるプログラミング入門26
コピーコンストラクタ/代入演算子・後編

 こんにちは。今回は、前回の前編の完結編です。
 さて、前編では、コピーと代入は注意しなければいけない、ということを説明しました。
 コピーと代入は次のようなものでした。

Betu one;
Betu two = one;   //oneをtwoにコピー
Betu three;
three = one;           //oneをthreeに代入

 実は、コピーの時には、コピーコンストラクタという特別なコンストラクタが呼び出されることになっています。また、代入の時には、代入演算子という演算子(「=」のことです)が使われるわけです。そこで、コピーや代入の問題は、これらを自分で定義すれば解決されるのです。
 そのために、コピーコンストラクタと代入演算子のカタチを紹介しなければなりません。それは一般に、次のようなカタチです。(実はバリエーションがないこともないのですが、下のものが一番「普通」だと思いますので、これを使います。)

コピーコンストラクタ:
 クラス名(const クラス名& x);
代入演算子:
 クラス名& operator=(const クラス名& x);

 なんだかちんぷんかんぷんですね。はじめて見たとき、私はそう思いました。
 あとで具体例を見せますが、このような特別な関数を書いておけば、それぞれがコピーと代入のときに呼び出されるということなのです。代入演算子は、使うときは「=」だけですが、自分で定義するときは(自分で定義できるんですよ)、上のように書くわけです。代入演算子も、特殊な形をした「関数」なのです。
 上のように書くとxが仮引数(関数内で引数を表すパラメータ)ですが、コピーコンストラクタの引数、代入演算子の引数は、それぞれ、コピー元、代入元になります。最初に書いた例で言うと、oneがコピー元、代入元です。
 このxの型は、基本的に「そのクラス自身」です。コピー元・代入元は、自分と同じクラスのオブジェクトだからです。ただ、constと&がついていますね。この&は、参照(リファレンス)というものの記号で、アドレス演算子ではありません。これは、簡単に言うと、関数内部では、「(引数のコピーではなく)引数そのもの」を使うという意味です。
 また、constは、「この変数を変更しない」という意味です。「別のクラスのオブジェクトを引数として受け取ったら、そのオブジェクトそのものを関数内部で使うけど、使うだけで、変更するつもりはないよ」というのが、「const クラス名& x」の意味なのです。

 また、代入演算子の戻り値にも&がついていますが、これは、(普通は)「自分自身を戻すため」です。
 う〜ん、ますますちんぷんかんぷんですね。

 しかし、この説明は、ここまでとさせてください。とにかく、コピーコンストラクタと代入演算子というものがあり、それが上のような形で定義されること、また、これらはコピーと代入のときに呼ばれるということだけ、信じてください。
 それを信じれば、あとは、Meiboについて書くだけです。その具体例から考えてみてください。

class Aru{
     int data;
public:
    Aru(int d) : data(d){}
    int get_data() const{ return data; }
};

class Betu
{
    Aru *a;  //Aruのポインタ
public:
    Betu();
    ~Betu();
    //コピーコンストラクタ
    Betu(const Betu&);  //宣言なので仮引数は省略した
    //代入演算子
    Betu& operator=(const Betu&); //宣言なので仮引数は省略した
    void input();
    void show() const;
    int get_data() const{ return a->get_data(); }  //新しく定義
};

//ポインタの0は「どこも指さない」という意味。後述。
Betu::Betu() : a(0){}

Betu::~Betu(){
    delete a;
}

//コピーコンストラクタ
Betu::Betu(const Betu& x){
    a = new Aru(x.get_data());
}

//代入演算子
Betu& Betu::operator=(const Betu& x){
    if(this == &x) return *this;   //自己代入
    delete a;
    a = new Aru(x.get_data());
    return *this;
}

void Betu::input(){
    int d;
    delete a;
    cout << "整数を入力してください:" << endl;
    cin >> d;
    a = new Aru(d);
}

void Betu::show() const{
    if(a == 0) return;  //ポインタがどこも指し示していなければ終了
    cout << "データ:" << a->get_data() << endl;
}

 宣言に仮引数はいらないので省略しました。また、クラス定義の外にある関数の定義では、クラス名を表すBetu::を忘れないようにします。
 また、コピーコンストラクタと代入演算子で使いたいので、Betuにはget_dataという関数を定義しました。これは、aが指すオブジェクトの持つint値を戻すものとしました。
 それでは、まず、コピーコンストラクタの定義を見てください。これは、コピー元である引数からデータの値を取り出し、その値を元に、新しくAruオブジェクトを生成しているのです。ここでxは、コピー元そのものを表すのであって、ポインタではないことを確認しておきます。(「引数の型」についている&はアドレスとはなんの関係もないのです。)
 次に、代入演算子です。代入の場合、自分を自分に代入するということもあります。

Betu one;
one = one;   //oneをoneに代入

そ、そんな、ばかなことはしない。と思うかもしれませんが、長いプログラムなら知らずにやってしまうかもしれません。そのような場合、何もしないのがよいはずです。
 ここで、thisというものを紹介します。これは、オブジェクトの自身のアドレスを表すものです。つまり、oneというオブジェクト内ならoneの、twoというオブジェクト内ならtwoのアドレスを表すポインタなのです。
 代入演算子の定義の中では、自分のアドレスと代入元のアドレス(&x)を比べています。&xの&は、(ややこしいですが)アドレス演算子です。もし、これらが一致していれば、自己代入なので、関数の実行を中断します。ところで、代入演算子は、自分自身を戻すように定義するのが普通です。そこで、*thisを戻しているのです。thisが自分を指し示すポインタなので、*thisは「(オブジェクトにとって)自分自身」ということになりますよね。
 自己代入でない場合は、現在あるAruオブジェクトをdeleteで破棄してから、代入元のデータを取り出し、新しいAruオブジェクトを生成し、aでそれを指し示すようにしています。
 長かったですね。これで、完成です。これで、問題のない「ポインタをデータメンバに持つクラス」が完成したわけです。今は簡単な例で行いましたが、いつもだいたい同じようなコード書けばよいことになります。
 もちろん、「ポインタをデータメンバに持つクラス」でも、ポインタの指し示すオブジェクトを、自分で生成したり破棄しないクラスなら、何もする必要はありません。

 また、前回の最後に書いたように、「コピーや代入をしない」という方法もあります。ただし、「約束」だけでは、知らず知らずに破られるかもしれません。そこで、コンパイラの助けを借りる方法もあります。それは、コピーコンストラクタと代入演算子をprivateにしてしまうのです。privateなものは、クラス外で使えないので、結局、使えないということになるのです。「約束」を忘れてコピーや代入をするコードを書くと、コンパイラが「ダメ出し」してくれるわけです。
 この場合、宣言だけで十分で、定義を書く必要はありません。どうせ使わないからです。

class Betu
{
    Aru *a;  //Aruのポインタ
    //コピー・代入の禁止(private化)
    Betu(const Betu&);
    Betu& operator=(const Betu&);
public:
    Betu();
    ~Betu();
    void input();
    void show() const;
};


 いかがでしたか。これで、C++入門は、とりあえず、おしまいにしましょう。


目次のページ
前のページ 最後のページ