特殊なコンストラクタ
コピーコンストラクタ
自動生成されるコンストラクタは、デフォルトコンストラクタのほかにコピーコンストラクタというものもあります。
デフォルトコピーコンストラクタ
#include <iostream>
#include <string>
class SimpleClass
{
private:
int number;
std::string name;
public:
//コンストラクタ
SimpleClass() { number = 0; }
SimpleClass(int n, char *s)
{
number = n;
name = s;
}
int getNumber() { return number; }
void setNumber(int n) { number = n; }
std::string getName() { return name; }
void setName(char* s) { name = s; }
};
int main()
{
SimpleClass sc1(1, "John");
//number: 1 name: John
std::cout << "number: " << sc1.getNumber();
std::cout << "\nname: " << sc1.getName() << std::endl;
//コピーコンストラクタの呼び出し
SimpleClass sc2(sc1);
//↓でもOK
//SimpleClass sc2 = sc1;
//number: 1 name: John
std::cout << "number: " << sc2.getNumber();
std::cout << "\nname: " << sc2.getName() << std::endl;
std::cin.get();
}
SimpleClassのコンストラクタ定義は「引数なし」「int型、char*型」のふたつしかありません。
しかし、34行目ではSimpleClassのインスタンスを引数にして、新しいSimpleClassのインスタンスを生成しています。
sc2の中身を確認すると、sc1と同じ値になっていることが確認できます。
なぜこんなことができるのかというと、デフォルトコピーコンストラクタというものが自動的に生成されるからです。
デフォルトコピーコンストラクタは、自分と同じクラスのインスタンスを引数とするコンストラクタで、メンバ変数をすべてコピーしたものが新しく作られます。
自動生成のコピーコンストラクタを使わない方がいいケース
コピーコンストラクタの自動生成は便利ですが、これに頼っていると思わぬバグの原因となる場合があります。
#include <iostream>
class PointerClass
{
int *pointer;
public:
PointerClass(int *p = 0) { pointer = p; }
int *get() { return pointer; }
void set(int *p) { pointer = p; }
};
int main()
{
int num = 10;
PointerClass pc1(&num);
//デフォルトコピーコンストラクタを利用
PointerClass pc2(pc1);
//pc2から値を書き換え
*pc2.get() = 20;
std::cout << "pc1値: " << *pc1.get() << std::endl;
std::cout << "pc1ポインタ: " << pc1.get() << std::endl;
std::cout << "pc2値: " << *pc2.get() << std::endl;
std::cout << "pc2ポインタ: " << pc2.get() << std::endl;
std::cin.get();
}
pc1値: 20 pc1ポインタ: 00CFFA38 pc2値: 20 pc2ポインタ: 00CFFA38
メンバ変数にポインタを持つシンプルなクラスです。
ポインタというのはメモリ上の位置を示す値です。
コピーコンストラクタはこの「値」までもそのままコピーしてしまいます。
つまり、インスタンスpc1とpc2のメンバ変数が指すメモリ位置はどちらも同じになっています。
これではインスタンスpc2を通してメモリ上の値を書き換えると、本来無関係なはずのインスタンスpc1にも影響が出てしまいます。
こういった場合にはデフォルトのコピーコンストラクタを使用すべきではありません。
コピーコンストラクタの定義
これを解決するには、自分でコピーコンストラクタを定義します。
class PointerClass
{
int *pointer;
public:
PointerClass(int *p = 0) { pointer = p; }
//コピーコンストラクタ
PointerClass(const PointerClass &c)
{
pointer = 0;
}
int *get() { return pointer; }
void set(int *p) { pointer = p; }
};
コピーコンストラクタは、自分自身のインスタンスの参照を引数に受け取るコンストラクタです。
ここでコピーしたいメンバ変数を任意に選ぶことができます。
単純なメンバ変数のコピー以外にも処理は書けますが、「コピー」に関係のない処理を書くとややこしくなるので書かない方が良いです。
引数は参照なので、それを通して書き換えが可能な状態です。
それでは危険なので、引数にはconstを付けておきます。
これで、コピーコンストラクタ内で引数を書き換えようとするとコンパイラがエラーを出してくれます。
ただし、このままだと「メンバ変数が勝手にコピーされない」だけで、コピーコンストラクタを呼び出すことはできてしまいます。
「コピーできたように見える」のに実際にはできていないので(メンバ変数pointerはNULL)、プログラムを実行するとアクセス違反が発生します。
int main()
{
int num = 10;
PointerClass pc1(&num);
//コピーコンストラクタが使用できてしまう
PointerClass pc2(pc1);
//実行時エラー
*pc2.get() = 20;
}
このような場合には、コピーコンストラクタ自体を禁止してしまうことができます。
コピーコンストラクタの禁止
class PointerClass
{
int *pointer;
//コピーコンストラクタ
PointerClass(const PointerClass &c) {}
public:
PointerClass(int *p = 0) { pointer = p; }
};
int main()
{
int num = 10;
PointerClass pc1(&num);
//この時点でコンパイラがエラーを出す
PointerClass pc2(pc1);
//これもエラーになる
PointerClass pc3 = pc1;
}
コピーコンストラクタをprivateに記述することで、外部からはアクセスができなくなります。
そのため、コピーコンストラクタを使用しようとした時点でコンパイラがエラーを出してくれますから、コーディング時にエラーに気づくことができます。
自動的に生成されるコンストラクタは、以下のように記述すると生成を禁止することができます。
引数の種類でコンストラクタの種類を判別していますので、引数は省略できません。
class PointerClass
{
int *pointer;
public:
PointerClass(int *p = 0) { pointer = p; }
//コピーコンストラクタの自動生成禁止
PointerClass(const PointerClass &c) = delete;
};
なお、この方法で禁止できるのはあくまで「コピーコンストラクタ」の使用です。
代入操作は禁止されませんから、以下のような場合は相変わらずコピーできてしまいます。
class PointerClass
{
int *pointer;
//コピーコンストラクタ
PointerClass(const PointerClass &c) {}
public:
PointerClass(int *p = 0) { pointer = p; }
};
int main()
{
int num = 10;
PointerClass pc1(&num);
PointerClass pc2;
//メンバ変数がそのままコピーされる
pc2 = pc1;
}
これを防ぐには代入時の処理も書き換えが必要になります。
その方法については演算子のオーバーロードで詳しく説明します。
変換コンストラクタ
引数がひとつだけのコンストラクタは、以下のようにして呼び出すことが可能です。
class SimpleClass
{
int number;
public:
SimpleClass() { number = 0; }
//変換コンストラクタ
SimpleClass(int n) { number = n; }
int get() { return number; }
void set(int n) { number = n; }
};
int main()
{
SimpleClass sc;
//変換コンストラクタの呼び出し
sc = 10;
//暗黙の変換ができないのでNG
//sc = "abc";
std::cout << sc.get() << std::endl;
std::cin.get();
}
scはSimpleClassのインスタンスなのに、整数をそのまま代入(のような操作を)することができます。
引数ひとつで呼び出せるコンストラクタは変換コンストラクタとして働きます。
変換コンストラクタは、与えられた値の暗黙的な変換が可能な場合、値からインスタンスを生成します。
変換ができない場合はインスタンスを生成することはできません。
「引数ひとつで呼び出せる」という条件を満たせば良いので、以下のコンストラクタはすべて変換コンストラクタとして働きます。
class SimpleClass
{
int integer;
double real;
std::string str;
public:
//全部変換コンストラクタ
SimpleClass(double x) { real = x; }
SimpleClass(char * x) { str = x; }
SimpleClass(int x, double y = 0.0) //これもOK
{
integer = x;
real = y;
}
};
デフォルト引数を指定した引数は、その引数を省略して呼び出すことができます。
その結果、引数ひとつで呼び出せるようになっていれば、変換コンストラクタとして働きます。
当然ながら、デフォルト引数を省略しない形では変換コンストラクタは呼び出せません。
//こういうのは×
SimpleClass sc1 = (1, 2.0);
//これは〇(通常のコンストラクタの明示的な呼び出し)
SimpleClass sc2(1, 2.0);
また、「=」記号で代入のような記述をしますが、これは代入ではなくコンストラクタの暗黙的な呼び出しです
以下のコードでは「変換コンストラクタの呼び出し」の表示が実行されます。
class SimpleClass
{
int integer;
public:
SimpleClass(int x)
{
std::cout << "変換コンストラクタの呼び出し" << std::endl;
integer = x;
}
};
int main()
{
SimpleClass sc = 10;
std::cin.get();
}
変換コンストラクタの禁止
変換コンストラクタは便利ですが、以下のような問題もあります。
class SimpleClass
{
int integer;
public:
SimpleClass(int x) { integer = x; }
};
void func(SimpleClass c)
{
//省略
}
int main()
{
//これはエラーにならない
func(10);
}
関数funcはSimpleClassのインスタンスを受け取るつもりで作った関数です。
この関数は引数に整数を指定して呼び出すことができます。
これは、引数に指定した整数が関数に渡された時点で、SimpleClassの変換コンストラクタが働きSimpleClassのインスタンスが生成されるためです。
この動作を意図してやるならば良いのですが、普通は意図していない場合が多いでしょう。
このような場合、変換コンストラクタを禁止することができます。
class SimpleClass
{
int integer;
public:
//暗黙的な呼び出しの禁止
explicit SimpleClass(int x) { integer = x; }
};
void func(SimpleClass c)
{
//省略
}
int main()
{
//暗黙的なコンストラクタの呼び出しはエラー
//func(10);
//明示的なコンストラクタの呼び出しはOK
SimpleClass sc(10);
func(sc);
}
コンストラクタの前にexplicitというキーワードを付けると、暗黙的な呼び出しを禁止することができます。
変換コンストラクタは意図しない動作となることが多いので、基本的に禁止しておいた方がいいかもしれません。