第5章では、C++のオブジェクト指向プログラミングの核となるclassの基本的な使い方を学びました。しかし、クラスを真に強力なツールとして使いこなすには、もう少し知識が必要です。この章では、オブジェクトのコピー、演算子のオーバーロード、クラスで共有されるメンバなど、より実践的でパワフルな機能について掘り下げていきます。これらの概念をマスターすることで、あなたの書くクラスはより安全で、直感的で、再利用性の高いものになるでしょう。
オブジェクトをコピーしたい場面は頻繁にあります。例えば、関数の引数にオブジェクトを渡すとき(値渡し)や、既存のオブジェクトで新しいオブジェクトを初期化するときなどです。
Vector2D v1(1.0, 2.0);
Vector2D v2 = v1; // ここでコピーが発生!多くの場合、コンパイラが自動的に生成するコピー機能で十分です。しかし、クラスがポインタなどでリソース(メモリなど)を管理している場合、単純なコピーでは問題が発生します。
まず、コピーの機能を自分で作らなかった場合に何が起きるか見てみましょう。 コンパイラは、メンバ変数を単純にコピーするだけの「浅いコピー」を行います。
ここでは、intへのポインタを一つだけ持つResourceHolder(リソース保持者)というクラスを考えます。
この例では、r2が作られるときにr1のポインタm_dataの値(メモリアドレス)だけがコピーされます。その結果、2つのオブジェクトが1つのメモリ領域を指してしまいます。プログラム終了時にそれぞれのデストラクタが呼ばれ、同じメモリを2回解放しようとしてエラーになります。
この問題を解決するために、コピーコンストラクタとコピー代入演算子を自分で定義して、「深いコピー」を実装します。深いコピーとは、ポインタの指す先の実体(データそのもの)を新しく作ってコピーすることです。
(メモリアドレスは実行するたびに変わります)
実行結果を見ると、rh1, rh2, rh3 はそれぞれ異なるメモリアドレス (Address) を持っていることがわかります。これにより、各オブジェクトは独立したリソースを管理でき、プログラム終了時にそれぞれのデストラクタが安全にメモリを解放できます。
| 機能 | いつ呼ばれるか | 何をするか |
|---|---|---|
| コピーコンストラクタ | オブジェクトが作られる時に、他のオブジェクトで初期化される場合<br>ResourceHolder r2 = r1; | 新しいリソースを確保し、元のオブジェクトの値をコピーする。 |
| コピー代入演算子 | 既にあるオブジェクトに、他のオブジェクトを代入する場合<br>r3 = r1; | 1. 自分が持っている古いリソースを解放する。<br>2. 新しいリソースを確保し、元のオブジェクトの値をコピーする。 |
このように、ポインタでリソースを管理するクラスでは、安全なコピーを実現するためにこの2つの関数を自分で定義することが不可欠です。
C++では、+, -, ==, << などの組み込み演算子を、自作のクラスで使えるように**再定義(オーバーロード)**できます。これにより、クラスのインスタンスをあたかも組み込み型(intやdoubleなど)のように直感的に扱えるようになります。
例えば、2次元ベクトルを表す Vector2D クラスがあるとします。v3 = v1 + v2; のように、ベクトル同士の足し算を自然に記述できると便利ですよね。
演算子のオーバーロードは、メンバ関数または非メンバ関数(グローバル関数)として定義します。
| 演算子 | メンバ関数での定義 | 非メンバ関数での定義 |
|---|---|---|
二項演算子 (+, == etc.) | T operator+(const U& rhs); | T operator+(const T& lhs, const U& rhs); |
単項演算子 (-, ! etc.) | T operator-(); | T operator-(const T& obj); |
Vector2D クラスで +(加算)、==(等価比較)、<<(ストリーム出力)をオーバーロードしてみましょう。
operator<< は、左辺のオペランドが std::ostream 型(std::cout など)であるため、Vector2D のメンバ関数としては定義できません。そのため、非メンバ関数として定義するのが一般的です。
通常、クラスのメンバ変数はオブジェクトごとに個別のメモリ領域を持ちます。しかし、あるクラスの全てのオブジェクトで共有したい情報もあります。例えば、「これまでに生成されたオブジェクトの総数」などです。このような場合、staticメンバを使用します。
static キーワードを付けて宣言されたメンバ変数は、特定のオブジェクトに属さず、クラスそのものに属します。そのため、全オブジェクトでただ1つの実体を共有します。これをクラス変数と呼ぶこともあります。
static を付けて行います。static キーワードを付けて宣言されたメンバ関数は、特定のオブジェクトに依存せずに呼び出せます。そのため、this ポインタ(後述)を持ちません。
クラス名::関数名() のように、オブジェクトを生成しなくても呼び出せます。ゲームに登場する Player クラスがあり、現在何人のプレイヤーが存在するかを管理する例を見てみましょう。
playerCount は p1, p2, p3 の全てで共有されており、一つの値が更新されていることがわかります。
非staticなメンバ関数が呼び出されるとき、その関数は「どのオブジェクトに対して呼び出されたか」を知る必要があります。コンパイラは、そのメンバ関数に対して、呼び出し元のオブジェクトのアドレスを暗黙的に渡します。このアドレスを保持するのが this ポインタです。
this は、メンバ関数内で使用できるキーワードで、自分自身のオブジェクトを指すポインタです。
this ポインタが主に使われるのは、以下のような場面です。
メンバ変数と引数の名前が同じ場合
コンストラクタの初期化子リストを使わない場合など、引数名とメンバ変数名が同じになることがあります。その際、this-> を付けることでメンバ変数であることを明示できます。
void setX(double x) {
this->x = x; // this->x はメンバ変数, x は引数
}自分自身の参照やポインタを返す場合
コピー代入演算子で return *this; としたように、オブジェクト自身を返したい場合に使います。これにより、メソッドチェーン(obj.setX(10).setY(20); のような連続したメソッド呼び出し)が可能になります。
メソッドチェーンを実現する簡単な例を見てみましょう。
setX が p 自身の参照を返すため、その返り値に対して続けて .setY(20) を呼び出すことができます。
この章では、クラスをより効果的に利用するための応用的な機能を学びました。
+ や == などの演算子を自作クラスに対して定義することで、コードの可読性を高め、直感的な操作を可能にします。staticメンバ変数やメンバ関数は、クラスの全オブジェクトで共有されるデータや機能を提供します。オブジェクトを生成しなくてもアクセスできるのが特徴です。これらの機能を組み合わせることで、C++のクラスは単なるデータの入れ物から、振る舞いを伴った洗練された部品へと進化します。
実部 (real) と虚部 (imaginary) をdouble型で持つ複素数クラス Complex を作成してください。以下の要件を満たすものとします。
+) と掛け算 (*) を演算子オーバーロードで実装する。
std::cout で (a + bi) という形式で出力できるように、<< 演算子をオーバーロードする。(虚部が負の場合は (a - bi) のように表示されるとより良い)整数 (int) の動的配列を管理するクラス IntArray を作成してください。このクラスは、コンストラクタで指定されたサイズの配列を new で確保し、デストラクタで delete[] を使って解放します。
この IntArray クラスに対して、深いコピーを正しく行うためのコピーコンストラクタとコピー代入演算子を実装してください。