C++ コピーコンストラクタ【オブジェクトを使った初期化方法】

C++
この記事は約15分で読めます。

こんにちは、ナナです。

「コンストラクタ」とは、クラスオブジェクトを生成したときに呼び出される初期化を行うためのメンバ関数でした。

クラスオブジェクトの初期化方法によっては、さらに特別な「コピーコンストラクタ」と呼ばれるメンバ関数が呼び出されることになります。

コピーコンストラクタの役割と、使い方を解説していきましょう。

本記事で学習できること
  • コピーコンストラクタが呼び出されるタイミングとは?
  • コピーコンストラクタの定義方法とは?
  • コピーコンストラクタはなぜ必要なのか?
  • コピーコンストラクタがよく使われるシーンとは?
  • コピーコンストラクタの引数に「const」が付く理由とは?
スポンサー

コピーコンストラクタはどんな時に呼び出されるのか?

はい、はーい。新人の僕はみなさんから「コピーしておいて!」と頼まれることが毎日なんです。コピーくらい自分でしてほしいですよね~。

今回紹介してもらえる「コピーコンストラクタ」は僕と同じ仲間ですかね?友達になりたいです。

ナナ

君はコピー機を使って原稿を「コピー」しているわけだね。

「コピーコンストラクタ」はオブジェクトをコピーすることで初期化するための機能なんですよ。

まずは、「コピーコンストラクタ」がどのような時に呼ばれるのかを理解しましょう。

「初期化」と「代入」の違いを知ろう

コピーコンストラクタを知るためには、「初期化」と「代入」の違いを明確に意識できる必要があります。

それでは、int型の変数を例に違いをおさらいしておきましょう。

int main()
{
    int num = 50;   //  初期化

    num = 70;       //  代入
    num = 80;       //  代入

    return 0;
}

初期化と代入は共に「=」を利用したものであるため、この2つの違いを意識していない方もいますが、明確に違うのです。

項目意味備考
初期化変数定義と同時に値を入れること変数が生成される時に一度しかできない
代入定義済みの変数に値を入れること変数に対して何度も行うことができる

このように、「初期化」とは

変数定義と同時に値をいれる

ことを示します。

ナナ

変数定義が行われることで、変数用のメモリが確保されます。「初期化」とはその変数が生まれる瞬間に一度しかできないのです。

「代入」も値を入れるのは一緒ですが、「初期化」と異なり何度でも値を入れることができます。

「コピーコンストラクタ」はどんな時に呼ばれるのか?

それでは、本題の「コピーコンストラクタ」ですが、

同じクラスのオブジェクトを使って初期化するときに呼ばれるメンバ関数

のことです。

具体的な例で、コピーコンストラクタの呼び出しタイミングを示しましょう。

#include <stdio.h>

class POS
{
public:
    int x;
    int y;

    POS(int tmpx, int tmpy);
};

//  引数付きのコンストラクタ
POS::POS(int tmpx, int tmpy)
{
    x = tmpx;
    y = tmpy;
}

int main()
{
    POS posA(100, 200); //  posAに対するコンストラクタ呼び出し

    POS posB = posA;    //  posBに対するコピーコンストラクタ呼び出し

    printf("posB.x:%d posB.y:%d", posB.x, posB.y);

    return 0;
}

このプログラムでは、コピーコンストラクタはまだ定義していません。

コピーコンストラクタを定義していない場合は自動的に定義が行われ、クラスのメンバ変数の値がそのままコピーされることになります。

ナナ

メンバ変数の値がそのままコピーされるのは「構造体」と同じと思えば不思議ではありませんね。

スポンサー

コピーコンストラクタの定義方法と呼び出し方

ほほほーい。コンストラクタの定義って「クラス名」が名前になってましたよね。

「コピーコンストラクタ」もコンストラクタって付いてますから、やっぱり同じなんですか?

ナナ

するどいね、その通りだよ。「コピーコンストラクタ」も名前はクラス名になるんです。

コンストラクタとの違いは、引数の型が決められていることですね。

続いて「コピーコンストラクタ」を明確に定義してみましょう。

コピーコンストラクタの定義方法

次のプログラムが「コピーコンストラクタ」を定義したものです。

class POS
{
public:
    int x;
    int y;

    POS(int tmpx, int tmpy);
    POS(const POS & pos);   //  コピーコンストラクタ
};

//  引数付きのコンストラクタ
POS::POS(int tmpx, int tmpy)
{
    x = tmpx;
    y = tmpy;
}

//  コピーコンストラクタの定義
POS::POS(const POS & pos)
{
    x = pos.x;
    y = pos.y;
}

ここで「参照」というキーワードが登場しました。

「参照」に関してはC++で新しく追加された機能であり、『C++ 参照【関数におけるポインタ渡しと参照渡しの違い】』にて紹介しております。

ナナ

「参照」は「ポインタ」とよく似た役割を持つ機能です。最初に学ぶ人はポインタの簡易表現のようなもととして捉えるとよいでしょう。

コピーコンストラクタの呼び出しを確認してみる

