第6章: クラスを使いこなす

第5章では、C++のオブジェクト指向プログラミングの核となるclassの基本的な使い方を学びました。しかし、クラスを真に強力なツールとして使いこなすには、もう少し知識が必要です。この章では、オブジェクトのコピー、演算子のオーバーロード、クラスで共有されるメンバなど、より実践的でパワフルな機能について掘り下げていきます。これらの概念をマスターすることで、あなたの書くクラスはより安全で、直感的で、再利用性の高いものになるでしょう。

オブジェクトのコピー: コピーコンストラクタと代入演算子

オブジェクトをコピーしたい場面は頻繁にあります。例えば、関数の引数にオブジェクトを渡すとき(値渡し)や、既存のオブジェクトで新しいオブジェクトを初期化するときなどです。

Vector2D v1(1.0, 2.0); Vector2D v2 = v1; // ここでコピーが発生!

多くの場合、コンパイラが自動的に生成するコピー機能で十分です。しかし、クラスがポインタなどでリソース(メモリなど)を管理している場合、単純なコピーでは問題が発生します。

何もしないとどうなる? (浅いコピー)

まず、コピーの機能を自分で作らなかった場合に何が起きるか見てみましょう。 コンパイラは、メンバ変数を単純にコピーするだけの「浅いコピー」を行います。

ここでは、intへのポインタを一つだけ持つResourceHolder(リソース保持者)というクラスを考えます。

shallow_copy.cpp

この例では、r2が作られるときにr1のポインタm_dataの値(メモリアドレス)だけがコピーされます。その結果、2つのオブジェクトが1つのメモリ領域を指してしまいます。プログラム終了時にそれぞれのデストラクタが呼ばれ、同じメモリを2回解放しようとしてエラーになります。

解決策:コピー機能を自作する (深いコピー)

この問題を解決するために、コピーコンストラクタコピー代入演算子を自分で定義して、「深いコピー」を実装します。深いコピーとは、ポインタの指す先の実体(データそのもの)を新しく作ってコピーすることです。

resource_holder.cpp

(メモリアドレスは実行するたびに変わります)

実行結果を見ると、rh1, rh2, rh3 はそれぞれ異なるメモリアドレス (Address) を持っていることがわかります。これにより、各オブジェクトは独立したリソースを管理でき、プログラム終了時にそれぞれのデストラクタが安全にメモリを解放できます。

機能いつ呼ばれるか何をするか
コピーコンストラクタオブジェクトが作られる時に、他のオブジェクトで初期化される場合<br>ResourceHolder r2 = r1;新しいリソースを確保し、元のオブジェクトのをコピーする。
コピー代入演算子既にあるオブジェクトに、他のオブジェクトを代入する場合<br>r3 = r1;1. 自分が持っている古いリソースを解放する。<br>2. 新しいリソースを確保し、元のオブジェクトのをコピーする。

このように、ポインタでリソースを管理するクラスでは、安全なコピーを実現するためにこの2つの関数を自分で定義することが不可欠です。

演算子のオーバーロード

C++では、+, -, ==, << などの組み込み演算子を、自作のクラスで使えるように**再定義(オーバーロード)**できます。これにより、クラスのインスタンスをあたかも組み込み型(intdoubleなど)のように直感的に扱えるようになります。

例えば、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_overloading.cpp

operator<< は、左辺のオペランドが std::ostream 型(std::cout など)であるため、Vector2D のメンバ関数としては定義できません。そのため、非メンバ関数として定義するのが一般的です。

staticメンバ

通常、クラスのメンバ変数はオブジェクトごとに個別のメモリ領域を持ちます。しかし、あるクラスの全てのオブジェクトで共有したい情報もあります。例えば、「これまでに生成されたオブジェクトの総数」などです。このような場合、staticメンバを使用します。

staticメンバ変数

static キーワードを付けて宣言されたメンバ変数は、特定のオブジェクトに属さず、クラスそのものに属します。そのため、全オブジェクトでただ1つの実体を共有します。これをクラス変数と呼ぶこともあります。

  • 宣言: クラス定義の中で static を付けて行います。
  • 定義: クラス定義の外(ソースファイル)で、メモリ上の実体を確保し、初期化します。

