ストラウストラップのプログラミング入門(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); }
};

Xのオブジェクトを生成したりコピーしたりして、コンストラクタとデストラクタの動きを確認する、というわけです。

分かったこと

  1. 参照の評価では、コピーコンストラクタは実行されない
  2. 参照に参照を代入しても、コピー代入は実行されない
  3. vectorオブジェクトの確保では、要素をフリーストアにコピーする(…ように見える)
  4. 配列の確保では、コピーコンストラクタは動かず、単に確保し、解放する(…ように見える)

# 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)

配列の確保では、コンストラクタとデストラクタだけが実行され、コピーコンストラクタは動きません。

そして、メモリはやっぱり昇順に確保されて、確保したのと逆の順に解放されています。

というわけで、今日はここまで。