Effective C++ 入門
〜 第3章 コンストラクタ、デストラクタ、代入演算子 〜


□ 11項 □ ポインタメンバを持つクラスでは、コピーコンストラクタと代入演算子を定義しよう
■ 課題

ポインタメンバを持つクラスで、コピーコンストラクタのないクラスでは、C++のデフォルトのコピーコンストラクタが呼ばれ、コピーをするたびにメモリーリークが発生する可能性がある。

以下例。

#include <iostream>
#include <cstring>
using namespace std;

// コピーコンストラクタのない String クラス
class String {
public:
__String();
__String(const char* value);
__~String();
__const char* const ptr() const { return data; }
private:
__char* data;
};

String::String() : data(new char[1]) {
__cout << __PRETTY_FUNCTION___ << endl;
__*data = '\0';
};

String::String(const char* value) {
__cout << __PRETTY_FUNCTION___ << endl;
__int len = strlen(value);
__if (len) {
____data = new char[len+1];
____strncpy(data, value, len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____*data = '\0';
__}
}

String::~String() {
__cout << __PRETTY_FUNCTION___ << endl;
__delete [] data;
}

void function(String& a) {
__String b("World");
__// デフォルトのコピーコンストラクタ呼び出し。
__// ポインタ b.data が a.data にコピーされる。
__// a.data の内容は "Hello" から "World" になり、
__// a が保持していた文字列 "Hello" はメモリーリーク!
__a = b;

__// 関数を抜けた時点で b のデストラクタが呼ばれ、
__// 文字列 "World" は delete される。
__// a のデータの内容は不定になる
}

int main() {
__String a("Hello");
__cout << "a.data: " << a.ptr() << endl;
__function(a);
__cout << "a.data: " << a.ptr() << endl;
}

■ 解決

コピーコンストラクタ、代入演算子をきちんと定義する。

コピーコンストラクタや代入演算子をつかってほしくない場合は、単にクラス内で宣言し実装をしないことで抑止することができる


□ コピーコンストラクタ・代入演算子を定義
.... (snip) .....
class String {
public:
__String();
__String(const char* value);
__~String();
__String(const String& s);
__String& operator=(const String& s);
__const char* getData() const { return data; }
private:
__char* data;
};
.... (snip) ....
String::String(const String& s) {
__cout << __PRETTY_FUNCTION__ << endl;
__int len = strlen(s.data);
__if (len) {
____data = new char[len+1];
____strncpy(data, s.data, len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____
*data = '\0';
__}
}
String& String::operator=(const String& rhs) {
__cout << __PRETTY_FUNCTION__ << endl;
__delete [] data; // 古いデータを削除
__
int len = strlen(rhs.data);
__if (len) {
____data = new char[len+1];
____strncpy(data, rhs.data, len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____
*data = '\0';
__}
__return *this;
}

.... (snip) ....

□ コピーコンストラクタ・代入演算子の使用を禁止
..... (snip) .....
class String {
public:
__String();
__String(const char* value);
__~String();
__const char* const ptr() const { return data; }
private:
__String(const String& s);
__const String& operator=(const String& s);
__char* data;
};
..... (snip) .....
void function(String& a) {
__String b("World");
__a = b; // Compile Error!
}
..... (snip) .....

■ 補足

参照カウンタ機構を用いることで、よりエレガントにコピーコンストラクタや代入演算子の問題を解決することができる。参照カウンタの詳細は、『More EffectiveC++』を参照すること
□ 12項 □ コンストラクタでは代入よりも初期化を使う
■ 課題

以下のコードでは、Stringクラス(11項で用いたものを利用)のコンストラクタ2回と、コピーコンストラクタ1回のコンストラクタが計3回走り非効率。

class Customer {
public:
__Customer(const char* name);
__~Customer() {}
private:
__String name_;
};

Customer::Customer(const char* n) {
__name_ = String(n);
}

int main() {
__Customer user("yohta");
}

■ 解決

コンストラクタでは、代入よりも初期化を使う

以下のようにすれば、コンストラクタは一回のみ。

.... (snip) .....
Customer::Customer(const char* n) : name_(n) { }
.... (snip) .....

■ 補足

クラスのstaticメンバをコンストラクタで初期化しても意味はない。
□ 13項 □ 初期化リストのメンバは、宣言順に並べる
■ 課題

コンストラクタの初期化時にメンバの値の大小に影響するような場合は初期化の順番に注意しないと潜在的な問題をはらむ

以下、問題をとなるコード例。

#include <iostream>
#include <vector>
using namespace std;

class Array {
public:
__Array(int lb, int hb);
__~Array() { }
__size_t getSize() const { return data.size(); }
private:
__vector<int> data;
__size_t size;
__int lBound, hBound;
};

Array::Array(int lb, int hb):
__size(hb-lb+1),
__lBound(lb),
__hBound(hb),
__data(size) {}

int main() {
__Array a(3, 10);
__cout << a.getSize() << endl;
}

■ 解決

初期化リストのメンバは宣言した順番に並べること

上記プログラムは以下の順番にメンバを並べることで解決する。

..... (snip) .....
class Array {
public:
__Array(int lb, int hb);
__~Array() { }
__size_t getSize() const { return data.size(); }
private:
__size_t size;
__int lBound, hBound;
__vector<int> data;
};
.... (snip) .....

■ 補足

・ デストラクタでは、コンストラクタとは逆の順番で呼び出される。
・ このルールにしたがって初期化されるのは、static でないデータメンバーに限られる。
□ 14項 □ 基底クラスには仮想デストラクタを持たせる
■ 課題

仮想デストラクタのない基底クラスを継承した派生クラスを、基底クラスにキャストして delete した場合の挙動は、C++の標準規定上その動作は不定となる。

以下例。

#include <iostream>
#include <new>
using namespace std;

class Target {
public:
__Target() { ++numTargets; }
__~Target() { --numTargets; }
__static size_t numOfTargets() { return numTargets; }
private:
__static size_t numTargets;
};

class Tank : public Target {
public:
__Tank() { ++numTanks; }
__~Tank() { --numTanks; }
__static size_t numOfTanks() { return numTanks; }
private:
__static size_t numTanks;
};

size_t Target::numTargets;
size_t Tank::numTanks;

int main() {

__Tank* tankPtr1 = new Tank();

__
cout << "targets " << tankPtr1->numOfTargets() << endl;
__cout << "tanks " << tankPtr1->numOfTanks() << endl;

__
Target* targetPtr1 = (Target*)tankPtr1;
__delete targetPtr1;

__Tank* tankPtr2 = new Tank();

__
cout << "targets " << tankPtr2->numOfTargets() << endl;
__cout << "tanks " << tankPtr2->numOfTanks() << endl;

__
Target* targetPtr2 = (Target*)tankPtr2;
__delete targetPtr2;

__Tank* tankPtr3 = new Tank();

__
cout << "targets " << tankPtr3->numOfTargets() << endl;
__cout << "tanks " << tankPtr3->numOfTanks() << endl;

__
Target* targetPtr3 = (Target*)tankPtr3;
__delete targetPtr3;
}

■ 解決

継承される可能性のある基底クラスを定義するときは、必ず仮想デストラクタをもたせよう!

以下のように修正すれば問題なし。

.... (snip) ....
class Target {
public:
__Target() { ++numTargets; }
__virtuial ~Target();
__static size_t numOfTargets() { return numTargets; }
private:
__static size_t numTargets;
};

class Tank : public Target {
public:
__Tank() { ++numTanks; }
__virtual ~Tank();
__static size_t numOfTanks() { return numTanks; }
private:
__static size_t numTanks;
};
.... (snip) ....
Target::~Target() { --numTargets; }
Tank::~Tank() { --numTanks; }

.... (snip) ....

■ 補足

・ クラスに少なくとも1個の仮想関数が含まれているときは、仮想デストラクタを宣言すること

・ 仮想デストラクタをインライン宣言すると、関数呼び出しのオーバーヘッドを防ぐことはできるが、コンパイラはその関数の非インライン版も生成するため、オブジェクトサイズが大きくなってしまうことに注意!(一般的に仮想関数をインライン宣言してはいけない)

・ 仮想関数は、仮想関数テーブルに関数ポインタを保持するため、仮想関数の定義分サイズが増えることに注意。仮想関数を増やせば増やしただけ、オブジェクトサイズが増大する

以下例。

#include <iostream>
using namespace std;

class ClassA {
public:
__ClassA() : value(0) {}
__~ClassA() {}
__char get() { return value; }
private:
__char value;
};

class ClassB {
public:
__ClassB() : value(0) {}
__virtual ~ClassB() {}
__virtual char get();
private:
__char value;
};

char ClassB::get() { return value; }

int main() {
__cout << "ClassA: " << sizeof(ClassA) << endl;
__cout << "ClassB: " << sizeof(ClassB) << endl;
}
□ 15項 □ operator= を書くときは、*this へのリファレンスを返そう
■ 課題

operator = を定義するときに、x = y = z = "Hello" として使われるようなケースを考慮しよう。

例)
x = y = z = "Hello"; は次のような擬似コードで表現できる。
x.operator=(y.operator=(z.operator=("Hello")));


■ 解決

operator = の戻り値には、必ず *this を返す。11項を引用。詳細は、『EffectiveC++』を参照。

例)
String& String::operator=(const String& s) {
__int len = strlen(s.data);
__if (len) {
____data = new char[len+1];
____strncpy(data, s.data, len);
____*(data + len) = '\0';
__} else {
____*data = '\0';
__}
__return *this;
}

■ 補足

戻り値を const String& String::operator=(const String& s) と定義することがあるが、これは間違い。以下のような構文をコンパイルすることができなくなる。

例)
String x, y, z;
(x = y) = z;
□ 16項 □ operator= では、すべてのデータメンバに代入しよう
■ 課題