それでは、次のプログラムを動かしてどのタイミングで「コンストラクタ」「コピーコンストラクタ」「デストラクタ」が呼ばれるのかを確認してみてください。

#include <stdio.h>

class POS
{
public:
    int x;
    int y;

    POS(int tmpx, int tmpy);
    POS(const POS& pos);
    ~POS();
};

POS::POS(int tmpx, int tmpy)
{
    printf("コンストラクタ\n");
    x = tmpx;
    y = tmpy;
}

//  コピーコンストラクタの定義
POS::POS(const POS& pos)
{
    printf("コピーコンストラクタ\n");
    x = pos.x;
    y = pos.y;
}

POS::~POS()
{
    printf("デストラクタ\n");
}

int main()
{
    POS posA(100, 200);     //  初期化
    {
        POS posB = posA;    //  コピーによる初期化

        posB = posA;        //  代入
    }

    return 0;
}

デバッガを使ってステップ実行を行うことで明確に呼び出しタイミングがわかります。

コンストラクタ
コピーコンストラクタ
デストラクタ
デストラクタ
ナナ

皆さん自身で動かして呼び出されるタイミングを明確に理解できるようにしておきましょう。

コピーコンストラクタのもう一つの呼び出し方

「コピーコンストラクタ」はオブジェクト定義時に「=」による初期化によって呼び出されます。

POS posA(100, 200);
POS posB = posA;    //  コピーコンストラクタの呼び出し

この呼び出し方以外にもうひとつ、別の書き方があります。

POS posA(100, 200);
POS posB(posA);         //  ()を使ったコピーコンストラクタの呼び出し

こちらの方が、よりコピーコンストラクタのメンバ関数を呼び出している印象が強くなります。

ナナ

どちらの書き方でも正解ですが、このような書き方があることも知っておくことで他の人が作ったプログラムも読めるようになります。覚えておきましょう。

スポンサー

コピーコンストラクタが必要な理由

はーい、気になることがありまーす。「コピーコンストラクタ」は定義しなくても、オブジェクトの中身がコピーされるんですよね?

じゃあ定義する意味なくないですか?別にそれで問題ないじゃないですか?

ナナ

そうだね、クラスの構成次第では特に定義しなくても問題がないね。

でもね、「コピーコンストラクタ」が必要になるケースがあるんですよ。それはクラスが資源を管理している時なんです。

何もしなくてもオブジェクトが持つ情報をコピーできるのであればよいのではないか?と思うかもしれません。

しかし、情報を単純にコピーするだけでは問題があるケースがあるのです。それが、資源を管理するクラスです。

資源を管理するクラスのコピー問題

次のようにメンバ変数にポインタを持つクラスを定義したとしましょう。コンストラクタではnew演算子を使ってヒープメモリを割り付け、デストラクタで解放するとします。

class Human
{
public:
    char * mName;   //  氏名

    Human(const char * name);
    ~Human();
};

//  コンストラクタ
Human::Human(const char * name)
{
    //  ヒープメモリを確保
    mName = new char[strlen(name) + 1]();

    //  引数の氏名をメンバ変数にコピー
    strcpy_s(mName, strlen(name) + 1, name);
}

//  デストラクタ
Human::~Human()
{
    //  ヒープメモリを解放
    delete[] mName;
}

このクラスを使って、オブジェクトを別オブジェクトへ初期化してみます。

int main()
{
    Human human1("Jones");
    {
        //  別オブジェクトへ初期化
        Human human2 = human1;
    }
    
    return 0;
}

オブジェクトをコピーすることで、ポインタの内容がそのままコピーされます。

この後、human2オブジェクトがブロックから外れることで「デストラクタ」が呼ばれます。そうすると動的メモリが解放されます。

最後にhuman1オブジェクトが動的メモリを再度解放しようとして、例外によりプログラムが停止します。

ナナ

このように資源を持つクラスは、単純にコピーすると資源を共有してしまい、お互いの処理が影響しあってしまうのです。

この現象を防ぐためには、資源も含めて複製を行う必要があるのです。

コピーコンストラクタによる資源の複製

それではこの現象を「コピーコンストラクタ」を使って防止してみましょう。

//  コピーコンストラクタ
Human::Human(const Human & human)
{
    //  ヒープメモリを新たに確保
    mName = new char[strlen(human.mName) + 1]();

    //  コピー元オブジェクトの氏名をコピー
    strcpy_s(mName, strlen(human.mName) + 1, human.mName);
}

このようにすることで、動的メモリの資源を新たに確保し複製を行っています。

ナナ

コピーというものは、単純にコピーすればよいというわけではないということです。

複雑なデータ構造を持つクラスはコピーするのも大変な作業となります。

スポンサー

コピーコンストラクタがよく使われるシーンとは

ほいほーい。オブジェクトを使って初期化するって、そんなに使うことはないですよね?僕もプログラムしてますけど、あんまりそんなシーンに出くわしたことがないですよ。

あまり使うことはないけど、大事ってことなんですかね?

ナナ

それはね、初期化が行われていることに気づいてないだけですよ。実はあるシーンにおいて「コピーコンストラクタ」はよく行われることになるんです。

