コンストラクタとデストラクタ

C++でクラスの勉強を始めたら最初の方で覚える内容ですね。いまさらそんな基本的な内容のトピックを取り上げてどうするんだといわれそうだがしかし私個人的にちょっとどういう仕様か不安な部分がいくつかあったので実験してみました。

まずは実験用のクラスを作りました。

class C{
public:
	C(){ cout << "default constractor" << endl; }
	C( const C& e ){ cout << "copy constractor" << endl; }
	virtual ~C(){ cout << "destractor" << endl; }
	C& operator=( const C& e ){ cout << "equal operator" << endl; return *this; }
};

はい。とってもわかりやすいですね(ぇ デフォルトコンストラクタ・コピーコンストラクタ・デストラクタ、ついでに代入演算子を呼び出されるとそのことを通知してくれるというだけのクラスです。こいつを使っていろいろ実験してみました。ちなみに試したのはVC2003だけですので他のコンパイラだと違う結果になる可能性もありますのでお気をつけください。

以下、クソ長いので


実験1

C c1;
C c2( c1 );
C c3 = c1;
C c4;
c4 = c1;

結果1

default constractor
copy constractor
copy constractor
default constractor
equal operator
destractor
destractor
destractor
destractor

はい。教科書通りですね。面白くともなんともありません。あえて注目するとしたら3番目の初期化が代入演算子でなくコピーコンストラクタが呼ばれている点でしょうか。まあコピーコンストラクタと代入演算子が違う働きをすることなんてまずないでしょうから問題ナシ(ぇー


実験2

struct S{
	C c;
};

S s1;
S s2( s1 );
S s3 = s1;
S s4;
s4 = s1;

さて、実験1と違って今度はネストしています。しかもネストしている構造体はコンストラクタ類がありません。なので本来ならコピー関連はすべてコンパイルエラーで弾かれるべきだと個人的には思うんですがC++コンパイル通るしちゃんと動くんですよね。正直言ってカンベンして欲しいんですが・・・。でまあ動くなら動くで仕方がないとしてどう動いているかというのを確かめるテストです。コピー関連が定義されてないクラスをコピーすると、メモリコピーになるということを聞いたことがある。もしそれが本当ならクラスCのコピーコンストラクタや代入演算子は呼ばれないはずである。さてはて結果は・・・?
結果2

default constractor
copy constractor
copy constractor
default constractor
equal operator
destractor
destractor
destractor
destractor

なんと結果1と全く同じである。つまり、メンバのコピーコンストラクタや代入演算子が正しく呼ばれているのである。ちょっとビックリ。これならメンバコピーの為のコピーコンストラクタや代入演算子を頑張って作る必要はなさそうだな。


実験3

void Funct(){
	C c;
	throw "exception";
}

cout << "function start" << endl;
try{
	Funct();
}
catch( const char* e ){
	cout << e << endl;
}
cout << "function end " << endl;

今度は例外処理が絡んだ場合の話である。例外が投げられたらキャッチされるまで全ての処理をすっ飛ばすというのがC++の例外処理の仕様だが、気になるのがデストラクタがちゃんと呼ばれるかどうかである。今回の実験は関数を抜けた後、ちゃんとクラスCが正しく解体されているかを確かめるのが目的です。
結果3

function start
default constractor
destractor
exception
function end

どうやらちゃんとデストラクタが呼ばれているようですね。まあ当たり前といえば当たり前なんですがちゃんとこうした証拠がそろえば一安心です(^^;


実験4

void Funct(){
	throw C();
}

cout << "function start" << endl;
try{
	Funct();
}
catch( const C& ){
	cout << "catch" << endl;
}
cout << "function end " << endl;

さて今回のトピックの本題にたどり着きました。最初からこれ一つだけでもよかったんだけどまあ折角だし(何)。今回はクラスを投げてます。投げられたクラスはいつ解体されるか?それが疑問だったんですよね。例外処理はスコープや処理を無視してすっ飛んでいくのでメモリ関係がよくわからないので試してみたかったんですよね(^^; んで結果は以下の通り。
結果4

function start
default constractor
catch
destractor
function end

とりあえずデストラクタは呼ばれているのでちゃんと解体は出来ているようです(そりゃそうだ)。で、注目すべき点はどこで解体されているか。なんと例外がキャッチされてそのスコープが抜けた後なんですよね。考えてみれば当たり前な気もしますがちょっと驚きました。これなら平気で危ないクラス(何)もガンガン投げられますね(ぇ

実験5

void Funct(){
	C c;
	throw c;
}

cout << "function start" << endl;
try{
	Funct();
}
catch( const C& ){
	cout << "catch" << endl;
}
cout << "function end " << endl;

さて、先程と間違い探し級な違いですが、今度はローカルオブジェクトを投げてます。私の予想では先程と同じ結果になると思っていたのですが・・・結果は以下の通り。
結果5

function start
default constractor
copy constractor
destractor
catch
destractor
function end

なんと、例外を投げる直前にコピーコンストラクタが呼ばれています。つまり、例外として投げるためのインスタンスを生成してそこに内容をコピーしてそいつを投げています。なるほど・・・その手があったか(ぇ

実験6:おまけ

static C c;

int main(){
	return 0;
}

グローバルに置いてみました。
結果6

default constractor
destractor

ちゃんとデストラクタが呼ばれるらしいです(’’