カスタム検索
|
何が面白いって、C++ のオペレータのオーバーロードほど面白いものはありません。ここでは、次の項目について解説します。
C++ でマニアックといえば、オペレータのオーバーロードは絶対に外せない存在です。オペレータを自然に使いこなせるようにならないと、C++ を C++ らしく使えないといっても過言ではない存在です。では、オペレータとはとても難しいものなのかというと、そうでもありません。オーバーロードされたオペレータとは、端的にいえば、関数名が C++ の演算子であるような、クラスのメンバ関数のことです。
まずオペレータではない、普通の関数の簡単な例をあげてみましょう。
class CManiac {
public:
int Geti() {
return 2;
}
};
上に書いた CManiac という名前のクラスには、int 型の値 2 を返す、Geti という名前のメンバ関数が定義されていますね。このメンバ関数の名前を C++ の演算子である単項のプラス記号 + に置き換えるとします。このとき + は関数名としては許されないので、前に operator というキーワードをつけます。つまり Geti が operator+ に置き換わって、次のようになります。
class CManiac {
public:
// 単項 + 演算子を定義する。
int operator+() {
return 2;
}
};
これで、CManiac のクラスオブジェクトに単項演算子 + をつけると、整数値の 2 が返るようになります。つまり、次のようなプログラムが書けるようになります。
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
int iValue = +maniac; // 単項の+ 演算子を maniac に適用する。
printf("iValue = %d\n", iValue); // 答えは 2 になります。
return 0;
}
上記で、単項の + 演算子をクラスオブジェクト maniac に適用しているのですが、これの効果は、+ という名前の関数の呼び出しになるので、定義に従って 2 が返るということになります。つまり、+maniac という呼び出しは、通常の関数呼び出しの形式で書けば maniac.+() と書いたように解釈されます。実際には、このように書いてもコンパイルできませんが、+ が関数名になっていると思えば形式的には理解できるとおもいます。コンパイルできない理由は、operator キーワードによるメンバ関数定義は、そのメンバ関数が演算子として動作することを意図しているからで、それがまさに行いたいことであって、演算子の動作の定義をメンバ関数として記述できるというところに意味があるのです。
オペレータを定義する側から見ると、ページの冒頭で書いたように、オペレータとは関数名が C++ の演算子であるような、クラスのメンバ関数のことなのですが、使う側から見るとそれは、まさに演算子そのものであるわけです。
どうでしょう。オペレータの感じをつかんでもらえましたか?
なお、オペレータという言葉と演算子という言葉が出てきましたが、以下の解説ではこれらは同じものとして混同して使用します。
上記では、演算子 + をクラス CManiac のメンバ関数として定義しましたが、同じ演算子をグローバル関数で定義する方法もあります。
class CManiac {
friend int operator+(CManiac&); // この例では、この行は、なくても動作します。
};
// CManiac に対する単項演算子 + をグローバル関数として定義する。
int operator+(CManiac& rmaniac) {
return 2;
}
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
int iValue = +maniac; // 単項の+ 演算子を maniac に適用する。
printf("iValue = %d\n", iValue); // 答えは 2 になります。
return 0;
}
グローバル関数として単項の + 演算子を定義する場合には、上記の例のように、第一引数としてクラスオブジェクトの参照を渡すようにします。参照でなく、オブジェクトそのものでも文法的には問題ありません。
上記の例の friend 宣言についてですが、グローバル関数 + の中で rmaniac を使って CManiac の public でない(つまり private か protected な)メンバにアクセスする必要がある場合には、クラス CManiac 側で、+ 演算子を friend 宣言しておく必要があります。上記の例では public でないメンバにアクセスしていないので、friend 宣言はなくても動作します。
なお、注意点ですが、C++ の組み込み型の変数やその参照型/ポインタ型だけを引数とするような演算子をグローバル関数として定義することはできません。例えば加算を表す 2 項演算子である + を、次のようには定義できません。
// 以下は、定義できない。コンパイルエラーになる。
int operator+(int i1, int i2) {
return i1 - i2; // + 演算子で引き算を意図している。もしこのように定義できるとすると恐ろしい事になる。
}
もし、上記のような定義が有効になると、全ての int どうしの加算が、減算になってしまい、恐ろしいことが起こることは想像に難くありません。このようなことを避けるために、基本形だけを受け取る演算子のオーバーロードはできないようになっています。
もう一つ注意点ですが、演算子によっては、グローバル関数としては定義できないものがあります。以下の解説では、そのような場合、それぞれの項の最後に、その旨を記述することにします。
演算子はその種類によって特徴があり、関数定義の方法も少しずつ異なります。以下で、演算子を一つずつ見ていくことにしましょう。
直下の表は、クラスに対して定義できる演算子の種類を示しています。表は上から優先順位の高い演算子が並んでおり、優先順位が低くなる度に背景色を変えてあります。意味をクリックすると、それぞれのサンプルコードとその解説にジャンプします。じっくりお楽しみください。
| 演算子 | 意味 | 結合方向 | 定義の可/不可 |
|---|---|---|---|
| :: | スコープを解決する | -- | × |
| () | 関数呼び出し | 左 | ○ |
| タイプ() | 型変換キャスト | -- | ○ |
| . | クラスのメンバを選択 | 左 | × |
| -> | クラスへのポインタからメンバにアクセス | 左 | ○ |
| [] | 配列の添字参照 | 左 | ○ |
| ++ | 後置きインクリメント | 左 | ○ |
| -- | 後置きデクリメント | 左 | ○ |
| typeid | 型名を返す | -- | × |
| const_cast | const 属性を変更 | -- | × |
| dynamic_cast | 実行時に型チェックしてキャスト | -- | × |
| static_cast | 普通のキャスト | -- | × |
| reinterpret_cast | 無条件キャスト | -- | × |
| new | メモリの確保 | -- | ○ |
| delete | メモリの開放 | -- | ○ |
| new[] | 配列メモリの確保 | -- | ○ |
| delete[] | 配列メモリの開放 | -- | ○ |
| ++ | 前置きインクリメント | 右 | ○ |
| -- | 前置きデクリメント | 右 | ○ |
| ~ | 全ビット反転 | 右 | ○ |
| ! | 論理反転 | 右 | ○ |
| + | 単項プラス | 右 | ○ |
| - | 単項マイナス | 右 | ○ |
| & | アドレスを得る | 右 | ○ |
| * | ポインタから実体を参照 | 右 | ○ |
| sizeof | サイズを得る | 右 | × |
| (タイプ) | 型変換キャスト | 右 | ○ |
| .* | クラスオブジェクトのメンバを選択 | 左 | × |
| ->* | クラスオブジェクトへのポインタからメンバを選択 | 左 | ○ |
| * | 乗算 | 左 | ○ |
| / | 除算 | 左 | ○ |
| % | 剰余 | 左 | ○ |
| + | 加算 | 左 | ○ |
| - | 減算 | 左 | ○ |
| << | 左シフト | 左 | ○ |
| >> | 右シフト | 左 | ○ |
| < | より小さい | 左 | ○ |
| <= | 以下 | 左 | ○ |
| > | より大きい | 左 | ○ |
| >= | 以上 | 左 | ○ |
| == | 等しい | 左 | ○ |
| != | 等しくない | 左 | ○ |
| & | ビット AND | 左 | ○ |
| ^ | ビット XOR | 左 | ○ |
| | | ビット OR | 左 | ○ |
| && | 論理 AND | 左 | ○ |
| || | 論理 OR | 左 | ○ |
| ?: | 条件分岐 | 右 | × |
| = | 代入 | 右 | ○ |
| *= | 乗算して代入 | 右 | ○ |
| /= | 除算して代入 | 右 | ○ |
| %= | 剰余を求めて代入 | 右 | ○ |
| += | 加算して代入 | 右 | ○ |
| -= | 減算して代入 | 右 | ○ |
| <<= | 左シフトして代入 | 右 | ○ |
| >>= | 右シフトして代入 | 右 | ○ |
| &= | ビット AND して代入 | 右 | ○ |
| ^= | ビット XOR して代入 | 右 | ○ |
| |= | ビット OR して代入 | 右 | ○ |
| throw | 例外を投げる | -- | × |
| , | 文法的に区切る | 左 | ○ |
() は、関数呼び出しの演算子です。これをクラス定義の中でオーバーロードすると、クラスオブジェクトの後ろに () を付けて、関数呼び出しの形式で呼び出せるようになります。
まず、定義の仕方を見てみましょう。次の例では、整数の引数 iValue を取り、これを 2 倍にして返す演算子 () を定義しています。
class CManiac {
public:
int operator()(int iValue) {
return 2 * iValue;
}
};
次に、これを使う側の例を見てみましょう。
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
int iValue = maniac(1); // 関数呼び出し演算子 () を maniac に適用する。
printf("iValue = %d\n", iValue); // 答えは 2 になります。
return 0;
}
まるで、maniac という名前の関数を呼び出しているかのごとくに見えますね。それで、この maniac のような () 演算子が定義されているオブジェクトを関数オブジェクトと呼びます。
上記のサンプルでは、operator() の引数は一つでしたが、元々が関数呼び出しなので、複数の引数を使うことも、もちろん可能です。可変パラメタ関数として使用できる関数呼び出し演算子を定義してみましょう。第一引数は int、第二引数は float、第三引数以降は可変パラメタとしますが、このサンプルでは第三引数を char* として取り出しています。
class CManiac {
public:
int operator()(int iValue, float fValue, ...) {
va_list valist; // 可変パラメタの取り出し
va_start(valist, fValue); // 可変パラメタの取り出し
char* pcText = va_arg(valist, char*); // 可変パラメタの取り出し
va_end(valist); // 可変パラメタの取り出し
return iValue + int(fValue) + strlen(pcText);
}
};
「可変パラメタの取り出し」と書かれた行は、第三引数以降の可変パラメタを取り出しているコードですが、ここでは解説しません。ここで解説したいのは、operator() の引数に複数の引数が使用可能なことと、可変パラメタも使用可能なことです。
上記を使用するコードは例えば、次のようになります。
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
int iValue = maniac(1, 2.3f, "maniac");
printf("iValue = %d\n", iValue); // 答えは、9 になります。
return 0;
}
関数呼び出し演算子 () をグローバル関数として定義することはできません。
タイプ() は、タイプ、つまり変数型の後ろに () をつけた形の演算子で、型変換キャストに使う演算子です。(タイプ) も同様に型変換キャスト演算子です。これらは、演算子の優先順位が異なります。両者の使い方は、下の main 関数を見てください。
さて、これらをクラス定義の中でオーバーロードすると、クラスオブジェクトをタイプであらわされる型にキャストできるようになります。
では、定義の仕方のサンプルコードを見てみましょう。キーワード operator の後にタイプを書きその後に () を書きます。この例では、CManiac オブジェクトを int 型に変換するキャスト演算子を定義しています。これで、両型変換キャスト演算子の両方に使える共通の定義になります。
class CManiac {
public:
operator int() {
return 1;
}
};
次に、これを使う側の例を見てみましょう。
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
int iValue1 = (int)maniac; // キャスト演算子 (int) を maniac に適用する。
int iValue2 = int(maniac); // 上記と異なる形で、キャスト演算子 int() を maniac に適用する。
printf("iValue1 = %d, iValue2 = %d\n", iValue1, iValue2); // 両方とも 1 になります。
return 0;
}
型変換キャスト演算子 タイプ() のサンプル Cast1.cpp
上記の例では、int にキャストする演算子の定義の中身は整数値の 1 を返すことだけですが、文法的には関数の定義には何を書いても良いので、キャスト演算子の中で、ありとあらゆる事ができてしまいます。しかし、キャスト以外の目的でプログラミングしてしまうと、使う側が混乱して、後々の災いの元となる可能性大なので、キャスト以外の目的でプログラミングすることは避けた方が良いでしょう。
なお、上記のサンプルでは、明示的にキャストを行なっていますが、実は、行わなくてもコンパイル可能で、正しく動作します。つまり、次のように書いても問題ありません。
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
int iValue1 = maniac; // キャスト演算子によって、自動的に型変換が行われる。
printf("iValue1 = %d\n", iValue1); // 答えは 1 になります。
return 0;
}
型変換キャスト演算子 タイプ() のサンプル Cast2.cpp
キャスト演算子、タイプ() を、グローバル関数として定義することはできません。
演算子 -> は、もともとクラスへのポインタからメンバにアクセスする演算子ですが、オーバーロードするときにも、何かのクラスのメンバに -> でアクセスする演算子としてしか定義できません。必然的に、戻り値は、クラスオブジェクトへのポインタが最初の選択肢となります。他の選択肢としては、演算子 -> が定義されているクラスオブジェクト(またはその参照)ということになります。
では、さっそく、例を見て見ましょう。戻り値は、最初は最も簡単に、自分自身へのポインタ this としましょう。-> は、単項演算子なので、引数は取りません。
class CManiac {
public:
CManiac* operator->() {
return this; // 自分自身へのポインタを返す。
}
void Hello() {
printf("Hello!\n");
}
};
上記のように CManiac を定義すると、演算子 -> を使って、自分自身の関数 Hello にアクセスすることができるようになります。
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
maniac->Hello(); // 上で定義した -> 演算子を介して、Hello! と表示されます。
(&maniac)->Hello(); // これは、普通の -> 演算子を介して Hello! と表示されます。
return 0;
}
メンバアクセス演算子 -> のサンプル RightArrow1.cpp
もう一つ例を追加しましょう。上記で CManiac には -> 演算子が定義されたので、今度は CManiac への参照を返す -> 演算子を定義してみます。
class CMoreManiac {
CManiac m_maniac;
public:
CManiac& operator->() {
return m_maniac;
}
};
では、使ってみましょう。
int main() {
CMoreManiac moremaniac;
moremaniac->Hello(); // CMoreManiac と CManiac の両 -> 演算子を介して Hello! と表示されます。
return 0;
}
メンバアクセス演算子 -> のサンプル RightArrow2.cpp
演算子 -> の使い方のエッセンスを理解してもらえたでしょうか。面白い使い方ができそうな気がしませんか。
クラスへのポインタからメンバにアクセスする演算子 -> をグローバル関数として定義することはできません。
配列の添え字演算子 [] で使用できる添え字は、通常整数ですが、[] をオーバーロードすることにより、任意の型のオブジェクトで添え字参照できるようになります。
良くある例として、文字列で配列参照する例を見て見ましょう。山の名前を与えて、その高さを得るようにしてみます。簡単のため、富士山にしか対応していません。
class CManiac {
public:
// 山の名前から、山の高さを得る。
int operator[](char* pcMountain) {
if (strcmp(pcMountain, "Fuji") == 0) {
return 3776;
} else {
return 0;
}
}
};
次のように使用します。
int main() {
CManiac maniac;
int iHeight = maniac["Fuji"]; // 富士山の名前 "Fuji" で参照する。
printf("iHeight = %d\n", iHeight); // 答えは 3776 になります。
return 0;
}
添字参照演算子 [] のサンプル SquareBracket1.cpp
[] は元々は添え字参照の演算子ですが、上記の例からわかるように、[] の引数は何でも良いので、引数が 1 個の単なる関数と見ることもできます。たとえば、渡された文字列の長さを返すというようにもできるでしょう。プログラミングしだいというわけです。
配列の添え字参照演算子 [] をグローバル関数として定義することはできません。
後置きのインクリメント ++ と、後置きのデクリメント -- は殆ど同じなので、ここでは ++ のみを解説します。
後置きインクリメントの場合、オーバーロード関数の定義にダミーの引数 int を設けます。これを省略すると、前置きインクリメントの定義になってしまいます。
もう一つのポイントは、後置きの機能を実現するには、インクリメントする前のオブジェクトの状態を返す必要があるため、コピーを作ってからオブジェクトを更新し、コピーを返すようにします。
例として、内部に int 型の変数を持つクラスに後置きインクリメント演算子 ++ を定義してみます。
class CManiac {
private:
int m_iValue;
public:
CManiac() { m_iValue = 0; } // 構築時に 0 に初期化する。
operator int() { return m_iValue; } // int へのキャスト
CManiac operator++(int) { // 後置きの場合ダミーの引数 int をつける。
CManiac maniacOld;
maniacOld.m_iValue = m_iValue; // 古い値を保存しておく。
m_iValue += 1; // 値を更新する。
return maniacOld; // 古い値を返す。
}
};
使い方は、次のようになります。上記のようなコーディングで後置きの ++ の機能が実現されていることを確認してください。
int main() {
CManiac maniac;
int iValue0 = maniac; // 事前
int iValue1 = maniac++;
int iValue2 = maniac; // 事後
printf("iValue0 = %d, iValue1 = %d, iValue2 = %d\n", iValue0, iValue1, iValue2);
// 答えは 0、0、1 になります。
return 0;
}
後置きインクリメント演算子 ++ のサンプル PostIncrement1.cpp
上記と同じ動作をする後置きインクリメント演算子 ++ は、グローバル関数としても定義可能です。
後置きインクリメント演算子 ++ のサンプル PostIncrement2.cpp
形式的に後置きにすればよいだけであれば、上記のようなコーディングは不要で、単純にダミー引数 int をつけるだけで OK です。
new と delete は対で使われるものなので、ここで一緒に解説します。これらは、もともとメモリの確保と開放に使われるものなので、それ以外の目的に使用することは考えない方が良いでしょう。使用目的としては、メモリ確保や開放時に、デバグ用にメッセージを出す/ログと取るとか、システムが用意したヒープ領域以外に独自にメモリを確保して、そこからメモリのアロケートをするなどが普通の使い方になります。
まずは、もっとも単純な普通の new と delete をオーバーロードするサンプルコードを見てみましょう。最初は、グローバル関数として定義してみます。機能は元からある new と delete とほぼ同じものとします(メモリを確保できない場合に、例外を投げないので、その部分が異なります)。この例では、メモリの確保と開放時に、標準出力にメッセージを出力します。
void* operator new(size_t size) {
printf("This is operator new! size = %d\n", size);
return malloc(size);
}
void operator delete(void* pv) {
printf("This is operator delete!\n");
free(pv);
}
メモリの確保と開放 new と delete のサンプル NewDelete1.cpp
上記を使うサンプルコードは、示すまでもありませんが、例えば次のようになります。
int main() {
int* pi = new int;
printf("pi = %08X\n", pi);
delete pi;
return 0;
}
実行結果は、次のようになり、new に int の長さ 4 が渡されていること、delete が呼ばれていることなどが分かります。
This is oparator new! size = 4 pi = 00872950 This is operator delete!
ここで、ちょっと心配になるのは、クラスオブジェクトを new、delete したときに、コンストラクタやデストラクタが正しく呼ばれるのかということですが、これを確認してみましょう。次のサンプルコードで試してみます。new、delete の定義は上記のものをそのまま使用します。
class CManiac {
int m_iValue; // メンバ変数は、int、4 バイト
public:
~CManiac() { printf("CManiac destructor\n"); }
CManiac() { printf("CManiac constructor\n"); }
};
int main() {
CManiac* pmaniac = new CManiac; // クラスオブジェクトを new してみる。
printf("pmaniac = %08X\n", pmaniac);
delete pmaniac;
return 0;
}
メモリの確保と開放 new と delete のサンプル NewDelete2.cpp
実行の結果は、次のようになり、コンストラクタに int の長さ 4 バイトが渡されていることや、何もコーディングしなくてもコンストラクタとデストラクタが正しく呼ばれることが分かります。
This is oparator new! size = 4 CManiac constructor pmaniac = 00872950 CManiac destructor This is operator delete!
このように、コンストラクタとデストラクタが自動的に呼ばれるので、new と delete をメモリの確保と開放以外の目的のためにオーバーロードすることは、ほぼありえないことになります。
なお、上記のサンプルで new CManiac の結果を CManiac* で受けていますが、これを void* で受けると delete したときに CManiac のデストラクタが呼ばれません。これは、delete が、ポインタの型を見てどのデストラクタを呼ぶかを決めていることを示しています。
さて、次に、上記とまったく同じ動作をする new、delete を、クラスの静的メンバ関数として定義してみましょう。new、delete は静的でないメンバ関数としては定義できません。サンプルは次のようになります。
class CManiac {
int m_iValue;
public:
~CManiac() { printf("CManiac destructor\n"); }
CManiac() { printf("CManiac constructor\n"); }
static void* operator new(size_t size) {
printf("This is operator new! size = %d\n", size);
return malloc(size);
}
static void operator delete(void* pv) {
printf("This is delete!");
free(pv);
return;
}
};
メモリの確保と開放 new と delete のサンプル NewDelete3.cpp
このように定義された new、delete は、静的メンバ関数なので、this ポインタを使用することは出来ません。
先ほど上で使ったのと同じ main() 関数を使うと、やはり、上で見たのとまったく同じ実行結果が得られます。
私の経験からは、配置 new、delete は、コンパイラによって動作が異なるという問題があり、この項の最後に提示するサンプルコードのように アロケータを使用してメモリの確保/開放を行う 方法以外は、メリットとデメリットを比較すると、使わないですむのならば、使わないでおくというのが C++ を楽しむためには最良の方法であると思います。詳しくは、この項の最後の部分 私が、配置 new、delete を殆ど不要であると思う理由 を参照ください。
さて、最初から出鼻をくじくような事を書きましたが、それでは、配置 new、delete について解説します。配置 new は通常、既に確保してあるメモリ上にオブジェクトを配置したいときに使用します。配置 delete は配置 new に対して対象性があるように定義されます。
通常の new は第一引数に size_t 型の引数に取りますが、配置 new は更に、第二引数以降に自由な型、個数の引数をとります。例えば size_t 以外に 3 個の引数を取るような配置 new のプロトタイプは次のようになります。対象性のある配置 delete のプロトタイプも同時に示します。
void* operator new(size_t size, type1 t1, type2 t2, type3 t3); void operator delete(void* pv, type1 t1, type2 t2, type3 t3);
下記のような new の呼び出しで size には、コンパイラによって、確保すべきオブジェクト CManiac のサイズが与えられ、残りの引数 t1, t2, t3 にも見たとおりの順で与えられた引数が入ります。
CManiac* pmaniac = new(t1, t2, t3) CManiac;
それでは、既に確保されたメモリ上にオブジェクトを配置する例について見て見ましょう。まず、次のようにメモリが確保されているとします。
char acTable[100]; // 100 バイトの領域を確保した。
この acTable 上にオブジェクトを配置したいわけですが、例えば次のように配置 new、delete を定義します。
void* operator new(size_t size, void* pv) {
printf("This is oparator new! size = %d, pv = %08X\n", size, pv);
return pv;
}
void operator delete(void* pv, void*) {
printf("This is operator delete! pv = %08X\n", pv); // 普通は、何もする必要はない
}
呼び出し側のコードは次のようになります。new の後ろに () を付けて、その中にあらかじめ確保したメモリの先頭アドレスを入れます。
CManiac* pmaniac = new(acTable) CManiac;
これで、new の第一引数 size には、コンパイラが sizeof CManiac を自動的に与えます。第二引数には acTable が渡されます。pmaniac には、配置 new の呼び出しで、結果的に acTable のアドレスが入ります。
さて、上記のように確保したオブジェクトを破壊するには、配置 delete を次のように呼び出します。
pmaniac->~CManiac();
operator delete((void*)pmaniac, (void*)0); // 普通は、呼び出す必要はないので、この行は不要。
配置 delete をスマートに呼び出す構文は用意されていないので、かなり不恰好(使うなということでしょうか? または、実際上呼び出す必要が無いということ!)ですが、これで一応動作します。
呼び出し側コードをまとめると次のようになります。
int main() {
char acTable[100];
CManiac* pmaniac = new(acTable) CManiac; // クラスオブジェクトを new してみる。
printf("acTable = %08X, pmaniac = %08X\n", acTable, pmaniac);
pmaniac->~CManiac();
operator delete((void*)pmaniac, (void*)0);
return 0;
}
メモリの確保と開放 new と delete のサンプル NewDelete4.cpp
実行結果は次のようになります。
This is oparator new! size = 4, pv = 0012FF00 CManiac constructor this = 0012FF00 acTable = 0012FF00, pmaniac = 0012FF00 <---意図どおり、同じアドレスになっています。 CManiac destructor This is operator delete! pv = 0012FF00
配置 new、delete とアロケータを使用してメモリの確保/開放を行うサンプルだけをアップしておきます。ここでのアロケータは C++ の規格書に書かれているアロケータではなく、概念を示すためだけのきわめて単純なものです。
メモリの確保と開放 new と delete のサンプル NewDelete5.cpp
さて、私が、配置 new、delete を殆ど不要であると思う理由 を、少し述べておきます。
まず、配置 new、delete をクラスの静的メンバ関数として定義しようとすると、各コンパイラで動作がまちまちで、私の能力ではとても手に負えないぐらい厄介なことになります。これが、第一の理由です。
次に、配置 new でクラスオブジェクトを構築したとしても、そのクラスの定義の中で、通常の new を使って、メンバ変数などを確保していると、そのメンバ変数用のメモリは、通常のヒープから確保されてしまうので、配置 new を使ったからといって、独自ヒープからメモリが取られる保証がないということがあります。
最後に、独自に用意した領域からメモリを確保する方法として、アロケータを使用する方法があります。クラスの内部でメモリを確保するときに new や配置 new を使用しないで、陽に外から与えたアロケータオブジェクトを使用してメモリを確保するようにクラスを設計しておきます。このようにすれば、メモリはアロケータを介して確保されるので、アロケータの実装を変更すればメモリをどこから確保するかを変更できます。これは、C++ の標準ライブラリでも使われている方法で、一般的なものです。この場合クラスオブジェクト自体は通常の new で確保されますが、クラス内部の大きなメモリ領域などを独自のアロケータで確保するように制御できるわけです。普通はこれで十分です。このような、代替の方法があるので、よほどのことがない限り、問題の多い配置 new、delete を使う必要がないわけです。
逆にいうと、配置 new、delete は、アロケータを実装する場合以外には殆ど使い道はない、と思ってよいでしょう。しかし、アロケータを実装することは有意義なので、配置 new、delete は、このためだけにでも、やはり必要、必須なものなのです。
配列メモリの確保と開放をする new[]、delete[] と、配列用ではない new、delete との主な差は、使用時に、オブジェクトのコンストラクタとデストラクタが、配列で確保したオブジェクトの数だけ呼び出されることです。例を見て見ましょう。
まず、new[] と delete[] を次のように定義します。
void* operator new[](size_t iSize) {
printf("This is oparator new[] size = %d\n", iSize);
return malloc(iSize);
}
void operator delete[](void* pv) {
printf("This is operator delete[]\n");
free(pv);
}
構築するクラスと、new[]、delete[] を使用するコードを次のようにします。
class CManiac {
int m_iValue;
public:
~CManiac() { printf("CManiac destructor\n"); }
CManiac() { printf("CManiac constructor\n"); }
};
int main() {
CManiac* pmaniac = new CManiac[3]; // クラスオブジェクトを new[] してみる。
printf("pmaniac = %08X, count = %d\n", pmaniac, pmaniac[-1]);
printf("pmaniac = %08X, count = %d\n", pmaniac, ((int*)pmaniac)[-1]);
delete[] pmaniac;
return 0;
}
メモリの確保と開放 new[] と delete[] のサンプル NewDelete6.cpp
実行結果は、次のようになります。
This is oparator new! size = 16 CManiac constructor CManiac constructor CManiac constructor pmaniac = 00872954, count = 3 CManiac destructor CManiac destructor CManiac destructor This is operator delete[]
上記の出力で、少し特殊なのは、new[] に渡されているサイズが 4 バイトである int の 3 倍の 12 ではなく、16 となっているところですが、これは、コンパイラが何らかのヘッダ情報を入れるメモリを 4 バイト分確保しているからです。上記の結果では、配列要素の数 3 を保存しているらしいことが分かります。
これ以外に、new[]、delete[] に難しいところはないので、残りの詳細については new と delete、メモリの確保と開放 をご覧ください。
前置きインクリメントとデクリメントは、単項演算子で、あまり特殊性はありません。
前置きの ++ と -- は、何かを増加させる/減少させる、とか、進める/戻す、とか言う意味に使用するのがよいでしょう。ここでは例として、山手線の駅名を返すクラスを作成してみましょう。全ての駅を網羅すると、サンプルが大きくなるので、駅名は三つだけにします。++ 演算子で、次の駅に移動するようにします。-- は、まったく同様に実装できるので、省略します。次のコードを見てください。
class CYamanote {
private:
vector<string> m_vecstrStation; // 駅名を入れる配列
int m_iStation; // 駅のインデクス
public:
CYamanote() {
m_iStation = 0;
m_vecstrStation.push_back("東京" );
m_vecstrStation.push_back("神田" );
m_vecstrStation.push_back("秋葉原");
}
CYamanote& operator ++() {
m_iStation = ++m_iStation % m_vecstrStation.size(); // 次の駅に移動する。
return *this;
}
const char* c_str() {
return m_vecstrStation[m_iStation].c_str();
}
};
ここでは、operator ++ は、CYamanote オブジェクトへの参照を返すようにしました。const char* 型の駅名を関数 c_str() で返します。
上記のように定義することで、次のようなコードが使えるようになります。
int main() {
CYamanote yamanote;
{for (int iIter = 0; iIter < 5; iIter++) {
printf("%s\n", (++yamanote).c_str());
}}
return 0;
}
上記を実行すると、次のように表示されます。
神田 秋葉原 東京 神田 秋葉原
前置きインクリメント演算子 ++ のサンプル PreIncrement1.cpp
上記と同じ動作をする前置きインクリメント演算子 ++ は、グローバル関数としても定義可能です。
前置きインクリメント演算子 ++ のサンプル PreIncrement2.cpp
単項演算子の ~(全ビット反転)、!(論理反転)、+(単項プラス)、-(単項マイナス)、&(アドレスを得る)、*(ポインタから実体を参照)は、全てあまり特殊性がなく、取り扱いとしては殆ど同じです。単項演算子 + について このページの冒頭で定義の仕方を述べたので、そちらをご覧ください。本当に簡単なので、+ 以外の演算子についても、類推可能な範囲のはずです。
& と * は、もともとは、それぞれ、変数のアドレスを得る、ポインタから実体を参照、という意味がありますが、オーバーロードするという立場からは、単項 + などと同様、単純なものです。従って、どちらも単項 + のサンプルのように、単純に整数 2 を返す、というような実装も可能です。しかし、もともとの演算子の意味づけを考慮して & は何らかのアドレスを返すように、また、* は何らかのポインタ的なものから実体を参照するという意味に定義するのが良いでしょう。これらのサンプルをアップしておきます。
ポインタから実体を参照 * のサンプル UnaryAst1.cpp
->* 演算子は、元々、クラスのメンバにアクセスするためのもので、それ自体が少し特殊なので、私自身は、この演算子をオーバーロードして何かをしたことがありません。STL の mem_fun の実装には、この演算子が使われていますが、その場合でも、この演算子をオーバーロードしているわけではなく、単純に、使用しているだけです。
では、オーバーロードが難しいのかというと、そうではなく、単にオーバーロードするだけであれば、他の演算子と同様、次のように、オーバーロードできます。
class CManiac {
public:
int operator->*(int iValue) { // 単純に、int をもらってプリントする。
printf("%d\n", iValue);
return iValue;
}
};
int main() {
CManiac maniac;
maniac->*123; // iValue = 123 と表示されます。
return 0;
}
->* クラスオブジェクトへのポインタからメンバを選択のサンプル ArrowAst1.cpp
上記のサンプルを実行すると、ちゃんと、次のように表示されます。
iValue = 123
->* をオーバーロードして有用と思われる良い例を思いつかないので、ここまでとしておきます。->* 演算子のオーバーロードではなく、演算子そのものの使い方については.*、->* 演算子とポインタの使い方をご覧ください。
等しいを表す == 演算子は、より小さいを表す < 演算子と共に、比較的良く使われる演算子です。文字通りの意味である「等しい」を表すサンプルコードは次のようになります。CManiac オブジェクトが int 型変数と等しいかどうかを返すので、戻り値の型は bool にします。
class CManiac {
public:
bool operator==(int iValue) {
return iValue == 0;
}
};
使い方は、次のようになります。
int main() {
CManiac maniac;
bool bIsSame = maniac == 0; // == の左辺は CManiac、右辺は int
printf("bIsSame = %d\n", bIsSame); // 答えは 1 になります。
return 0;
}
同じ内容の演算子 == をグローバル関数として定義することもできますが、この場合、グローバル関数として定義しても、上記のようにクラスのメンバ関数として定義したとき以上の事はできませんので、あまり意味はありません。サンプルコードのみを置いておきます。
これに対して、グローバル関数として定義して意味があるのは、二項演算子である == の左辺と右辺を入れ替える場合です。上記の例で言えば、左辺を int 型とし、右辺を CManiac& 型とする場合です。これは CManiac のメンバ関数としては定義できないので、グローバル関数の出番となります。サンプルコードを見てみましょう。
class CManiac {};
bool operator==(int iValue, CManiac& rmaniac) {
rmaniac; // 本来ここは rmaniac を使って何かをするところです。
return iValue == 0;
}
int main() {
CManiac maniac;
bool bIsSame = 0 == maniac; // == の左辺は int、右辺は CManiac
printf("bIsSame = %d\n", bIsSame); // 答えは 1 になります。
return 0;
}
これで、== の左右に CManiac と int 型のオブジェクトを置いて、比較することが自由に出来ます。これでも、まだ CManiac どうしを比較することはできませんが、このための演算子定義は、最初の例から簡単に類推できるように、例えば、次のようになります。このサンプルコードでは、単純にオブジェクトのアドレスを比較して、同じ一つのオブジェクトかどうかを返しています。
class CManiac {
public:
bool operator==(CManiac& rmaniac) {
return &rmaniac == this; // メンバ変数比較などの他の実装方法も、もちろんあり得ます。
}
};
int main() {
CManiac maniac1, maniac2;
bool bIsSame = maniac1 == maniac2; // == の左辺は CManiac、右辺も CManiac
printf("bIsSame = %d\n", bIsSame); // 答えは 0 になります。
return 0;
}
様々な型に対して「等しい」を表す演算子 == を定義しようとすると、上記のように演算子の左右に来るオブジェクトの型の組み合わせ毎に関数を定義する必要があるため、完成したクラス定義を見ると一見、複雑に見えますが、その一つ一つは単純なものであることが分かると思います。
代入演算子 = の元の意味は、文字通り代入を表すものなので、オーバーロードするときにも代入を意味するようにするのが良いでしょう。標準的な例を見て見ましょう。CManiac オブジェクトに int 型の値を代入する演算子の定義です。
class CManiac {
private:
int m_iValue;
public:
int GetiValue() {
return m_iValue;
}
CManiac& operator=(int iValue) {
m_iValue = iValue;
return *this;
}
};
この例では、代入演算子の戻り値として、自分自身への参照型 CManiac& である *this を返しています。これが普通の使い方です。文法上はどのような型でも返すことができるので、例えば、int 型を返すというようなことも可能ですが、よほどの理由がない限りそのような使い方は有害無益なので、避けるべきでしょう。
さて、使い方ですが、次のようになります。
int main() {
CManiac maniac; // クラスオブジェクト maniac を作成する。
maniac = 1; // 整数 1 を代入する。
int iValue = maniac.GetiValue(); // 確認のため、読んでみる。
printf("iValue1 = %d\n", iValue); // 答えは 1 になります。
return 0;
}
maniac に整数を代入することができるようになりましたね。
代入演算子 = をグローバル関数として定義することはできません。
上記とは逆に、クラスオブジェクトを何かの型に代入したいという場合には、代入演算子ではなく、キャスト演算子を使用します。
... to be continued!
ナオ : このページ、ずっと ... to be continued! ってなってるけど、いつアップデートされるの?
店主 : (ぎくっ) いやぁ、まだ解説していない演算子は、ほとんど単純なものばかりなので、+ 演算子と同じと考えてもらえばいいんだけど。
ナオ : って、当分このままってこと?
店主 : ん、まぁ。(我ながら、歯切れが悪いノォ...)
当サイトは転載不可、リンクフリーです。