C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++でコピーコンストラクタを実装する方法を徹底解説

本記事では、C++におけるコピーコンストラクタの実装方法について詳しく解説します。実装方法に入る前に、まず「コピーコンストラクタとは何か」を理解しておきましょう。

コピーコンストラクタとは

コピーコンストラクタとは、同じクラスの既存のオブジェクトを使って新しいオブジェクトを初期化するコンストラクタのことです。主に以下のような場面で使用されます。

  • 同じ型の別のオブジェクトから、新しいオブジェクトを初期化する
  • オブジェクトをコピーして、関数の引数として渡す
  • オブジェクトをコピーして、関数から戻り値として返す

クラス内でコピーコンストラクタが定義されていない場合、コンパイラが自動的にデフォルトのコピーコンストラクタを生成します。しかし、クラスがポインタ変数を持ち、動的メモリ確保を行っている場合は、独自のコピーコンストラクタを定義することが必須です。そうしないと、シャローコピーによって複数のオブジェクトが同じメモリ領域を参照し、デストラクタ実行時の二重解放(ダブルフリー)などの深刻なバグを引き起こす可能性があります。

コピーコンストラクタの最も一般的な形式は以下のとおりです。

classname (const classname &obj) {
// コンストラクタの本体
}

ここで obj は、別のオブジェクトを初期化するために使われるオブジェクトへの参照です。引数を const 参照にすることで、元のオブジェクトが変更されるのを防ぎ、不要なコピーも回避できます。

サンプルコード1:関数への引数渡しでコピーコンストラクタが呼ばれる例

#include <iostream>
using namespace std;

class Line {
   public:
      int getLength( void );
      Line( int len );             // 通常のコンストラクタ
      Line( const Line &obj);      // コピーコンストラクタ
      ~Line();                     // デストラクタ
   private:
      int *ptr;
};

// コンストラクタを含むメンバ関数の定義
Line::Line(int len) {
   cout << "通常のコンストラクタでptrを確保" << endl;
   // ポインタ用にメモリを確保
   ptr = new int;
   *ptr = len;
}

Line::Line(const Line &obj) {
   cout << "コピーコンストラクタでptrを確保" << endl;
   ptr = new int;
   *ptr = *obj.ptr; // 値をコピー
}

Line::~Line(void) {
   cout << "メモリを解放!" << endl;
   delete ptr;
}

int Line::getLength( void ) {
   return *ptr;
}

void display(Line obj) {
   cout << "線の長さ : " << obj.getLength() << endl;
}

// プログラムのmain関数
int main() {
   Line line(10);
   display(line);
   return 0;
}

出力結果

通常のコンストラクタでptrを確保
コピーコンストラクタでptrを確保
線の長さ : 10
メモリを解放!
メモリを解放!

この例では、display() 関数にオブジェクトを値渡しで渡しているため、関数呼び出しのタイミングでコピーコンストラクタが呼び出されます。関数のローカルコピーが破棄される際にデストラクタが1回、元のオブジェクトが破棄される際にもう1回デストラクタが呼ばれるため、「メモリを解放!」が2回出力されます。

サンプルコード2:既存オブジェクトから新規オブジェクトを生成する例

次に、既存のオブジェクトを使って同じ型の別のオブジェクトを生成する例を見てみましょう。少しだけ変更を加えた同じサンプルコードです。

#include <iostream>
using namespace std;

class Line {
   public:
      int getLength( void );
      Line( int len );             // 通常のコンストラクタ
      Line( const Line &obj);      // コピーコンストラクタ
      ~Line();                     // デストラクタ
   private:
      int *ptr;
};

// コンストラクタを含むメンバ関数の定義
Line::Line(int len) {
   cout << "通常のコンストラクタでptrを確保" << endl;
   // ポインタ用にメモリを確保
   ptr = new int;
   *ptr = len;
}

Line::Line(const Line &obj) {
   cout << "コピーコンストラクタでptrを確保" << endl;
   ptr = new int;
   *ptr = *obj.ptr; // 値をコピー
}

Line::~Line(void) {
   cout << "メモリを解放!" << endl;
   delete ptr;
}

int Line::getLength( void ) {
   return *ptr;
}

void display(Line obj) {
   cout << "線の長さ : " << obj.getLength() << endl;
}

// プログラムのmain関数
int main() {
   Line line1(10);
   Line line2 = line1; // これもコピーコンストラクタを呼び出す
   display(line1);
   display(line2);
   return 0;
}

出力結果

通常のコンストラクタでptrを確保
コピーコンストラクタでptrを確保
通常のコンストラクタでptrを確保
線の長さ : 10
コピーコンストラクタでptrを確保
線の長さ : 10
メモリを解放!
メモリを解放!
メモリを解放!
メモリを解放!

注目すべきは Line line2 = line1; の行です。代入演算子のように見えますが、これは初期化であるため、コピーコンストラクタが呼び出されます。その後、display() を2回呼び出すたびに、引数渡しによってコピーコンストラクタがさらに2回実行されます。最終的に4つのオブジェクトが生成され、それぞれが破棄される際にデストラクタが4回呼び出されることが出力から確認できます。

まとめ

コピーコンストラクタは、オブジェクトのコピーが発生するすべての場面(初期化・関数への値渡し・関数からの値返し)で自動的に呼び出される重要な仕組みです。特に動的メモリを扱うクラスでは、ディープコピーを行う独自のコピーコンストラクタを定義することで、メモリの二重解放や不正アクセスを防ぎ、安全なプログラムを実現できます。

  1. C++とOpenCVを使って線を描画する方法

    OpenCVで線を描画するには、始点と終点の2つの座標が必要です。さらに、線を描くためのキャンバス(描画領域)も用意しなければなりません。線を描画するために必要な要素OpenCVでは、キャンバスとして機能するのが行列(Mat)です。この行列に対して、線の始点と終点を定義し、加えて以下の情報も指定します。線の色線の太さつまり、OpenCVで線を描くには、行列・2つの点・色・線の太さという4つの要素を宣言しておく必要があります。必要なヘッダーファイルOpenCVで線を描画するには、<imgproc.hpp>ヘッダーをインクルードします。これは、line()関数がこのヘッダー内で定義され

  2. C++で点集合の線対称(ラインリフレクション)を判定するアルゴリズム

    問題概要2次元平面上にn個の点が与えられます。このとき、y軸に平行な直線で全ての点を鏡映(反射)した結果が、元の点集合と完全に一致するような直線が存在するかどうかを判定します。言い換えれば、ある直線を対称軸として全ての点を反転させたとき、反転後の点の集合が元の集合と同一になるかを確認する問題です。例えば、入力が points = [[1,1],[-1,1]] の場合を考えてみましょう。この場合、x = 0 の直線(y軸)を対称軸とすると、点 (1,1) は (-1,1) へ、(-1,1) は (1,1) へと移ります。点集合全体としては変化がないため、出力は true となります。解法のポイン