C++ コピーコンストラクタと代入演算子のオーバーロードを徹底攻略

それぞれの処理が呼び出されるタイミングについて徹底調査

ログを仕込んだテストプログラムにて、
コピーコンストラクタ、代入演算子オーバーロード
どのようなタイミングで呼び出されるか徹底的に確認。

ついでに、コンストラクタ、デストラクタにもログを仕込み、
インスタンスの生存期間も確認。

以下確認用コード。

#ifndef _ACCOUNT_H_INCLUDE_
#define _ACCOUNT_H_INCLUDE_

#include <iostream>
#include <vector>
#include <string>

using namespace std;

class Account {
public:
    Account();
    Account(string name, int age);
    ~Account();

    Account(const Account& obj);

    void operator=(const Account& obj);
    
    string getName();
    void setName(string name);

    int getAge();
    void setAge(int age);

private:
    string name;
    int age;
};

#endif // ifndef _ACCOUNT_H_INCLUDE_
#include "Account.h"
using namespace std;

Account::Account() : name(""), age(0)
{
    cout << "    Call Constructor 1" << endl;
}
Account::Account(string name, int age) : name(name), age(age)
{
    cout << "    Call Constructor 2" << endl;
}
Account::~Account()
{
    cout << "    Call Destructor" << endl;
}

Account::Account(const Account& obj)
{
    cout << "    Call Copy Constructor" << endl;
}

void Account::operator=(const Account& obj)
{
    cout << "    Call Operator=" << endl;
}

string Account::getName()
{
    return name;
}
void Account::setName(string name)
{
    this->name = name;
}
int Account::getAge()
{
    return age;
}
void Account::setAge(int age)
{
    this->age = age;
}
#include <iostream>
#include "Account.h"
using namespace std;

void TestFuncAtai(Account account)
{
    cout << "        TestFuncAtai Start!" << endl;
    cout << "        TestFuncAtai End!" << endl;
}

void TestFuncSanshou(Account& account)
{
    cout << "        TestFuncSanshou Start!" << endl;
    cout << "        TestFuncSanshou End!" << endl;
}

void TestFuncPointer(Account* account)
{
    cout << "        TestFuncPointer Start!" << endl;
    cout << "        TestFuncPointer End!" << endl;
}

int main()
{

    cout << "main() Start!" << endl;

    cout << "Account new ope1" << endl;
    Account ope1("テスト 太郎", 23);

    cout << "Account new ope2" << endl;
    Account ope2;

    cout << "ope1 -> ope2" << endl;
    ope2 = ope1;

    cout << "Account Pointer ope3" << endl;
    Account* ope3;

    cout << "ope1 -> ope3" << endl;
    ope3 = &ope1;

    cout << "ope3(ope1) -> ope2" << endl;
    ope2 = *ope3;

    cout << "Call TestFuncAtai" << endl;
    TestFuncAtai(ope1);

    cout << "Call TestFuncSanshou" << endl;
    TestFuncSanshou(ope1);

    cout << "Call TestFuncPointer" << endl;
    TestFuncPointer(&ope1);

    cout << "main() End!" << endl;
}

以下、実行ログ。

main() Start!
Account new ope1
    Call Constructor 2
Account new ope2
    Call Constructor 1
ope1 -> ope2
    Call Operator=
Account Pointer ope3
ope1 -> ope3
ope3(ope1) -> ope2
    Call Operator=
Call TestFuncAtai
    Call Copy Constructor
        TestFuncAtai Start!
        TestFuncAtai End!
    Call Destructor
Call TestFuncSanshou
        TestFuncSanshou Start!
        TestFuncSanshou End!
Call TestFuncPointer
        TestFuncPointer Start!
        TestFuncPointer End!
main() End!
    Call Destructor
    Call Destructor

テストプログラムの実行結果から分かったこと

コピーコンストラクタが呼び出されるタイミング

  • 関数へ値渡しをしたとき

値渡しの場合は、関数呼び出し時に引数に指定した変数が新たに作られる。
そのとき、引数に指定した変数を「実引数」と呼び、
新たに作られた変数を「仮引数」と呼ぶ。

そのタイミングで、コピーコンストラクタが呼び出され、
実引数の値で仮引数が初期化される。

代入演算子オーバーロードが呼び出されるタイミング

  • 代入したとき

ポインタ変数を含むクラスを代入または値渡しする際に注意すること

例えば、メンバ変数にポインタ変数を持ち、
コンストラクタでメモリを確保、
デストラクタでメモリを解放するクラスがあったとする。
そのようなクラスをデフォルトのコピーコンストラクタ、
代入演算子を使用してコピーしてしまうと、
ポインタ変数に格納されたアドレスをそのままコピーしてしまうことになる。
要するに、コピー元と先のポインタ変数が同じメモリを参照している状態になる。
その状態で、コピー先のデストラクタが呼ばれると、
コピー元、先のポインタ変数が指すメモリが解放されることになる。
そして再びコピー元のデストラクタが呼ばれると、
すでに解放されたメモリを解放しようとしてしまいエラーが発生してしまう。