忍者ブログ
[42] [43] [44] [45] [46] [47] [48] [49] [50] [51] [52]

DATE : 2025/01/19 (Sun)
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。


DATE : 2006/09/27 (Wed)

コピーコンストラクタを用意したり代入演算子のオーバーロードを行うことで、ポインタをメンバに持つクラスでも安全にオブジェクトをコピーしたり代入したりできるようになります。

しかし、場合によってはコピーや代入を禁止したい場合もあります。例えば、メンバの依存関係が複雑で安全にコピー、代入ができない場合などが挙げられます。「○○クラスはコピー、代入禁止」と約束事を決めておいても良いのですが、忘れたり見過ごしてしまえば即バクを作りこんでしまうことになります。

そこで、前述のようなオブジェクトがコピーや代入されている文が見つかった場合は、コンパイラがエラーを出すようにします。

具体的には、コピーコンストラクタや代入演算子のオーバーロードを private として宣言します。private として宣言すると、他のクラスからコピーコンストラクタや代入演算子を使えなくなるので、使おうとするとコンパイル時にエラーとなります。

例えば、Line オブジェクトのコピー、代入を禁止するには、次のようにします。

class Line {
private :
	const Point *start;
	const Point *end;
	Line(const Line& line);
	Line& operator=(const Line& line);

public :
	Line(const Point* start, const Point* end);
	~Line();
	const Point& getStart() const;
	const Point& getEnd() const;
};

Line::Line(const Point* start, const Point* end) :
	start(start), end(end) { }

Line::~Line() {
	delete this->start;
	delete this->end;
}

const Point& Line::getStart() const {
	return *(this->start);
}

const Point& Line::getEnd() const {
	return *(this->end);
}

private に移したコピーコンストラクタ、代入演算子のオーバーロードは宣言だけで、実装の方は消去しました。コピー、代入を禁止したので、実装を書く必要がなくなったためです。

上の例では今回の記事用にコピー、代入を禁止しましたが、実際にオブジェクトのコピー、代入を禁止すると、そのオブジェクトの使い勝手が悪くなりがちです。そこで、なるべくはコピーコンストラクタや代入演算子のオーバーロードを用意して、止むを得ない場合に限ってコピー、代入を禁止した方が良いかもしれません。

(ちなみに、フレンドクラスという種類のクラスを使うと、private で宣言されたメンバにもアクセスできます。すると、実装もあった方が良いと思えるかもしれません。しかし、安全にコピー、代入できない理由からコピーコンストラクタや代入演算子のオーバーロードを private 宣言した場合は、実装を用意しても意味がありません)

PR

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 文で自分のメンバ変数のオブジェクトを破棄してしまいます。そのため、代入元が自分のオブジェクトでないかどうかは必ずチェックしなければいけません。

次の部分は、自分のメンバ変数のオブジェクトを破棄しています。代入元が持つポインタで自分の持つポインタを上書きするわけですから、上書きされる前に自分の持つポインタが指すオブジェクトは破棄しておかなければいけません。

そして最後に代入元のポインタの指すオブジェクトを複製します。複製が終われば、自分自身への参照を返して、オーバーロードされた動作は終了です。

「コピーコンストラクタ」と比べると少々面倒なところがあるので、処理の順序を以下にまとめておきます。

  1. 代入元(引数、右辺)が代入先(自分自身)(this、左辺)と同じであれば自分自身への参照(*this)を返す。
  2. 代入元によって上書きされるポインタ変数が指すオブジェクトを破棄する。
  3. 代入元の持つポインタ変数が指すオブジェクトを複製する。
  4. 自分自身への参照(*this)を返す。

すると、ポインタ変数をメンバに持つクラスは、

  • コピーコンストラクタ
  • 代入演算子のオーバーロード

を持たなければならないことがわかります。


DATE : 2006/09/24 (Sun)
Line line(new Point, new Point(1, 1));

Line line2 = line;

上のように変数 line2 を変数 line のオブジェクトで初期化したとき、line のオブジェクトが line2 に「コピー」されます。また、値渡しで関数にオブジェクトを渡す際や返す際にもオブジェクトは「コピー」されます。

Line line(new Point, new Point(1, 1));
Line line2(new Point(1, 1), new Point(2, 2));

line2 = line;

ちなみに、上のように宣言済みの変数 line2 に line のオブジェクトを代入する場合は、コピーではなく「代入」と呼ばれます。

オブジェクトのコピーや代入が行われると、コピー元のメンバ変数の値がコピー先のメンバ変数にコピーされます。

しかし、メンバ変数にポインタ変数がある場合には問題が発生します。例えば、次の Line クラスを考えてみます。

