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に格納することなので、代入ではなくコピーというのです。つまり、こういうことです。
ポインタをデータメンバに持ち、それらが指し示すオブジェクトを生成したり破棄したりするクラスでは、コピー・代入に注意が必要!
それなら、「コピーや代入をしなければいいじゃん?」と思うかもしれません。しかし、そのような「約束」はおうおうにして忘れてしまうものです。ちゃんと、対処するコードを書いておかなければならないのです。どうすればいいのでしょう?次回にご期待を。