関数における値渡しの引数はコピーコンストラクタが呼ばれる

コピーコンストラクタがもっともよく使われるシーンは、関数呼び出しです。

関数の引数が「値渡し」である場合、引数のオブジェクトに対して「コピーコンストラクタ」が呼ばれることになります。

関数の引数に対しては、オブジェクトが初期化の構文にて受け渡しがされるため「コピーコンストラクタ」が呼ばれるのです。

ナナ

案外身近なところで、初期化が行われるのがわかりますね。

「コピーコンストラクタ」を正しく処理しないと、このようなタイミングで資源管理がおかしくなってしまいますよ。

スポンサー

Q&A:コピーコンストラクタに関するよくある質問

Q:コピーコンストラクタの引数の参照に「const」が付いているのはなぜ?

はーい、質問です。「const」って定数化するときに付ける修飾子でしたよね。

「参照」の引数にはどうして「const」がついているんですか?

ナナ

「const」についてはC言語と同じで、変数を定数化するためのものだね。

実は「コピーコンストラクタ」は参照に「const」を付けなくても定義が可能です。でも付けておくことでより安全にオブジェクトを管理できるようになるんです。

コピーコンストラクタは次のように「const」を付けなくても定義ができます。

//  コピーコンストラクタの定義
POS::POS(POS & pos)
{
    x = pos.x;
    y = pos.y;
}

それでは、なぜ「const」を付けるのかというと、初期化元となるクラスオブジェクトを誤って変更しないように保護しているのです。

コピーという作業は、コピー元となる原稿はコピーしても変化しませんよね。

つまり、コピーコンストラクタの引数であるコピー元のオブジェクトは変化することが許されないわけです。

POS::POS(POS & pos)
{
    pos.x = 500;    //  コピー元を書き換え可能

    x = pos.x;
    y = pos.y;
}
POS::POS(const POS & pos)
{
    pos.x = 500;    //  コピー元を書き換え可能

    x = pos.x;
    y = pos.y;
}

このように「参照」で渡されたオブジェクトへの変更を「const」を付与することでガードすることができるようになります。

ナナ

C++では、関数の引数でよくオブジェクトへの「参照」が登場します。

「const」をつけるべきシーンであれば、積極的に付けることで予期せぬ間違いを防止することができるようになります。

Q:コピーコンストラクタでコピー処理以外のことでも実施していいの?

僕、会社でコピーを取るときに、たまにイタズラで紙の端っこに落書きしてるんですよね。

「コピーコンストラクタ」でもコピー以外のことだってやってもいいんですよね。

ナナ

「コピーコンストラクタ」というメンバ関数で何を行うかはもちろんエンジニアに決定権があります。

ただ、「コピーコンストラクタ」の役割は、オブジェクトによるコピーをするためのものだということを忘れてはいけませんよ。

例えば、コピーコンストラクタで次のようにコピーとは異なる処理をしたとしましょう。

//  コピーコンストラクタの定義
POS::POS(const POS & pos)
{
    //  10倍の値をX,Yに設定
    x = pos.x * 10;
    y = pos.y * 10;
}

この状態で次のようにコピーコンストラクタを呼び出してみましょう。

int main()
{
    POS posA(100, 200);

    //  コピーしないコピーコンストラクタ
    POS posB = posA;

    printf("posB.x:%d posB.y:%d", posB.x, posB.y);

    return 0;
}
posB.x:1000 posB.y:2000

もちろん、結果は元のposAが持つX、Yの10倍の値となります。

プログラムとしては正しい動きですが、「コピー」を行うための処理を期待しているのにコピーされていないわけです。

プログラムとは自由なものではありますが、はちゃめちゃなプログラムは誰も求めていません。

ナナ

みんなが期待する動きをさせるようにプログラムを作りましょう!

Q:オブジェクトの「代入」の場合は問題にならないの?

は、は、は、はーーい。オブジェクトの「代入」だってコピーと言えばコピーですよね。オブジェクトの「代入」の場合はどうなっちゃうんですか?

「コピーコンストラクタ」は呼ばれないんですよね?

ナナ

そこは痛いところを突いてくるね。確かに「代入」も同じくコピー処理なんだよね。でも「コピーコンストラクタ」は呼ばれません。

ってことは問題だね~。

それでは実際にオブジェクトを代入して結果がどうなるかを見てみましょう。

int main()
{
    POS posA(100, 200); //  コンストラクタ呼び出し
    POS posB = posA;    //  コピーコンストラクタ呼び出し
    POS posC(300, 400);

    //  オブジェクトの「代入」もコピー?
    posA = posC;

    printf("posA.x:%d posA.y:%d", posA.x, posA.y);

    return 0;
}
posA.x:300 posA.y:400

このように、「代入」においてもオブジェクト中身がコピーされていますね。

つまり、「代入」においても資源の複製問題が同様に発生することになります。しかし、「代入」ではコピーコンストラクタは利用できません。

この問題を解決するには「演算子オーバーロード」という機能を利用して対策します。

ナナ

「オーバーロード」に関しては、別の記事で紹介しますのでお待ちください。

次章