staticメンバ関数

static キーワードを付けて宣言されたメンバ関数は、特定のオブジェクトに依存せずに呼び出せます。そのため、this ポインタ(後述)を持ちません。

  • アクセス: staticメンバ変数や他のstaticメンバ関数にはアクセスできますが、非staticなメンバ(インスタンスごとのメンバ変数やメンバ関数)にはアクセスできません。
  • 呼び出し: クラス名::関数名() のように、オブジェクトを生成しなくても呼び出せます。

実装例

ゲームに登場する Player クラスがあり、現在何人のプレイヤーが存在するかを管理する例を見てみましょう。

static_members.cpp

playerCountp1, p2, p3 の全てで共有されており、一つの値が更新されていることがわかります。

thisポインタ

非staticなメンバ関数が呼び出されるとき、その関数は「どのオブジェクトに対して呼び出されたか」を知る必要があります。コンパイラは、そのメンバ関数に対して、呼び出し元のオブジェクトのアドレスを暗黙的に渡します。このアドレスを保持するのが this ポインタです。

this は、メンバ関数内で使用できるキーワードで、自分自身のオブジェクトを指すポインタです。

this ポインタが主に使われるのは、以下のような場面です。

  1. メンバ変数と引数の名前が同じ場合 コンストラクタの初期化子リストを使わない場合など、引数名とメンバ変数名が同じになることがあります。その際、this-> を付けることでメンバ変数であることを明示できます。

    void setX(double x) { this->x = x; // this->x はメンバ変数, x は引数 }
  2. 自分自身の参照やポインタを返す場合 コピー代入演算子で return *this; としたように、オブジェクト自身を返したい場合に使います。これにより、メソッドチェーンobj.setX(10).setY(20); のような連続したメソッド呼び出し)が可能になります。

実装例

メソッドチェーンを実現する簡単な例を見てみましょう。

this_pointer.cpp

setXp 自身の参照を返すため、その返り値に対して続けて .setY(20) を呼び出すことができます。

この章のまとめ

この章では、クラスをより効果的に利用するための応用的な機能を学びました。

  • オブジェクトのコピー: ポインタなどリソースを管理するクラスでは、コピーコンストラクタコピー代入演算子を定義し、深いコピーを実装することが重要です。これにより、リソースの二重解放などの問題を未然に防ぎます。
  • 演算子のオーバーロード: +== などの演算子を自作クラスに対して定義することで、コードの可読性を高め、直感的な操作を可能にします。
  • staticメンバ: staticメンバ変数やメンバ関数は、クラスの全オブジェクトで共有されるデータや機能を提供します。オブジェクトを生成しなくてもアクセスできるのが特徴です。
  • thisポインタ: 非staticメンバ関数内で、呼び出し元のオブジェクト自身を指すポインタです。メンバ変数と引数の区別や、メソッドチェーンの実装に役立ちます。

これらの機能を組み合わせることで、C++のクラスは単なるデータの入れ物から、振る舞いを伴った洗練された部品へと進化します。

練習問題1: 複素数クラス

実部 (real) と虚部 (imaginary) をdouble型で持つ複素数クラス Complex を作成してください。以下の要件を満たすものとします。

  1. コンストラクタで実部と虚部を初期化できるようにする。
  2. 複素数同士の足し算 (+) と掛け算 (*) を演算子オーバーロードで実装する。
    • 加算: $(a+bi) + (c+di) = (a+c) + (b+d)i$
    • 乗算: $(a+bi) * (c+di) = (ac-bd) + (ad+bc)i$
  3. std::cout(a + bi) という形式で出力できるように、<< 演算子をオーバーロードする。(虚部が負の場合は (a - bi) のように表示されるとより良い)
practice6_1.cpp

練習問題2: 動的配列クラスのコピー制御

整数 (int) の動的配列を管理するクラス IntArray を作成してください。このクラスは、コンストラクタで指定されたサイズの配列を new で確保し、デストラクタで delete[] を使って解放します。

この IntArray クラスに対して、深いコピーを正しく行うためのコピーコンストラクタコピー代入演算子を実装してください。

practice6_2.cpp