DATE : 2006/09/30 (Sat)
Java では、クラスの持つ仕様を定義したインタフェースと呼ばれる型があります。
C++ でインタフェースを実現するには、純粋仮想関数を使用します。純粋仮想関数とは、実装を派生クラスに任せるための関数です。
Java で言うとこれは abstract メソッドに相当します。例えば、次のような abstract クラスがあったとします。
public abstract class AbstractCommand {
private final String name;
public AbstractCommand(String name) {
if (name == null) {
throw new IllegalArgumentException();
}
this.name = name;
}
public abstract void execute();
public String getName() {
return this.name;
}
}
C++ では次のようになります。(インクルード文などは省略しています)
class AbstractCommand {
private:
string name;
public:
AbstractCommand(string& name);
virtual ~AbstractCommand() { }
virtual void execute() = 0;
string getName() const;
};
AbstractCommand::AbstractCommand(string& name) : name(name) {
}
string AbstractCommand::getName() const {
return this->name;
}
ここで、次の部分に注目してください。
virtual void execute() = 0;
virtual と修飾子を付けたメンバ関数の中身を「= 0」とすると、「純粋仮想関数」になります。ちなみに、「= 0」を付けずに処理を定義すると、「仮想関数」になります。例えば、あらかじめ標準的な処理を定めておきたい場合には、次のように仮想関数にします。
class AbstractCommand {
private:
string name;
public:
AbstractCommand(string& name);
virtual ~AbstractCommand() { }
virtual void execute();
string getName() const;
};
AbstractCommand::AbstractCommand(string& name) : name(name) {
}
void AbstractCommand::execute() {
cout << "Command not found." << endl;
}
string AbstractCommand::getName() const {
return this->name;
}
「仮想関数」にすると、その関数を派生クラスでオーバーライドすることができます。言い換えると、「仮想関数」でなければオーバーライドできません。
(;^ω^)Java の場合、メソッドに final 修飾子を付けない限りはサブクラスで自由にオーバーライドできますが、C++ はその逆ですね。
ところで、virtual の付いた次のデストラクタは何でしょうか。
virtual ~AbstractCommand() { }
これは「仮想デストラクタ」と呼ばれています。virtual を付けることで、派生クラスにもデストラクタがあることを示しています。仮想デストラクタがない場合は、呼び出したときの型から上位のクラスにあるデストラクタのみが呼び出されます。例えば、上のコードに仮想デストラクタがなかった場合、次のコードでは AbstractCommand のデストラクタしか呼ばれません。
// ListCommand は AbstractCommand を継承 AbstractCommand* command = new ListCommand(); delete command;
そのため、派生クラスがある場合は必ずと言っても良いほど「仮想デストラクタ」を用意しなければなりません。
ちなみに、純粋仮想関数が1つでもクラスにあると、そのクラスのインスタンスは生成できません。
以上から、インタフェースを実現するには、メンバ変数をなくして、メンバ関数を全て純粋仮想関数にすれば良いことが分かります。
例えば、これまでに出てきた Point クラスをインタフェースにすると、次のようになります。(インクルード文などは省略しています)
class Point {
public:
virtual ~Point() { }
virtual Point* clone() const = 0;
virtual int getX() const = 0;
virtual int getY() const = 0;
virtual string toString() const = 0;
};
次回は、このインタフェースを利用して装飾可能な点オブジェクトを作ってみたいと思います。例えば、前回までの Point オブジェクトの場合、文字列表現は必ず「(0, 0)」という形になっていました。そこで次回は、「0, 0」や「name(0, 0)」、「(name[0, 0])」と生成する Point オブジェクトを考えてみます。
(;^ω^)実際に点を描く処理であれば点の形を変えたりと非常に華やかなのですが、文字列表現をいじるだけにとどめたいと思います。
DATE : 2006/09/29 (Fri)
C++ で文字列を扱うには、標準C++ライブラリの string クラスを使用します。C と同じように char 型の配列としても扱えますが、ここでは string クラスを使った方法を取り上げます。
文字列の静的な宣言
string クラスを使用するには、まず string ヘッダを取り込む必要があります。
#include <string>
string オブジェクトを文字列リテラルで宣言するには、次のようにします。
std::string str = "string";
なお、「string」の前にある「std」は「名前空間」です。string クラスは std 名前空間に属しているので、上のように書きます。Java で言えば、次のようなコードに相当します。
java.lang.String str = "string";
Java の場合、java.lang パッケージは自動的にインポートされるので、上のように java.lang と書く必要はありません。
ただし、C++ の場合でも、あらかじめ使用する名前空間を宣言しておくことで、クラスの名前空間を省略できます。
例えば、ファイル中で std 名前空間を使用するときは次のように書きます。
using namespace std;
これは、Java の場合は次のようなコードに相当します。
import java.lang.*;
ちなみに、名前空間の表記を特定のクラスのみ省略する場合は、次のように書きます。
using std::string;
これは、Java の場合は次のようなコードに相当します。
import java.lang.String;
文字列の動的な生成
Java では、+演算子を使うことで文字列を結合できます。
String str = "str" + "ing";
string オブジェクトも、+演算子で文字列を結合できます。ただし、文字列リテラル同士はアドレス同士の加算となるので、結合できません。そのため、string オブジェクトを作成した上での加算となります。
string s1 = "str"; string s2 = "ing"; string str = s1 + s2;
もしくは、次のようにも書けます。
string str = string("str") + string("ing");
また、文字列ストリームを使う方法もあります。
文字列ストリームを使うには、sstream ヘッダを取り込みます。
#include <sstream>
文字列ストリームを使って文字列を生成するコードを以下に示します。
std::ostringstream stream; stream << "str" << "ing"; std::string str = stream.str();
サンプルコード
ここで、 Point クラス、Line クラス に、オブジェクトの情報を文字列として返す toString メソッドを作ります。
(;^ω^)本当は、実際に点や線を描くメソッドを設けた方がそれらしいのですが、簡単のために文字列にしました。
追加したコードは次の通りです。
#include <string>
#include <sstream>
using std::string;
using std::ostringstream;
class Point {
private :
int x;
int y;
public :
Point();
Point(int x, int y);
Point* clone() const;
int getX() const;
int getY() const;
string toString() const;
};
Point::Point() : x(0), y(0) { }
Point::Point(int x, int y) : x(x), y(y) { }
Point* Point::clone() const {
return new Point(this->x, this->y);
}
int Point::getX() const {
return this->x;
}
int Point::getY() const {
return this->y;
}
string Point::toString() const {
ostringstream stream;
stream << "(" << (this->x) << ", " << (this->y) << ")";
return stream.str();
}
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;
string toString() const;
};
Line::Line(const Point* start, const Point* end) :
start(start), end(end) { }
Line::Line(const Line& line) :
start(line.getStart().clone()),
end(line.getEnd().clone()) {
}
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 = line.getStart().clone();
this->end = line.getEnd().clone();
return *this;
}
const Point& Line::getStart() const {
return *(this->start);
}
const Point& Line::getEnd() const {
return *(this->end);
}
string Line::toString() const {
ostringstream stream;
stream << (this->start->toString()) << " - " <<
(this->end->toString());
return stream.str();
}
変更した部分は、次の通りです。
#include <string> #include <sstream> using std::string; using std::ostringstream;
class Point {
private :
int x;
int y;
public :
Point();
Point(int x, int y);
Point* clone() const;
int getX() const;
int getY() const;
string toString() const;
};
string Point::toString() const {
ostringstream stream;
stream << "(" << (this->x) << ", " << (this->y) << ")";
return stream.str();
}
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;
string toString() const;
};
string Line::toString() const {
ostringstream stream;
stream << (this->start->toString()) << " - " <<
(this->end->toString());
return stream.str();
}
このようにしておくと、次のように Line オブジェクトの情報を toString メソッドを呼び出すだけで取得することができます。
Line line(new Point(0, 0), new Point(10, 10)); std::cout << line.toString() << std::endl;
上のコードを実行すると、次のように表示されます。
(0, 0) - (10, 10)
DATE : 2006/09/28 (Thu)
これから継承の方面へと日記を進めていく予定ですが、これまでのサンプルコードに問題が見つかったのでここで修正しておきます。
これまでのサンプルコードで問題点が見つかったのは、次の部分です。
Line::Line(const Line& line) :
start(new Point(line.getStart())),
end(new Point(line.getEnd())) {
}
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;
}
これから先の記事では Point クラスのサブクラスが出てくる予定です。しかし、上のソースコードを見ると、Line オブジェクトのメンバとして Point オブジェクトを決め打ちで生成しています。このままでは、Point クラスのサブクラスが出てきても Line オブジェクトのコピーや代入を行うと、中身が Point クラスそのものに変化してしまいます。
そこで次のように、Point クラスにコピーコンストラクタの代わりとして clone メソッドを用意して、コンストラクタに依存しない方法で Point オブジェクトのコピーを行うようにします。
class Point {
private :
int x;
int y;
public :
Point();
Point(int x, int y);
Point* clone() const;
int getX() const;
int getY() const;
};
Point::Point() : x(0), y(0) { }
Point::Point(int x, int y) : x(x), y(y) { }
Point* Point::clone() const {
return new Point(this->x, this->y);
}
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();
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(line.getStart().clone()),
end(line.getEnd().clone()) {
}
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 = line.getStart().clone();
this->end = line.getEnd().clone();
return *this;
}
const Point& Line::getStart() const {
return *(this->start);
}
const Point& Line::getEnd() const {
return *(this->end);
}
変更点は以下の通りです。
class Point {
private :
int x;
int y;
public :
Point();
Point(int x, int y);
Point* clone() const;
int getX() const;
int getY() const;
};
Point* Point::clone() const {
return new Point(this->x, this->y);
}
Line::Line(const Line& line) :
start(line.getStart().clone()),
end(line.getEnd().clone()) {
}
Line& Line::operator=(const Line& line) {
if (this == &line) {
return *this;
}
delete this->start;
delete this->end;
this->start = line.getStart().clone();
this->end = line.getEnd().clone();
return *this;
}
( ´∀`)このようにしておけば、いつ Point クラスにサブクラスを作っても、Line クラスは変更しなくていいので楽ですね
(;^ω^)ちなみにもうひとつ、Line クラスのコンストラクタにヌルポインタを渡した時の動作にも問題点があります。ただこの部分は、例外処理に触れた後で解決を図ってみようと思います。
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 宣言した場合は、実装を用意しても意味がありません)
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)を返す。
すると、ポインタ変数をメンバに持つクラスは、
- コピーコンストラクタ
- 代入演算子のオーバーロード
を持たなければならないことがわかります。
