ストラウストラップのプログラミング入門(28) 18章「ベクタと配列」(続)
『ストラウストラップのプログラミング入門』を読む。今日は、18章「ベクタと配列」18.3.2。間が開きましたが、前回の続きです。
本は読み進めず、コンストラクタとデストラクタの動きを追っただけ。リハビリ・リハビリ。
前提となるコード
p.573から引用します。
struct X { int val; void out(const string& s, int nv) { cerr << this << "->" << s << ": " << val << " (" << nv << ")\n"; } X(){ out("X()", 0); val = 0; } // デフォルトコンストラクタ X(int v) { out("X(int)", v); val = v; } X(const X& x) { out("X(X&) ", x.val); val = x.val; } // コピーコンストラクタ X& operator=(const X& a) // コピー代入 { out("X::operator=()", a.val); val = a.val; return *this; } ~X() { out("~X()", 0); } };
分かったこと
- 参照の評価では、コピーコンストラクタは実行されない
- 参照に参照を代入しても、コピー代入は実行されない
- vectorオブジェクトの確保では、要素をフリーストアにコピーする(…ように見える)
- 配列の確保では、コピーコンストラクタは動かず、単に確保し、解放する(…ように見える)
# 3つ目と4つ目は、そう観測したのだけれども、自信がないです。
以下、淡々と詳細
(1) グローバル変数
X glob(2);
000641A0->X(int): 0 (2)
コロンの直後の値が0です。これは、globがグローバル変数であり、メモリが確保された跡に0に初期化されるためです。
(2) ローカル変数を宣言して、引数のintで初期化
int main() { X loc(4); }
003BFE0C->X(int): -858993460 (4)
コロンの直後の値が、今度は何かゴミゴミしています。これは、locが初期化前のローカル変数であるためです。intを引数に取るコンストラクタが呼び出されるという点では(1)と同じですが、グローバル変数とローカル変数の違いが、ここにあります。
(3) ローカル変数を宣言して、他の変数を代入
int main() { X loc(4); // 済 X loc2 = loc; }
003BFE00->X(X&): -858993460 (4)
コピーコンストラクタが実行されます。locのval値が4だから、カッコの中は4になっています。上の出力では確認できませんが、この後で、loc2のvalに4が代入されます。
出力行の先頭に見えるメモリの位置も、(2)の状態から進んでいます(数値としては小さくなっているけれども)。
(4) 宣言済みローカル変数に、新しい名前無しオブジェクトを代入
int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); }
003BFC18->X(int): -858993460 (5) 003BFE0C->X::operator=(): 4 (5) 003BFC18->~X(): 5 (0)
まず、右オペランドで、intを引数に取るコンストラクタが実行されます。ここでも、初期化前のゴミゴミした値が見えています。
次に、コピー代入が実行されます。コピーコンストラクタではないことに注意。(3)と似ているのに、コンストラクタでなく代入が行われるのは、左オペランドにXの宣言がないから、と考えればよい・・・?(ハテ)
最後に、デストラクタが実行されます。コロン直後の値が5であることから分かるように、削除されるのは右オペランドです。代入が終わればもう不要になるから、削除されるのでしょう。
(5) 宣言済みローカル変数に、copy関数の戻り値を代入
X copy(X a) { return a;} int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); }
003BFBF4->X(X&): 911368474 (5) 003BFC30->X(X&): -858993460 (5) 003BFBF4->~X(): 5 (0) 003BFE00->X::operator=(): 4 (5) 003BFC30->~X(): 5 (0)
最初に、コピーコンストラクタが2回実行されています。これはおそらく、copy関数の内部と、戻り値(copy(loc)全体)の評価の時に実行されているのだろう、というのが前回の日記での推測でした。ここは、よくわからないままです。ただ、その後にデストラクタが実行されていることから、copy関数内部で確保したメモリが解放されていると考えると、納得がいく…かも?
さらに、コピー代入が実行され、二度目のデストラクタが実行されています。ここで、右オペランドが占めていたメモリが、解放されているのでしょう。
ここまでが、前回までに見た部分でした。だいぶ思い出してきました。
(6) 宣言済みローカル変数に、copy2関数の戻り値を代入
X copy2(X a) { X aa = a; return aa; } int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); }
003BFBF4->X(X&): 911368474 (5) 003BFBD4->X(X&): -858993460 (5) 003BFC48->X(X&): -858993460 (5) 003BFBD4->~X(): 5 (0) 003BFBF4->~X(): 5 (0) 003BFE00->X::operator=(): 5 (5) 003BFC48->~X(): 5 (0)
末尾の2つの出力は、(5)の最後と同様、右オペランドが不要になったから消しているのでしょう。
copy2関数はcopy関数と違い、内部でXのオブジェクトを宣言します。だから、冒頭で、コピーコンストラクタが(5)と比べて1回多く実行されているのでしょう。しかし、分からないのは前方です。右オペランドを評価している間のどのタイミングで、コピーコンストラクタが実行されているのかが、相変わらず不明です。うーん。。。
(7) ローカル変数を宣言して、引数のintで初期化
int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); // 済 X loc3(6); }
003BFDF4->X(int): -858993460 (6)
これは、(2)と同じです。
(8) Xの参照を宣言して、ref_to関数の戻り値を代入
X& ref_to(X& a) { return a; } int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); // 済 X loc3(6); // 済 X& r = ref_to(loc); }
何も出力されません。ref_to関数は、参照渡しの引数を取り、そのまま返します。
整理すると、
- 参照の評価では、コピーコンストラクタは実行されない
- 参照に参照を代入しても、コピー代入は実行されない
ということでしょうか。
(9) make関数の戻り値をdeleteする(×2回)
X* make(int i) { X a(i); return new X(a); } int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); // 済 X loc3(6); // 済 X& r = ref_to(loc); // 済 delete make(7); cout << "--------\n"; delete make(8); }
003BFBD8->X(int): -8557555555 (7) 000B55C0->X(X&): -8557555555 (7) 003BFBD8->~X(): 7 (0) 000B55C0->~X(): 7 (0) -------- 003BFBD8->X(int): -8557555555 (8) 000B55C0->X(X&): -8557555555 (8) 003BFBD8->~X(): 8 (0) 000B55C0->~X(): 8 (0)
make関数では、まず、intを引数に取ってローカル変数aを宣言します。次に、aを引数に取ってXオブジェクトをnewして、ポインタを返します。
Xはフリーストア上で作成されるから、コピーコンストラクタが実行されるし、後から削除もされます。また、先頭の値を見ると、これまでと随分(メモリアドレス的に)離れた場所に確保されたことが分かります。これがフリーストアでしょうか。
さらに、make(7)でメモリが解放された後、make(8)で同じ場所が再利用されていることも分かります。
(10) Xの要素を4つ持つvectorオブジェクトを宣言する
int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); // 済 X loc3(6); // 済 X& r = ref_to(loc); // 済 delete make(7); // 済 delete make(8); // 済 vector<X> v(4); }
003BF69C->X(): -858993460 (0) 000B5658->X(X&) : -842150451 (0) 003BF69C->~X(): 0 (0) 003BF69C->X(): 0 (0) 000B565C->X(X&) : -842150451 (0) 003BF69C->~X(): 0 (0) 003BF69C->X(): 0 (0) 000B5660->X(X&) : -842150451 (0) 003BF69C->~X(): 0 (0) 003BF69C->X(): 0 (0) 000B5664->X(X&) : -842150451 (0) 003BF69C->~X(): 0 (0)
都合4回、コンストラクタ、コピーコンストラクタ、デストラクタが実行されています。
Xのオブジェクトがvectorの要素として生成されるとき、作られたオブジェクトはどこぞにコピーされています。それは、出力の先頭の値を見る限り、フリーストアであるようです。vectorって、そうなんでしたっけ。
(11) XXのローカル変数を宣言する
struct XX { X a; X b; }; int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); // 済 X loc3(6); // 済 X& r = ref_to(loc); // 済 delete make(7); // 済 delete make(8); // 済 vector<X> v(4); // 済 XX loc4; }
003BFDBC->X() : -858993460 (0) 003BFDC0->X(): -858993460 (0)
XXクラスは、メンバーに2つのXオブジェクトを持ちます。そのため、Xのコンストラクタが2回実行されています。
ここでは、メモリが昇順に使われるんですね。(003BFDBC→003BFDC0)
(12) フリーストアでXを確保し、削除する
int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); // 済 X loc3(6); // 済 X& r = ref_to(loc); // 済 delete make(7); // 済 delete make(8); // 済 vector<X> v(4); // 済 XX loc4; // 済 X* p = new X(9); // フリーストア上のX cout << "--------\n" delete p; }
000B56A8->X(int): -842150451 (9) -------- 000B56A8->~X(): 9 (0)
003BF***から遠く離れたこの場所は、やっぱりフリーストアだったと分かりました。
ポインタ型への代入では、コピー代入は実行されないのですね。
(13) フリーストアでXの配列を確保し、削除する
int main() { X loc(4); // 済 X loc2 = loc; // 済 loc = X(5); // 済 loc2 = copy(loc); // 済 loc2 = copy2(loc); // 済 X loc3(6); // 済 X& r = ref_to(loc); // 済 delete make(7); // 済 delete make(8); // 済 vector<X> v(4); // 済 XX loc4; // 済 X* p = new X(9); // 済 delete p; // 済 X* pp = new X[5]; cout << "--------\n"; delete[] pp; }
000B56AC->X(): -842150451 (0) 000B56B0->X(): -842150451 (0) 000B56B4->X(): -842150451 (0) 000B56B8->X(): -842150451 (0) 000B56BC->X(): -842150451 (0) -------- 000B56BC->X(): -842150451 (0) 000B56B8->X(): -842150451 (0) 000B56B4->X(): -842150451 (0) 000B56B0->X(): -842150451 (0) 000B56AC->X(): -842150451 (0)
配列の確保では、コンストラクタとデストラクタだけが実行され、コピーコンストラクタは動きません。
そして、メモリはやっぱり昇順に確保されて、確保したのと逆の順に解放されています。
というわけで、今日はここまで。