C++の基礎 - コンストラクタ

提供:MochiuWiki : SUSE, EC, PCB
2021年11月22日 (月) 19:14時点におけるWiki (トーク | 投稿記録)による版 (文字列「</source>」を「</syntaxhighlight>」に置換)
ナビゲーションに移動 検索に移動

概要

自動生成されるコンストラクタは、デフォルトコンストラクタの他にコピーコンストラクタがある。


デフォルトコピーコンストラクタ

CMyClassのインスタンスを引数にして、新しいCMyClassのインスタンスを生成することができる。
clsMyClass2の中身を確認すると、clsMyClass1と同じ値になる。
これは、デフォルトコピーコンストラクタというものが自動的に生成されるからである。
デフォルトコピーコンストラクタは、自分と同じクラスのインスタンスを引数とするコンストラクタで、
メンバ変数をすべてコピーしたものが新しく生成される。

<source lang="c++">
#include <iostream>
#include <string>

class CMyClass
{
   private:
      int m_iNumber;
      std::string m_strName;

   public:
      // コンストラクタ
      CMyClass() : m_iNumber(0) {}
      CMyClass(int n, char *s) : m_iNumber(n), m_strName(s) {}

      int getNumber() { return number; }
      void setNumber(int n) { number = n; }

      std::string getName() { return name; }
      void setName(char* s) { name = s; }
};

int main()
{
   CMyClass clsMyClass1(1, "John");

   std::cout << "number : " << clsMyClass1.getNumber() << std::endl;
   std::cout << "name : " << clsMyClass1.getName() << std::endl;

   // デフォルトコピーコンストラクタの呼び出し
   CMyClass clsMyClass2(clsMyClass1);
   // デフォルトコピーコンストラクタの呼び出し(上と同じ)
   CMyClass clsMyClass3 = clsMyClass1;

   std::cout << "number : " << clsMyClass2.getNumber() << st::endl;
   std::cout << "name : " << clsMyClass2.getName() << std::endl;

   std::cin.get();
}
</syntaxhighlight>



デフォルトコピーコンストラクタが使用できないケース

デフォルトコピーコンストラクタは便利であるが、過信していると致命的なバグの原因となる場合がある。

下記のソースコードにおいて、メンバ変数にポインタを持つクラスの場合、
デフォルトコピーコンストラクタが実行されると、このポインタのアドレス値もコピーされる。
つまり、clsMyClass1とclsMyClass2の*m_piNumberが指すアドレスはどちらも同じになる。
clsMyClass2を通してm_piNumberの値を変更すると、無関係のclsMyClass1にも影響が出てしまう。
このような場合には、デフォルトコピーコンストラクタを使用してはいけない。

<source lang="c++">
#include <iostream>

class CMyClass
{
   private:
      int *m_piNumber;
   public:
      CMyClass(int *p = 0) : m_piNumber(p) {}

      int *get() { return m_piNumber; }
      void set(int *p) { m_piNumber = p; }
};

int main()
{
   int num = 10;
   CMyClass clsMyClass1(&num);

   // デフォルトコピーコンストラクタを使用
   CMyClass clsMyClass2(clsMyClass1);
 
   // clsMyclass2から変数m_piNumberの値を変更する
   *pc2.get() = 20;

   std::cout << "clsMyClass1のm_piNumberの値: " << *clsMyClass1.get() << std::endl;
   std::cout << "clsMyClass1のポインタ : " << clsMyClass1.get() << std::endl;

   std::cout << "pc2値: " << *pc2.get() << std::endl;
   std::cout << "pc2ポインタ: " << pc2.get() << std::endl;

   std::cin.get();
}
</syntaxhighlight>



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

これを解決するには、自分でコピーコンストラクタを定義する。

コピーコンストラクタは、自身のインスタンスの参照を引数に取るコンストラクタである。
コピーコンストラクタで、コピーしたいメンバ変数を任意に選択することができる。
ただし、このままだとメンバ変数が勝手にコピーされないだけで、デフォルトコピーコンストラクタを呼び出すことができる。

<source lang="c++">
class CMyClass
{
   private:
      int *m_piNumber;

   public:
      CMyClass(int *p = 0) : m_piNumber(p) {}

      // コピーコンストラクタ
      CMyClass(const CMyClass &c)
      {
         m_piNumber = nullptr;
      }