派生クラスで、operator = を定義するとき、基底クラスの値も代入する ことを考慮しなければならない。

以下例では正しく基底クラスの値をコピーできない。

#include <iostream>
using namespace std;

class Base {
public:
__Base(int n = 0): x(n) {}
__virtual ~Base() {}
__int getx() { return x; }
private:
__int x;
};

class Derrived: public Base {
public:
__Derrived(int v1 = 0, int v2 = 0): Base(v1), y(v2) {}
__Derrived& operator=(const Derrived& rhs);
__virtual ~Derrived() {}
__int gety() { return y; }
private:
__int y;
};

Derrived& Derrived::operator=(const Derrived& rhs) {
__if (this == &rhs) return *this;
__y = rhs.y;
__return *this;
}


int main() {
__Derrived a(10, 20);
__Derrived b;
__b = a;
__cout << a.getx() << ":" << a.gety() << endl;
__cout << b.getx() << ":" << b.gety() << endl;
}

■ 解決

派生クラスで、operator = を定義するときは、Baseクラスのoperator = も呼び出すこと。

.... (snip) ....
Derrived& Derrived::operator=(const Derrived& rhs) {
__if (this == &rhs) return *this;
__Base::operator=(rhs); // デフォルトの代入演算子を呼び出し
__y = rhs.y;
__return *this;
}
.... (snip) ....

