DATE : 2006/09/26 (Tue)
ポインタ変数をメンバに持つクラスは、次のようにオブジェクトの「コピー」や「代入」を行うと、別々のオブジェクト内でメンバのオブジェクトを共有することになってしまいます。
// (引数)オブジェクトの「コピー」 void function1(Point point) { ... } // (戻り値)オブジェクトの「コピー」 Point function2(Point point) { ... }
Point point; // オブジェクトの「コピー」 Point newPoint = point; // オブジェクトの「コピー」 function1(point); // オブジェクトの「代入」 Point samePoint; samePoint = point;
オブジェクトの「コピー」を行う場合には、「コピーコンストラクタ」内でメンバのオブジェクトを複製することで共有を阻止できます。
それでは、「代入」の場合はどのようにメンバのオブジェクトを複製するのでしょうか。
C++ では、各種演算子の動作を定義することができます。このことを、「演算子のオーバーロード」と言います。例えば Java では、以下のように String オブジェクト同士を + 演算子で結合することができます。
String string = "Hello, " + "World.";
Java では演算子の動作を独自に定義することはできませんが、これも演算子のオーバーロードです。
つまり、代入演算子の動作を独自に定義して、その中でポインタメンバのオブジェクトを複製すれば良いわけです。
代入演算子のオーバーロードを加えた Line クラスは、次のようになります。
class Line { private : const Point *start; const Point *end; public : Line(const Point* start, const Point* end); Line(const Line& line); ~Line(); Line& operator=(const Line& line); const Point& getStart() const; const Point& getEnd() const; }; Line::Line(const Point* start, const Point* end) : start(start), end(end) { } Line::Line(const Line& line) : start(new Point(line.getStart())), end(new Point(line.getEnd())) { } Line::~Line() { delete this->start; delete this->end; } Line& Line::operator=(const Line& line) { if (this == &line) { return *this; } delete this->start; delete this->end; this->start = new Point(line.getStart()); this->end = new Point(line.getEnd()); return *this; } const Point& Line::getStart() const { return *(this->start); } const Point& Line::getEnd() const { return *(this->end); }
次の部分に注目してください。
Line& operator=(const Line& line);
Line& Line::operator=(const Line& line) { if (this == &line) { return *this; } delete this->start; delete this->end; this->start = new Point(line.getStart()); this->end = new Point(line.getEnd()); return *this; }
「戻り値& operator=(引数&)」の形のメンバ関数が代入演算子のオーバーロードになります。引数が演算子の右辺、戻り値や this の指すオブジェクトが左辺のオブジェクトになります。通常、右辺のオブジェクトに手は加えないので、引数は const で参照します。例えば、
Line line(new Point, new Point(1, 1)); Line line2(new Point(1, 1), new Point(2, 2)); line2 = line;
の場合は、line の参照が引数、line2 がメンバ関数 operator= 内の this。そして、戻り値が格納されるは右辺の line2 となります。
ちなみに、メンバ関数なので、次のように書くこともできます。
Line line(new Point, new Point(1, 1)); Line line2(new Point(1, 1), new Point(2, 2)); // line2 = line line2.operator=(line);
この形で見ると、line と line2 の関係がよく分かります。(一見すると、operator= の戻り値は意味がないように思えます。しかし、元々 = 演算子は代入された値を返す仕様になっています。例えば、「line3 = line2 = line」と演算子を繋げるには、戻り値がなければできません)。
それでは、オーバーロードした中身について見てみます。
Line& Line::operator=(const Line& line) { if (this == &line) { return *this; } delete this->start; delete this->end; this->start = new Point(line.getStart()); this->end = new Point(line.getEnd()); return *this; }
まず初めに、代入元が自分(代入先)と同じオブジェクトでないかどうかを調べています。参照は変数の別名なので、参照のアドレスを取得すると、参照先の変数のアドレスが取得できます。代入元が同じオブジェクトというのは、次のような場合を言います。
line2 = line2;
つまり、
line2.operator=(line2)
です。この場合、コピーする必要はないので、自分への参照をそのまま返します。ちなみにこの部分がないと、次に出てくる delete 文で自分のメンバ変数のオブジェクトを破棄してしまいます。そのため、代入元が自分のオブジェクトでないかどうかは必ずチェックしなければいけません。
次の部分は、自分のメンバ変数のオブジェクトを破棄しています。代入元が持つポインタで自分の持つポインタを上書きするわけですから、上書きされる前に自分の持つポインタが指すオブジェクトは破棄しておかなければいけません。
そして最後に代入元のポインタの指すオブジェクトを複製します。複製が終われば、自分自身への参照を返して、オーバーロードされた動作は終了です。
「コピーコンストラクタ」と比べると少々面倒なところがあるので、処理の順序を以下にまとめておきます。
- 代入元(引数、右辺)が代入先(自分自身)(this、左辺)と同じであれば自分自身への参照(*this)を返す。
- 代入元によって上書きされるポインタ変数が指すオブジェクトを破棄する。
- 代入元の持つポインタ変数が指すオブジェクトを複製する。
- 自分自身への参照(*this)を返す。
すると、ポインタ変数をメンバに持つクラスは、
- コピーコンストラクタ
- 代入演算子のオーバーロード
を持たなければならないことがわかります。