class Line {
private :
	const Point *start;
	const Point *end;

public :
	Line(const Point* start, const Point* end);
	~Line();
	const Point& getStart() const;
	const Point& getEnd() const;
};

Line::Line(const Point* start, const Point* end) :
	start(start), end(end) { }

Line::~Line() {
	delete this->start;
	delete this->end;
}

const Point& Line::getStart() const {
	return *(this->start);
}

const Point& Line::getEnd() const {
	return *(this->end);
}

ここで、次のようなプログラムを考えます。

void function(Line line) {
	// line に関する処理
}

int main() {
	Line line(new Point, new Point(1, 1));

	function(line);

	return 0;
}

関数 function は、Line オブジェクトを値渡しで行って処理する関数です。そこに、Line オブジェクトを渡すのが main 関数なのですが、このプログラムでは実行時にエラーが発生します。

というのも、関数 function に値渡しを行った際にはオブジェクトの「コピー」が行われるからです。コピーが行われると、変数 line のメンバ変数 line が引数 line にコピーされることになります。そのとき、Line オブジェクトの持っている Point オブジェクトへのポインタもコピーされることになります。

すると、変数 line と引数 line は別々の Line オブジェクトにもかかわらず、持っている Point オブジェクトへのポインタは同じ Point オブジェクトを指すことになります。

そして関数 function が終了すると、引数 line の Line オブジェクトは破棄されます。この時 Line クラスのデストラクタが呼ばれ、変数 line と引数 line が共有していた Point オブジェクトをも破棄してしまうことになります。

そこで、Line オブジェクトがコピーされる際には、Point オブジェクトも複製しなければなりません。この「コピー」される際に呼び出されるのが、「コピーコンストラクタ」です。

「コピーコンストラクタ」を追加した Line クラスを次に示します。

class Line {
private :
	const Point *start;
	const Point *end;

public :
	Line(const Point* start, const Point* end);
	Line(const Line& 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) {
	const Point& start = line.getStart();
	const Point& end = line.getEnd();

	this->start = new Point(start.getX(), start.getY());
	this->end = new Point(end.getX(), end.getY());
}

Line::~Line() {
	delete this->start;
	delete this->end;
}

const Point& Line::getStart() const {
	return *(this->start);
}

const Point& Line::getEnd() const {
	return *(this->end);
}

ここで、次の部分に注目してください。

Line(const Line& line);
Line::Line(const Line& line) {
	const Point& start = line.getStart();
	const Point& end = line.getEnd();

	this->start = new Point(start.getX(), start.getY());
	this->end = new Point(end.getX(), end.getY());
}

自分と同じクラスのオブジェクトの const 付き参照を受け取るコンストラクタが「コピーコンストラクタ」です。このコンストラクタが、「コピー」の際に呼び出されます。

(;^ω^)ちなみに、上のコードの場合は、Point クラスの方にコピーコンストラクタを用意して、それを Line クラスから呼び出した方が綺麗かもしれません。例えば次のように、です。

class Point {
private :
	int x;
	int y;

public :
	Point();
	Point(int x, int y);
	Point(const Point& point);
	int getX() const;
	int getY() const;
};

Point::Point() : x(0), y(0) { }

Point::Point(int x, int y) : x(x), y(y) { }

Point::Point(const Point& point) : x(point.getX()), y(point.getY()) { }

int Point::getX() const {
	return this->x;
}

int Point::getY() const {
	return this->y;
}


class Line {
private :
	const Point *start;
	const Point *end;

public :
	Line(const Point* start, const Point* end);
	Line(const Line& 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;
}

const Point& Line::getStart() const {
	return *(this->start);
}

const Point& Line::getEnd() const {
	return *(this->end);
}

(;^ω^)追加・変更した部分は次の通りです。

(Point クラス)

Point(const Point& point);
Point::Point(const Point& point) : x(point.getX()), y(point.getY()) { }

(Line クラス)

Line::Line(const Line& line) :
		start(new Point(line.getStart())),
		end(new Point(line.getEnd())) {
}

オブジェクトの代入については、次の記事に書こうと思います。


DATE : 2006/09/23 (Sat)

Java では、どこからも参照できなくなったオブジェクトはガーベジコレクタが検出してメモリを解放してくれます。C++ でも、以下のように宣言したオブジェクトは、スコープの終了とともに自動的に解放されます。

void func() {
	Point point;

	// 処理
}

しかし、動的に生成したオブジェクトは自分で破棄しなければなりません。

(;^ω^)「スマートポインタ」というものを使うと自動的に解放してくれるそうですが、今回は触れません。

動的に生成したオブジェクトを破棄するには、オブジェクトへのポインタに対して delete 演算子を使います。

Point* point = new Point;

delete point;

C でのメモリの解放と同様に、解放済みのポインタに対して delete を行うと実行時にエラーが発生します。

ところで、動的に確保したオブジェクトがメンバ変数にある場合はどのように解放すれば良いのでしょうか。

ここで、 前回の記事の Point クラスを使って2次元座標上の線分を表す Line クラスを考えてみます。

Java で表現したコードは次の通りです。

public class Line {
	private final Point start;
	private final Point end;