■ 補足

コピーコンストラクタを定義するときも、必ず基本クラスのコピーコンストラクタを呼び出すこと。ポインタメンバがある場合は、11項に従い、その扱いには十分注意すること。

例)
Drrived(const Drrived& d) : Base(d), y(d.y) {}

古いコンパイラでは、Base::operator=(rhs) でエラーになる場合がある。その場合は、static_castで解決できる場合がある。

例)
static_cast<Base&>this = rhs;
□ 17項 □ operator = では自分自身に代入するケースをチェックしよう
■ 課題

11項で定義した String クラスは以下のコードで正しく動作しない。

代入演算子で、代入される値が自分自身であることを考慮していないため、自分自身の data を削除してしまっているため。

.... (snip) ....
String& String::operator=(const String& rhs) {
__delete [] data; // rhs が自分自身だと、自分のdataを削除してしまう!
__int len = strlen(rhs.data);
__if (len) {
____data = new char[len+1];
____strncpy(data, rhs.datam len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____
*data = '\0';
__}
__return *this;
}
.... (snip) ....
int main() {
__String a;
__a = a;
__cout << a.getData() << endl;
}

■ 解決

operator = では、自分自身が代入されていないかチェックすること

.... (snip) ....
String& String::operator=(const String& rhs) {
__if (this == &rhs) return *this;
__delete [] data; // 古いデータを削除
__int len = strlen(rhs.data);
__if (len) {
____data = new char[len+1];
____strncpy(data, rhs.data, len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____*data = '\0';
__}
__return *this;
}
.... (snip) ....

■ 補足

何をもって等しい(自分自身である)かは、実装者によって決められる。たとえば、実装者によっては、String a("Hello") とString b("Hello")は同一であると解釈するかも知れない。その場合は、自分自身の解釈を以下のように変える必要がある。

例) if (strcmp(data, rhs.data) == 0) return *this;

このような定義をした場合、rhs で示される値がString から派生したクラスだった場合、派生クラスの情報は失われることに注意。

Copyright(C) 2005 Yoshinori Oota All rights reserved.

本ホームページのリンクは自由です。複製、転載される場合は、必ず著者までご連絡をください。