      int *get() { return m_piNumber; }
      void set(int *p) { m_piNumber = p; }
};

int main()
{
   int num = 10;
   CMyClass clsMyClass1(&num);

   // デフォルトコピーコンストラクタが使用できてしまう
   CMyClass clsMyClass2(clsMyClass1);
 
   // 実行時にエラーになる
   *clsMyClass2.get() = 20;
}
</syntaxhighlight>



コピーコンストラクタの禁止

上記の場合には、コピーコンストラクタ自体を禁止するとよい。
コピーコンストラクタをprivateに記述することで、外部からはアクセスできない状態にする。
そのため、コピーコンストラクタを使用した時点でコンパイラがエラーを出すので、コーディング時にエラーに気付くことができる。

なお、この方法で禁止できるのはコピーコンストラクタの使用であり、代入操作は禁止されない。

<source lang="c++">
class CMyClass
{
   private:
      int *pointer;

   private:
      // コピーコンストラクタ
      CMyClass(const CMyClass &c) {}

   public:
      CMyClass(int *p = 0) : m_piNumber(p) {}
};

int main()
{
   int num = 10;
   CMyClass clsMyClass1(&num);

   // コンパイルエラーが出る
   CMyClass clsMyClass2(clsMyClass1);

   // コンパイルエラーが出る
   CMyClass clsMyClass3 = clsMyClass1;
}
</syntaxhighlight>



変換コンストラクタ

引数が1つだけのコンストラクタは、以下のようにして呼び出すことが可能である。

clsMyClassはCMyClassのインスタンスなのに、整数をそのまま代入することが出来る。(暗黙的な呼び出し)
1つの引数で呼び出せるコンストラクタは変換コンストラクタとして動作することができる。

変換コンストラクタは、与えられた値の暗黙的な変換が可能な場合、値からインスタンスを生成する。
変換ができない場合は、インスタンスを生成することは出来ない。

<source lang="c++">
class CMyClass
{
   private:
      int m_iNumber;
      double m_dReal;
      std::string m_strName;

   public:
      CMyClass() { number = 0; }

      // 変換コンストラクタ
      CMyClass(int n) : m_iNumber(n) {}
      CMyClass(double r) : m_dReal(r) {}
      CMyClass(char * s) : m_strName(s) {}
      CMyClass(int n, double r = 0.0)  // 1つの引数で呼び出せるので可能
      { 
         m_iNumber = n;
         m_dReal = r;
      }

      int get() { return number; }
      void set(int n) { number = n; }
};

int main()
{
   CMyClass clsMyClass;

   // 変換コンストラクタの呼び出し
   clsMyClass = 10;

   // 暗黙の変換ができない
   //clsMyClass = "abc";

   std::cout << clsMyClass.get() << std::endl;

   std::cin.get();
}
</syntaxhighlight>



変換コンストラクタの禁止

変換コンストラクタは便利であるが、以下のような問題もある。

関数funcはCMyClassのインスタンスを受け取るつもりで作成した関数である。
この関数は引数に整数を指定して呼び出すことが出来る。
これは、引数に指定した整数が関数funcに渡された時点で、CMyClassの変換コンストラクタが動作して、
CMyClassのインスタンスが生成されるためである。

<source lang="c++">
class CMyClass
{
   private:
      int m_iNumber;

   public:
      CMyClass(int n) : m_iNumber(n) {}
};

void func(CMyClass c)
{
   // 省略
}

int main()
{
   // エラーにならない
   func(10);
}
</syntaxhighlight>


この動作を意図していない場合が多い。
そのため、このような場合は、変換コンストラクタを禁止することが出来る。

コンストラクタの前にexplicitキーワードを付けると、暗黙的な呼び出しを禁止することが出来る。
変換コンストラクタは意図しない動作となることが多いので、基本的に禁止しておいた方がよい。

<source lang="c++">
class CMyClass
{
   private:
      int m_iNumber;

   public:
      // 暗黙的な呼び出しの禁止
      explicit CMyClass(int n) : m_iNumber(n) {}
};

void func(CMyClass c)
{
   // 省略
}

int main()
{
   // 変換コンストラクタの暗黙的な呼び出しはコンパイルエラー
   //func(10);

   // 変換コンストラクタの明示的な呼び出しはコンパイル可能
   CMyClass clsMyClass(10);
   func(clsMyClass);
}
</syntaxhighlight>