	public Line(Point start, Point end) {
		this.start = start;
		this.end = end;
	}

	public Point getStart() {
		return this.start;
	}

	public Point getEnd() {
		return this.end;
	}
}

(;^ω^)本当はコンストラクタの引数が null でないかどうかをチェックしないといけないのですが、ここでは省略しています。

C++ で表現すると次のようになります。

class Line {
private :
	const Point *start;
	const Point *end;

public :
	Line(const Point* start, const Point* end);
	~Line();
	const Point& getStart() const;
	const Point& getEnd() const;
};

Line::Line(const Point* start, const Point* end) :
	start(start), end(end) { }

Line::~Line() {
	delete this->start;
	delete this->end;
}

const Point& Line::getStart() const {
	return *(this->start);
}

const Point& Line::getEnd() const {
	return *(this->end);
}

ここで、次の部分に注目してください。

~Line();
Line::~Line() {
	delete this->start;
	delete this->end;
}

「~クラス名()」の形のメンバを「デストラクタ」と言い、そのクラスのオブジェクトが解放されるタイミングで呼び出されます。「解放されるタイミング」とは、前述にあるような、「スコープを外れた時」や「delete を行った時」のことです。


DATE : 2006/09/22 (Fri)

2次元座標の点を表す Point クラスを考えてみます。

Java の場合は以下のようなクラスになります。

public class Point {
	private int x;
	private int y;

	public Point() { }

	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public int getX() {
		return this.x;
	}

	public int getY() {
		return this.y;
	}
}

引数なしのコンストラクタの場合は原点 (0, 0) を表し、引数を指定したコンストラクタでは引数に渡された座標を設定します。

このコードを C++ のクラスで表現すると次のようになります。

class Point {
private :
	int x;
	int y;

public :
	Point();
	Point(int x, int y);
	int getX() const;
	int getY() const;
};

Point::Point() : x(0), y(0) { }

Point::Point(int x, int y) : x(x), y(y) { }

int Point::getX() const {
	return this->x;
}

int Point::getY() const {
	return this->y;
}

Java の場合、上の Point オブジェクトを生成するには次のようにします。

Point origin = new Point();
Point point = new Point(1, 2);

C++ の場合は、基本的には次のようになります。

Point origin;
Point point(1, 2);

コンストラクタに渡す引数を指定しながら変数宣言をするイメージです。

ちなみに、Java の場合、origin, point の両変数に入るのは生成されたオブジェクトの参照ですが、上の C++ の場合はオブジェクトそのものになります。

なお、引数なしのコンストラクタを使用する場合、Java 風の書き方だと「Point origin();」と書きがちですが、この場合はコンパイルエラーとなります。

上の方法で生成した場合、次のようにしてクラスの public メンバにアクセスできます。

int originX = origin.getX();
int originY = origin.getY();

ただし、上の方法の場合、必要な時に必要な分だけ(動的に)Point オブジェクトを生成することができません。動的に Point オブジェクトを生成するには、次のようにします。

Point* origin = new Point;
Point* point = new Point(1, 2);

(,,゚Д゚)ポインタが出てきました。

new 演算子を使うと、生成されたオブジェクトのアドレスが返ってきます。

動的に生成したオブジェクトを使う場合は、C 同様アロー演算子(->)を使います。

int originX = origin->getX();
int originY = origin->getY();

ところで、動的に確保したメモリの領域は解放しなければならない C のように、C++ でも動的に生成したオブジェクトは破棄しなければなりません。そこで、次の記事はオブジェクトの破棄について書こうと思います。

忍者ブログ [PR]
ブログ内検索
最近の状況
リンク
カレンダー
12 2025/01 02
S M T W T F S
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
使用許諾
最新コメント
(08/15)
(05/04)
(03/06)
(03/04)
(09/25)
最新トラックバック
ブログ内検索
最近の状況
リンク
カレンダー
12 2025/01 02
S M T W T F S
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
使用許諾
最新コメント
(08/15)
(05/04)
(03/06)
(03/04)
(09/25)
最新トラックバック