DATE : 2006/10/10 (Tue)
Java では、次のように定数を書きました。
public Something { public static final int SOME_NUMBER = 10; public static final double SOME_VALUE = 0.1; ... }
C 言語では、主にマクロを使って定義します。
#define SOME_NUMBER 10 #define SOME_VALUE 0.1
C++ でも、マクロを使って定数を宣言できます。しかし、次のように宣言する方法もあります。
const int SOME_NUMBER = 10; const double SOME_VALUE = 0.1; // 宣言されたファイルでのみ使用する場合 static const int SOME_NUMBER = 10; static const double SOME_VALUE = 0.1;
上のコードはクラスの外で定義したものですが、クラス内に宣言することもできます。
class Something { public: static const int SOME_NUMBER; static const double SOME_VALUE; ... }; const int Something::SOME_NUMBER = 10; const double Something::SOME_VALUE = 0.1;
static なメンバ変数(静的メンバ変数)は、インスタンスの数によらず常に1つしかないので、const を付けて定数として扱います。
上のようなクラス内に定義された定数へは、次のような形でアクセスします。
Something::SOME_NUMBER
Something::SOME_VALUE
(;^ω^)Java の場合は、「Something.SOME_NUMBER」「Something.SOME_VALUE」ですね
マクロの代わりにこの方法を用いることで、定数が有効なスコープがはっきりします。
ちなみに、静的メンバ関数を使って定数を初期化することもできます。
class Something { public: static const int SOME_NUMBER; static int getNumber() { ... } }; const int Something::SOME_NUMBER = Something::getNumber();
しかし、これまでの方法では次のような場合に問題が発生します。
class Something { public: static const int SOME_NUMBER; static const double SOME_VALUE; double values[SOME_NUMBER]; ... }; const int Something::SOME_NUMBER = 10; const double Something::SOME_VALUE = 0.1;
つまり、SOME_NUMBER が値の定義(初期化)前に使用されるような場合です。
この問題は、初期化を定数の宣言と同時に行うことで解消できます。
class Something { public: static const int SOME_NUMBER = 10; static const double SOME_VALUE; double values[SOME_NUMBER]; ... }; const double Something::SOME_VALUE = 0.1;
一見すると、double の値も同様に初期化できるように思えます。しかし、定数の宣言と同時に初期化ができるのは、整数値に限られています(少なくとも、Visual C++ 2005 Express Edition では、整数値以外の定数で宣言と同時に初期化しようとすると、『スタティック const 整数データ メンバ以外をクラス内で初期化することはできません』というコンパイルエラーが発生します)。また、宣言と同時に初期化する方法では、静的メンバ関数を使った値の定義はできません。
参考文献
- C++(言語解説) const メンバ 下の方には、古いコンパイラ用のコードが載っています。
DATE : 2006/10/06 (Fri)
C 言語では、ヌルポインタは次のように定義されています。
#define NULL ((void *) 0)
それに対し、C++ では次のように定義されています。
#define NULL 0
C 言語では void 型のポインタだった NULL が、C++ ではただの整数値になっています(コンパイラによって、int 型や long 型に定義されています)。
これは、C++ の型チェックが C 言語と比べて厳しくなったためです。C 言語式では void 型のポインタに型を決定していますが、C++ 式では型を決定付けていません。つまり、C++式の場合は、NULL 値がそのポインタにあった型に自動的に変換されることになります。
(;^ω^)ちなみに、マシンによってはヌルポインタが0番地を指さないものもあります。ただし C や C++ のコンパイラは、ポインタ変数を0で初期化していたり代入、比較しようとしていると、0を自動的にそのマシンに応じたヌルポインタの値に変換してくれます。
しかし、C++ 式では特定の場合に問題が発生します。
次のような関数を考えてみます。
void function(Object* obj) { ... } void function(int i) { ... }
そこで、次のような呼び出しを考えます。
function(NULL);
Object* を引数にとる関数か、int を引数に取る関数のどちらが呼ばれるかは、コンパイラに依存します。そのため、上のようにポインタと int 型とでオーバーロードするのは推奨されていません。
なお、上の関数を正しく呼び出すには、それぞれの引数の型に NULL をキャストします。
このように、C++ の NULL は特別な値ではなくただの整数値であるために、誤解を招きやすい形になっています。そのため、「NULL」を使わずに「0」とそのまま書くことが推奨されています。
(;^ω^)特に、C 言語 → C++ と勉強してくると勘違いしやすいところですね
ちなみに、将来の C++ の仕様では、ヌルポインタを表す予約語が定義されるそうです。
参考文献
(;^ω^)勉強日記の最後にまとめて書こうかと思ったのですが、かなり多くなりそうなので、それぞれのページに付けていこうと思います。
- プログラミングの禁じ手Web版 C++編 - 理念の誤解に関するパターン「C++はC言語の完全な上位互換性があると思い込む」
- C++言語解説 テキストNo.1-5-3 ~ポインタのダークサイド~
- Bjarne Stroustrup's C++ Style and Technique FAQ "Should I use NULL or 0?"
- よりよいC : 空ポインタ定数
- C 言語 FAQ 日本語版「5章 ヌルポインター」
- C++ Topic 「NULLより 0 の方がいい」
- ひげぽん OSとか作っちゃうかMona- [Effective C++][C++] ポインタと数値型とにオーバーロードするのは避けよう 25項
- NULL について
DATE : 2006/10/04 (Wed)
C++ にあって C 言語(C99よりも前のもの)にはないものとして、bool 型の存在が挙げられます。
bool 型とは、真偽値を表す型で、true と false の2値からなります。
(;^ω^)Java で言う boolean 型ですね
ただし、true と false はそれぞれ int 型に変換できます。具体的には、true が 1、false が0となります。
(;^ω^)Java では boolean 型は他の型に変換できませんが、C 言語の条件分岐は 0 が偽、0以外が真という扱いでしたから、その名残なのでしょう
例えば、次のように bool 値を表示するとします。
cout << true << endl; cout << false << endl;
すると、次のような結果になります。
1 0
このように bool 値そのものが int 値と同じように扱えるため、条件分岐の際には bool 値をそのまま使います。
bool isValid = check(); if (isValid) { // OK } else { // ERROR }
(;^ω^)ここらへんは、Java と同じような書き方になりますね
ちなみに、false は int 値の 0 と等しいため、NULL ポインタも false と等しくなります。
すると、NULL チェックは次のように書けます。
// 処理に成功した場合は生成した Object を、 // 処理に失敗した場合は NULL ポインタを返す関数 Object* createObject() { ... } int main() { Object* object = createObject(); // NULL チェック if (object) { // OK } else { // ERROR } }
0(NULL ポインタ)以外は(当然のことながら)非0なので、NULL ポインタでなければ上の if 文は成立します。
DATE : 2006/10/02 (Mon)
(;´Д`)そういえば、配列については全然触れていませんでした。そこで、この記事で簡単に触れたいと思います。
Java では、配列は次のように生成します。
int[] numbers = new int[3];
配列の要素数は、変数や定数で指定することもできます。
int[] numbers = new int[count];
int[] numbers = new int[COUNT];
C++ の場合は、これまでにオブジェクトを生成してきた場合と同様に、静的に宣言する方法と動的に生成する方法があります。
静的に宣言するには、次のようになります。
int numbers[3];
ただし、静的に宣言する場合、変数を要素数の指定に使うことはできません。静的な宣言の場合に使えるのは、整数値と定数のみです。
ちなみに、定数は次のように宣言します。
static const int COUNT = 3;
(;^ω^)Java の static final と似ていますね。
初期化も同時に行うには、次の通りです。
int numbers[3] = {1, 2, 3};
また、動的に生成するには、次のようにします。
int* numbers = new int[3];
この場合は、変数も要素数の指定に使えます。
初期化も同時に行うには、次の通りです。
int* numbers = new int(1, 2, 3);
なお、動的に生成した配列を解放するには、次のようにします。
delete [] numbers;
delete の後に「[]」を付けるところがミソです。「[]」を付けない場合、配列全体のメモリは解放されますが、numbers が直接指すオブジェクト(numbers[0])のデストラクタしか呼ばれません。すると、numbers[1]~[9]のデストラクタが呼ばれず、メモリリークを起こす可能性があります。
ちなみに、Java の配列と C++ と配列には大きな違いが2つあります。
- C++ の配列は要素数を保持しない。
- C++ の配列の要素は未初期化状態。
Java では、次のようにすることで配列の要素数を取得できました。
numbers.length
しかし、C++ の配列そのものから要素数を取得する方法はありません。
また、Java の配列は、生成すると全要素が自動的に初期化されます。例えば、int 型の配列の場合は、生成時に全要素が0に初期化されます。
しかし、C++ の配列は、静的な宣言、動的な生成ともに要素が初期化されていません。そのため、配列を初期化する処理が必要になります。
(;^ω^)ここらへんは実に C らしいですね
ちなみに、C++ の標準ライブラリには vector というクラスがあります。このクラスは、要素数が動的に変わる配列を実現したもので、要素数を取得できたり、要素を追加・削除と言った処理も簡単に行えるようになります。
(;^ω^)そういえば、Java にも java.util.Vector というクラスがありました。
DATE : 2006/10/01 (Sun)
前回の記事の最後に、点オブジェクトを表すインタフェースとしてのクラスを作成しました。
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; };
このインタフェースを実装して、点そのものを表すクラスを考えます。
class PlainPoint : public Point { public : PlainPoint(); PlainPoint(const int& x, const int& y); Point* clone() const; int getX() const; int getY() const; string toString() const; private : int x; int y; }; PlainPoint::PlainPoint() : x(0), y(0) { } PlainPoint::PlainPoint(const int& x, const int& y) : x(x), y(y) { } Point* PlainPoint::clone() const { return new PlainPoint(this->x, this->y); } int PlainPoint::getX() const { return this->x; } int PlainPoint::getY() const { return this->y; } string PlainPoint::toString() const { ostringstream stream; stream << (this->x) << ", " << (this->y); return stream.str(); }
(;^ω^)コンストラクタの引数を参照にしたり、文字列を生成したりする部分以外はほとんど前の Point クラスと同じです。
例えば、次のように PlainPoint オブジェクトの文字列表現を出力してみます。
PlainPoint point(2, 3); cout << point.toString() << endl;
すると、次のような結果になります。
2, 3
ここで、次の部分に注目してください。
class PlainPoint : public Point { public : PlainPoint(); PlainPoint(const int& x, const int& y); Point* clone() const; int getX() const; int getY() const; string toString() const;
クラスの宣言に「: public 基底クラス」とすることで、基底クラスに指定したクラスを継承することができます。ちなみに、「public」とは、基底クラスの public メンバを派生クラスでもそのまま public にするという意味です。ここが「protected」だと基底クラスの public メンバは protected になりますし、「private」の場合は基底クラスのメンバ全てが private になります(public を省略した場合は、private になります)
続いて、PlainPoint クラスのコンストラクタ、コピーコンストラクタと続いて、この PlainPoint クラスで実装するメンバを並べています。virtual 修飾子は付いていませんが、基底クラスで(純粋)仮想関数であると宣言されているので、virtual 修飾子は必要ありません(ただし、省略可能であってもあえて virtual 修飾子を書く流派もあるようです)。
後は、普通にクラスを定義する場合と同じです。
続いて、修飾を施した点を表すクラスを考えてみます。
しかし、一口に「修飾」と言っても、修飾の種類は数多くあります。今回は文字列表現をいじるだけですので、カッコや名前を付けたりといった程度しかありませんが、実際に描画する際にはさらに多くの修飾が考えられます。例えば、周辺に及ぼす効果や色などが挙げられます。
ということで、まず修飾を施した点に共通した機能を抽出して抽象クラスを作ってみたいと思います。修飾を施した点というのは、修飾対象の点オブジェクトを保持していると考えられます。そこで、点オブジェクトを保持する DecoratedPoint クラスを考えます。
class DecoratedPoint : public Point { public: DecoratedPoint(Point *point); DecoratedPoint(const DecoratedPoint& source); virtual ~DecoratedPoint(); DecoratedPoint& operator=(const DecoratedPoint& source); Point* clone() const; int getX() const; int getY() const; string toString() const; private: Point* point; }; DecoratedPoint::DecoratedPoint(Point* point) : point(point) { } DecoratedPoint::DecoratedPoint(const DecoratedPoint& source) : point(source.point->clone()) { } DecoratedPoint& DecoratedPoint::operator=(const DecoratedPoint& source) { if (&source == this) { return *this; } delete this->point; this->point = source.point->clone(); return *this; } DecoratedPoint::~DecoratedPoint() { delete this->point; } Point* DecoratedPoint::clone() const { return this->point->clone(); } int DecoratedPoint::getX() const { return this->point->getX(); } int DecoratedPoint::getY() const { return this->point->getY(); } string DecoratedPoint::toString() const { return this->point->toString(); }
(;^ω^)やっていることは単純で、コンストラクタで受け取った Point オブジェクトに Point クラスとしての処理を横流しにしているだけです。
続いて、点に名前を付けるクラスを考えてみます。
(;^ω^)ただし、簡単にするため名前を変更する機能はありません。
class NamedPoint : public DecoratedPoint { public: NamedPoint(const string& name, Point* point); NamedPoint(const NamedPoint& source); NamedPoint& operator=(const NamedPoint& source); Point* clone() const; string toString() const; private: string name; }; NamedPoint::NamedPoint(const string& name, Point* point) : DecoratedPoint(point), name(name) { } NamedPoint::NamedPoint(const NamedPoint& source) : DecoratedPoint(source), name(source.name) { } NamedPoint& NamedPoint::operator=(const NamedPoint& source) { if (&source == this) { return *this; } DecoratedPoint::operator=(source); this->name = source.name; return *this; } Point* NamedPoint::clone() const { return new NamedPoint(this->name, DecoratedPoint::clone()); } string NamedPoint::toString() const { return this->name + DecoratedPoint::toString(); }
ここで、次の部分に注目してください。
NamedPoint::NamedPoint(const NamedPoint& source) : DecoratedPoint(source), name(source.name) { }
基底クラスのコンストラクタを初期化宣言で呼び出しています。このようにすると、基底クラスのコンストラクタを呼び出すことができます。
また、次の部分にも注目してください。
Point* NamedPoint::clone() const { return new NamedPoint(this->name, DecoratedPoint::clone()); }
「DecoratedPoint::clone()」とすることで、基底クラス DecoratedPoint の clone 関数を呼び出しています。このように、基底クラス名を指定すると、派生クラスからそのクラスの関数を呼び出すことができます。
(;^ω^)Java の super と少し似ていますね。
(;^ω^)ちなみに、上のように clone 関数を使わずに、保持している Point オブジェクトを返す関数を DecoratedPoint クラスに定義するという方法もあります。しかし、そのようにすると、修飾を行うクラスが修飾対象のオブジェクトに過剰に干渉できてしまうので、あえて保持している Point オブジェクトにアクセスできないようにしました。
最後に、カッコ付けを行うクラスを考えてみます。
class BracketedPoint : public DecoratedPoint { public: BracketedPoint(const string& left, const string& right, Point* point); BracketedPoint(const BracketedPoint& source); BracketedPoint& operator=(const BracketedPoint& source); Point* clone() const; string toString() const; private: string left; string right; }; BracketedPoint::BracketedPoint(const string& left, const string& right, Point* point) : DecoratedPoint(point), left(left), right(right) { } BracketedPoint::BracketedPoint(const BracketedPoint& source) : DecoratedPoint(source), left(source.left), right(source.right) { } BracketedPoint& BracketedPoint::operator=(const BracketedPoint& source) { if (&source == this) { return *this; } DecoratedPoint::operator=(source); this->left = source.left; this->right = source.right; return *this; } Point* BracketedPoint::clone() const { return new BracketedPoint( this->left, this->right, DecoratedPoint::clone()); } string BracketedPoint::toString() const { return this->left + DecoratedPoint::toString() + this->right; }
(;^ω^)このクラスで特に取り上げることはありません。
以上のクラスを使って、様々に修飾を施した、点の文字列表現を生成してみます。
次の例は、「name0, 0」となります。
NamedPoint point("name", new PlainPoint); cout << point.toString() << endl;
次の例は、「name(2, 3)」となります。
BracketedPoint point("(", ")", new PlainPoint(2, 3)); NamedPoint namedPoint("name", point.clone()); cout << namedPoint.toString() << endl;
次の例は、「[name(5, 6)]」となります。
BracketedPoint point("(", ")", new PlainPoint(5, 6)); NamedPoint namedPoint("name", point.clone()); BracketedPoint bracketedPoint("[", "]", namedPoint.clone()); cout << bracketedPoint.toString() << endl;
ちなみに、これらは全て Point クラスとして扱えるので、Line クラスに渡して使うことも可能です。例えば、以下の例では「[name(5, 6)] - 2, 3」となります。
BracketedPoint point1("(", ")", new PlainPoint(5, 6)); NamedPoint namedPoint("name", point1.clone()); BracketedPoint bracketedPoint("[", "]", namedPoint.clone()); PlainPoint point2(2, 3); Line line(bracketedPoint.clone(), point2.clone()); cout << line.toString() << endl;
ちなみに、Line クラスも今回と同様の方法で拡張していくことができます。