第四回-03 : コピーコンストラクタ・代入演算子のオーバーロード

本ページでは「コピーコンストラクタ」、
および「代入演算子のオーバーロード」について解説する。

どういう時にコピーコンストラクタや代入演算子の記述が必要になるのかであるが、
第二回-05 : オブジェクトの代入」や 「第三回-03 : 関数の戻り値」で既に学んだように、
メンバ変数としてポインタをもつクラスにおいて代入などを行う際に必要になるのであった。

まずこのページを学ぶまえに以前のページをしっかり復習しておくこと。


コピーコンストラクタ・代入演算子の呼ばれるタイミング

まず、コピーコンストラクタと代入演算子がどのようなタイミングで呼ばれるのかを簡単に解説しておこう。

int main(){

  samp x;   // ()
  samp y=x; // ()
  samp z;   

  z = x;    // ()

 //  以下、なんらかの処理
}


上記の例を見てみよう。この例は main 関数内で samp クラスのオブジェクトを 3 つ定義している。
(もちろん、samp クラスの定義は与えないないのでこれだけでは動かないが、解説のためにはこれだけで十分である) このように同じ「=」であっても、コピーコンストラクタが呼ばれる場合と代入演算子が呼ばれる場合があることに注意。
samp クラスがメンバとしてポインタを持つ場合はこの「=」によって問題が起こる場合があるのであった。(第二回-05 : オブジェクトの代入第三回-03 : 関数の戻り値を参照)

さて、「samp y=x;」なる初期化であるが、実はこのページで初めて登場した記述であるから、「そんな記述は自分はしないよ」と思う人もいるかも知れない。


しかし、上記のような場合以外にも「関数へのの引数としてオブジェクトを渡す場合」にもコピーコンストラクタは起動される (次の例でみる)。
さらに、「第三回-03 : 関数の戻り値」で触れたように、C++ ではコンパイラが勝手に一時オブジェクトを作成することがあり、
その際に (意図しないにも関わらず) コピーコンストラクタが呼ばれることもある。

そのため、「『samp y=x;』なんて記述は自分はしない」という人も、代入演算子の記述を行う際はコピーコンストラクタの記述も同時に行う必要がある。

さて、以下でその記述方法を見ていこう。


strtype クラスにおけるコピーコンストラクタ・代入演算子

ここででは、コピーコンストラクタと代入演算子の記述例を strtype クラスについて行う。
strtype クラスは 「第二回-04 : メモリとポインタ」、 「第二回-05 : オブジェクトの代入」 で解説されているので、自信のない人は復習しておこう。

strtype クラスにコピーコンストラクタと代入演算子の記述を追加したソースコードを以下に記述する。

#include <iostream>  // cout を使うため
#include <cstring>   // strlen 関数、strcpy 関数を使うため
#include <cstdlib>   // exit を使うため
using namespace std;

class strtype {  // クラス宣言
    char *p;
  public:
    strtype(){ p=0; cout << "デフォルトコンストラクタ\n";} // デフォルトコンストラクタ
    strtype(char *s);           // 文字列で初期化するためのコンストラクタ
    strtype(const strtype &o);  // コピーコンストラクタ

    ~strtype(){delete[] p; cout << "デストラクタ\n";} // デストラクタ

    strtype &operator=(const strtype &o);  // 代入演算子

    char *get(){ return p; }
};

// 文字列で初期化するコンストラクタ
strtype::strtype(char *s){

  cout << "文字列で初期化するコンストラクタ\n";

  int l;

  l = strlen(s)+1;  // s の文字列の長さにヌル文字用の1を加える
  p = new char[l];

  if(!p) { 
    cout << "メモリ割り当てエラー\n";
    exit(1);
  }

  strcpy(p,s);
}

// コピーコンストラクタ
strtype::strtype(const strtype &o){

  cout << "コピーコンストラクタ\n";

  int l;

  l = strlen(o.p)+1;  // o の文字列の長さにヌル文字用の1を加える
  p = new char[l];

  if(!p) { 
    cout << "メモリ割り当てエラー\n";
    exit(1);
  }

  strcpy(p,o.p);
}

// 代入演算子
strtype &strtype::operator=(const strtype &o){

  cout << "代入演算子\n";

  delete[] p;  // まず、現在のポインタの先を解放

  int l;

  l = strlen(o.p)+1;  // o の文字列の長さにヌル文字用の1を加える
  p = new char[l];

  if(!p) { 
    cout << "メモリ割り当てエラー\n";
    exit(1);
  }

  strcpy(p,o.p);

  return(*this);  // ここは決まり文句。これにより s1=s2=s3; などといった記述が可能になる。
}

// これは C 言語的な大域的関数
void show(strtype x)
{
  char *s;
  s = x.get();

  cout << s << "\n";
}

int main(){  // main 関数

  strtype a("Hello");
  strtype b("There");

  show(a);
  show(b);

//  strtype s1("This is a test.");   // こちらも有効にして動作を確認してみよ
//  strtype s2 = s1;
//  strtype s3;

//  s3 = s1;

  return 0;
}


クラス宣言部のポイントは以下である。 代入演算子の宣言「 strtype &operator=(const strtype &o);」であるが、
「s2=s1;」などのような代入の記述では実は「s2.operator=(s1);」なるメンバ関数が呼ばれているのである。

つまり、代入演算子「 strtype &operator=(const strtype &o);」を記述することで 「s2=s1;」と記述したときの振舞いを変更できる。
これがまさに「第二回-05 : オブジェクトの代入」でやりたかったことである。

クラス定義部には「文字列で初期化するコンストラクタ」、「コピーコンストラクタ」、「代入演算子」の定義が記述されている。
どれも似た記述になっていることに注意しよう。

以上の strtype クラスを利用する main 関数であるが、 デフォルトの記述では教科書 160 ページに対応するように、コピーコンストラクタの動作の確認のみとなっている。 をまず確認すること。

show 関数へ引数を渡すときにもコピーコンストラクタが呼ばれる理由は以下の通り。 show 関数の仮引数 x に対してオブジェクト a (または b) が渡されるのであるが、
このとき、仮引数 x はオブジェクト a (または b) で初期化される (すなわち模式的には「strtype x=a;」) のでコピーコンストラクタが呼ばれるのである。

コピーコンストラクタの振舞いを理解したら、今度は main 関数内部のコメント文を有効にし、そちらの振舞いを確かめてみよ。
こちらは「3つのコンストラクタ」と「代入演算子」が全て呼び出されているので、出力結果を追って振舞いを理解すること。

最後に、代入演算子の記述が何故本ページのタイトルにあるように「代入演算子のオーバーロード (多重定義)」となるのかに触れておこう。
一般に、代入演算子 operator= は様々な組み込み型でも定義されている。
例えば、「int = int」を規定する oeperator=、「double = double」を規定する oeperator= などである。
この operator= に対してさらに「strtype = strtype」を定義しようというのであるから、 これは多重定義に他ならない。

←第四回-02 : new/delete によるメモリの動的管理第四回-04 : 演算子のオーバーロード ([] と + )→

第四回トップページへ

クラスから入る C++ へ戻る