土曜日, 2月 23, 2008

C++解説13(クラスについて4)

1. コピーコンストラクタ

クラスの勉強をする時にきちっと勉強をしないとクラスの文法、継承、コンストラクタ、デストラクタというところまでは
共通して行われるようですが、コピーコンストラクタが抜ける勉強をする人を多く見かけたりします。
何でこのような事態が発生するのか疑問に思い、本屋に駆け込んだところ本によってはコピーコンストラクタを
クラスとは離れたところで説明している本、又はサイトを多く見かけました。これは結構頂けない事だと思います。
C++を勉強している多くの人はC言語を勉強してから始める傾向にあるようです。
そのため、クラスを覚えるとクラスを実際に使い始め関数に渡す事をしたり関数からクラスポインタを戻り値として受け取ったりといろいろな事をやり始めます。
しかし、そこにはコピーコンストラクタという技術が沢山使われておりしらない箇所で危険な処理が動いています。

ここでは、そのような事を防ぐためにコピーコンストラクタの存在についてと実際にどのような時に動く事になるのかなどを検証していきます。

--------------------------------------------------------------------------------
#include "iostream"

class Sample {
public:
Sample() { std::cout << "sample::sample()\n"; }
void View() { std::cout << "sample::View()\n"; }
~Sample() { std::cout << "sample::~sample()\n"; }
};

void func(Sample inObj) {
inObj.View();
}

int main(void) {
Sample obj;

func(obj);

return 0;
}
--------------------------------------------------------------------------------
実行結果
--------------------------------------------------------------------------------
sample::sample()
sample::View()
sample::~sample()
sample::~sample()
--------------------------------------------------------------------------------

デストラクタが2回動いている事が分かると思います。これは、C言語を勉強している人であれば問題ない事実でしょう。
関数func()が終了した時に引数のクラスはスコープ外となりデストラクタが動いているのです。
「!?だったらコンストラクタは何処で動いているの!?」

間違いなく、func()が動く前にコンストラクタが動いています。
ここで動くのがコピーコンストラクタという事になります。

コピーコンストラクタの文法です。
class Sample {
public:
Sample(); // コンストラクタ
Sample(const Sample&); // コピーコンストラクタ
};

基本的にコンストラクタと同じです。
クラスの参照を引数として受け取るコンストラクタという事になります。
これを名にコピーコンストラクタと呼んでいます。
--------------------------------------------------------------------------------
#include "iostream"

class Sample {
public:
Sample() {
std::cout << "(" << this << ")sample::sample()\n";
}
Sample(const Sample& obj) {
std::cout << "(" << this << ")sample::sample(cosnt Sample& obj)\n";
}
void View() {
std::cout << "(" << this << ")sample::View()\n";
}
~Sample() {
std::cout << "(" << this << ")sample::~sample()\n";
}
};

void func(Sample inObj) {
inObj.View();
}

int main(void) {
Sample obj;

func(obj);

return 0;
}
--------------------------------------------------------------------------------
実行結果
--------------------------------------------------------------------------------
(0012FF84)sample::sample() // オリジナルのコンストラクタ
(0012FF54)sample::sample(cosnt Sample& obj) // コピーコンストラクタ
(0012FF54)sample::View() // コピーのView
(0012FF54)sample::~sample() // コピーのデストラクタ
(0012FF84)sample::~sample() // オリジナルのデストラクタ
--------------------------------------------------------------------------------

という事になります。
じゃあ、何でコピーコンストラクタは重要なのでしょうか?
「別にデストラクタが2回動く事ぐらい問題ではないんじゃないの!」
と思ったあなた、C言語から勉強し直して下さい!。
こいつはめちゃくちゃ大きい問題です。
C言語で言うと、mallocしたアドレスを2回freeしている事になります。もう、問題って分かりましたよね。
実際に、コンストラクタでアドレスを確保してデストラクタで開放するプログラムを作成して実験して見ます。
--------------------------------------------------------------------------------
#include "iostream"
#include "cstdlib"
#include "cstring"

class Sample {
private:
char *cp;
public:
// どのオブジェクトか不明のためアドレス情報を出力
Sample() {
std::cout << "(" << this << ")sample::sample()\n";
cp = (char*)malloc(5);
strcpy(cp, "test");
}
void View() {
std::printf("(%p)sample::View()_%p_%s\n", this, cp, cp);
}
~Sample() {
std::cout << "(" << this << ")sample::~sample()\n";
free(cp);
}
};

void func(Sample inObj) {
inObj.View();
}

int main(void) {
Sample obj;

func(obj);
obj.View();

return 0;
}
--------------------------------------------------------------------------------
実行結果
--------------------------------------------------------------------------------
(0012FF88)sample::sample()
(0012FF5C)sample::View()_00913A30_test // cpのアドレスに注目
(0012FF5C)sample::~sample() // ここでフリーされてしまう
(0012FF88)sample::View()_00913A30_<・A // なんじゃこりゃ!
(0012FF88)sample::~sample()
--------------------------------------------------------------------------------

コピーのデストラクタが動いているんも関わらず、オリジナルのアドレスがフリーされてしまいます。
freeされたchar*にアクセスしているため、このような事象が発生してしまいます。
この内容から、コピーコンストラクタが存在しないクラスにおいて
クラスのコピーとは、バイナリーレベルでのコピーが発生している事が推測できます。


そこでコピーコンストラクタによって、同じ内容を格納する新しいアドレスを確保する必要性が出てきます

--------------------------------------------------------------------------------
class Sample {
private:
char *cp;
public:
// どのオブジェクトか不明のためアドレス情報を出力
Sample() {
std::cout << "(" << this << ")sample::sample()\n";
cp = (char*)malloc(5);
strcpy(cp, "test");
}
Sample(const Sample& obj) {
std::cout << "(" << this << ")sample::sample(cosnt Sample& obj)\n";
cp = (char*)malloc(strlen(obj.cp));
strcpy(cp, obj.cp);
}
void View() {
std::printf("(%p)sample::View()_%p_%s\n", this, cp, cp);
}
~Sample() {
std::cout << "(" << this << ")sample::~sample()\n";
free(cp);
}
};
--------------------------------------------------------------------------------
実行結果
--------------------------------------------------------------------------------
(0012FF88)sample::sample()
(0012FF5C)sample::sample(cosnt Sample& obj)
(0012FF5C)sample::View()_00913A40_test // コピー用のアドレスとなっている
(0012FF5C)sample::~sample() // コピーのデストラクタ
(0012FF88)sample::View()_00913A30_test // コピーがfreeされても問題なし
(0012FF88)sample::~sample()
--------------------------------------------------------------------------------

という事になります。
コピーコンストラクタの重要性は理解できたと思います。
他にも、クラス型を返却する時などもコピーコンストラクタが動く事になります。
是非、クラス設計を行う場合はコピーコンストラクタの存在を忘れないようにして下さい。

0 件のコメント: