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

 こんにちは。今まで、ずーっと、気になっていたことがあります。それはコピーコンストラクタと代入演算子です。この話をしなければ、C++の初級を卒業することはできません。(「入門」は初心者向けです。初心者には少し難しすぎる話題かもしれません。)また、この話は純然たる初級の話ですが、すぐ中級が見える初級なのです。初心者/初級者には難しく、かつ、退屈だと思います。前にも書きましたが、特に初心者のするべき事はプログラミングを楽しむということで、苦しむということではないと思うのです。それで、当初の予定ではこの話は「入門」ではせずに、「各自勉強してください」と言うつもりでした。
 しかし、それでも「各自、、、」というのは無責任かなぁとも思い、良心にしたがって書くことにしました。ただ、ちょっと読んでみて、面倒だとか退屈だと感じた人は、以下の規則だけ守るぞと、思って後の部分は読まないようにしてください。いずれ時が来たときにどこかで勉強すればよいと思います。

では、今回・次回の解説を読まない人のお約束:
ポインタをメンバに持つクラスはつくらないぞ

 もし、ポインタをメンバに持つクラスをつくりたいと思うのなら、この解説を読むか、教科書のコピーコンストラクタ/代入演算子の項を読んでください。繰り返しますが、初心者にはかなりヘビーです。この辺から中級が見えてくるところでもあります。

 さて、上の「お約束」からわかるように、これは「ポインタをデータメンバに持つクラス」の話です。
 そのために、Aruという名前のクラスのポインタを持つBetuというクラスを考えてみることにします。プログラムとしては何の意味もありませんが、C++を理解するための例です。

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

class Betu
{
    Aru *a;  //Aruのポインタ
public:
    Betu();
    ~Betu();
    void input();
    void show() const;
};

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

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

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のコンストラクタを見てください。一般に、C++(やC)では、定義しただけの変数にはどんな値が入っているかわかりません。ところで、ポインタの値として0は、「どこも指し示さない」という意味になります。Betuができた直後には、まだデータが入力されていないので、aには0を入れておくのがふさわしいでしょう。
 デストラクタでは、オブジェクトを破棄しています。デストラクタが呼び出される前に、aが何かのオブジェクトを指すようになっていなければ、aの値は(コンストラクタのおかげで)0になっているはずです。0の入っているポインタにdeleteを適用しても問題はありません。逆に、aに不適当な値が入っていて、これをdeleteしようとすると実行時エラーになってしまうかもしれません。この意味でも、aの値を0にしておいてよかったのです。
 inputは単に、ユーザに入力させているだけですね。ただし、inputが2回以上呼ばれる可能性もあります。その場合にそなえて、すでにあるオブジェクトは破棄してから、新しくオブジェクトを生成するようにしました。ここでも、最初にinputが呼ばれる時は、0にdeleteが適用されますが、問題ありません。
 showは、まずデータがあるかどうかを見てから出力するようにしました。aが0なら、オブジェクトがないということなので、その場合はshowを終了するようにしました。
 このクラスを使うプログラムは、たとえば、次のように書けると思います。AruとBetuは上で見たものなので、mainのところだけ見ればよいと思います。

//ab_sample.cpp
//実は問題があります。
#include <iostream>
#include <string>
using namespace std;

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

class Betu
{
    Aru *a;  //Aruのポインタ
public:
    Betu();
    ~Betu();
    void input();
    void show() const;
};

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

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

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;
}

int main()
{
    Betu one;
    one.input();
    one.show();
}

 これを実行すると、(たぶん)何も問題なく動くと思います。

 mainの中には、Aruのオブジェクトを破棄するコードがありませんが、これはBetuオブジェクトのoneが破棄されるときに、そのデストラクタで自動的に破棄されるのです。そのおかげで、オブジェクトの破棄のし忘れということがなくなります。実は、これこそ、デストラクタの一番一般的な利用方法なのです。この仕組みを使えば、ポインタを安全に扱えそうです。あーよかった。やれやれ。

 でもないのです。

 いえ、確かに、この方法を使えば、ポインタを(比較的)安全に使えます。お勧めです。しかし、まだ、Betuにはコードが足りないのです。たとえば、次のようなコードがあったとしましょう。

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

 こうすると、twoの値がoneに代入されます。それは、各データメンバが代入されるということです。その結果、oneとtwoのデータメンバであるポインタaが、同じ値を持つようになります。それは、「異なるポインタが同じオブジェクトを指し示す」ということです。


(同じオブジェクトを異なるポインタが指し示す(同じオブジェクトのアドレスを保持する))

 同じオブジェクトを異なるポインタが指し示しても、それ自身は問題ありません。しかし、これらのポインタには、inputやデストラクタでdeleteが適用されます。たとえば、なんらかの理由でtwoが先に破棄されて、そのaにdeleteが適用されると、oneのポインタは、オブジェクトがないところを指し示していることになります。


(oneのポインタが指し示すオブジェクトがもう破棄されているのに、oneは気がつかない。)

 これでoneがaの指し示すオブジェクトを使おうとすると、そんなオブジェクトはないので、実行時のエラーになってしまうのです(もちろん、実行時の状況によってエラーにならないこともあります)。

( わざとらしい例ですが、問題が出るコードとしては、次のようなものが考えられます。

Betu one;
one.input();
int x = 1;
if(x == 1){
    Betu two;
    two.input();
    one = two; //twoをoneに代入
}
one.show();

このコードでは、twoはif文の中カッコのブロック中で破棄されてしまいます。オブジェクトはブロックの中でのみ有効だからです。もちろん、実際には、もっと意味のあるコードでこのようなことが起こるわけです。)

 また、twoが破棄されたあとに、oneが破棄されると、もう一度oneのaが指し示している領域にdeleteをかけようとするでしょう。これも、場合によっては、おそろしいエラーになるはずです。

 また、同様のことが、コピーでも起こります。コピーとは、次のようなコードのことです。

Betu one;
...
Betu two = one;   //oneをtwoにコピー

 代入に似てますね。しかし、これは、twoを生成するときに、oneの値をtwoに格納することなので、代入ではなくコピーというのです。つまり、こういうことです。

 ポインタをデータメンバに持ち、それらが指し示すオブジェクトを生成したり破棄したりするクラスでは、コピー・代入に注意が必要!

 それなら、「コピーや代入をしなければいいじゃん?」と思うかもしれません。しかし、そのような「約束」はおうおうにして忘れてしまうものです。ちゃんと、対処するコードを書いておかなければならないのです。どうすればいいのでしょう?次回にご期待を。


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