コピーと代入

スポンサーリンク

コピーコンストラクタ

クラスのインスタンスを複製する際に呼び出される,コピーコンストラクタという特殊なコンストラクタがあります。
クラス名が Class1 のとき,コピーコンストラクタは Class1(const Class1& obj) { ... } と定義されます。

class Class1
{
    Class1(const Class1& obj);  // コピーコンストラクタ
};

次のプログラムは,時計を表すクラス Clock に,デフォルトコンストラクタ,コピーコンストラクタ,代入演算子を定義したものです。

#include <iostream>

class Clock
{
public:
    int hour;
    int minute;
    Clock();                         // デフォルトコンストラクタ
    Clock(const Clock&);             // コピーコンストラクタ
    Clock& operator=(const Clock&);  // 代入演算子
};

// デフォルトコンストラクタ
Clock::Clock()
    : hour(0), minute(0)
{
    std::cout << "default constructor called" << std::endl;
}

// コピーコンストラクタ
Clock::Clock(const Clock& obj)
    : hour(obj.hour), minute(obj.minute)
{
    std::cout << "copy constructor called" << std::endl;
}

// 代入演算子
Clock& Clock::operator=(const Clock& rhs)
{
    std::cout << "operator = called" << std::endl;
    this->hour = rhs.hour;
    this->minute = rhs.minute;
    return *this;
}

void func1(Clock c) { }

int main()
{
    Clock c;     // 出力: default constructor called
    func1(c);    // 出力: copy constructor called
    return 0;
}

関数 func1 に Clock クラスのインスタンスを値渡しすると,コピーコンストラクタが呼び出されることがわかります。

プログラマがコピーコンストラクタを定義しなかった場合は,コンパイラが自動的に実装します。
代入演算子についても,プログラマが代入演算子を定義しなかった場合は,コンパイラが自動的に実装します。

初期化と代入の違い

次のプログラムを実行すると,代入演算と初期化とで呼び出される関数が異なることがわかります。

class Clock
{
    // 上の Clock クラスの定義と同じ
}

int main()
{
    Clock c1;       // 出力: default constructor called
    Clock c2;       // 出力: default constructor called

    // 代入演算
    c2 = c1;        // 出力: operator = called

    // 初期化
    Clock c3 = c1;  // 出力: copy constructor called
    Clock c4(c1);   // 出力: copy constructor called

    return 0;
}

explicit 指定子

次のプログラムの関数 func1 を見ると,仮引数の型は Class1 であるが,int 型の実引数を渡しても関数を呼び出せることがわかります。
これは,int 型の引数を取るコンストラクタ Class1(int n) が暗黙的に呼び出されるためです。

class Class1
{
public:
    Class1(int n) { }
};

void func1(Class1 c) { }

int main()
{
    int n = 123;

    func1(n);      // 可 (関数呼び出し)
    Class1 a = n;  // 可 (コピー初期化)
    Class1 b(n);   // 可 (直接初期化)

    return 0;
}

こうした暗黙的な型変換を防ぐには,次のように explicit 指定子をコンストラクタに付けます。
このプログラムのコンパイル結果から,コピー初期化と直接初期化の挙動の違いがわかります。

class Class1
{
public:
    explicit Class1(int n) { }
};

void func1(Class1 c) { }

int main()
{
    int n = 123;

    func1(n);      // 不可 (関数呼び出し)
    Class1 a = n;  // 不可 (コピー初期化)
    Class1 b(n);   // 可 (直接初期化)

    return 0;
}
スポンサーリンク