
C++プログラミング入門―初心者からその先へ: カプセル化と初期化 (セクション13-2/25)
アクセス修飾子(public、private、protected)を活用して、クラスの内部データを保護し、必要な操作のみを外部に公開することで情報隠蔽を実現する。
クラスメソッドは、クラス内または外で実装可能であり、ヘッダーファイルとソースファイルに分割することで保守性と再利用性が向上する。
コンストラクタとデストラクタを利用してオブジェクトの初期化と破棄を適切に管理し、オーバーロードにより柔軟な初期化方法を提供できる。
このC++のオブジェクト指向プログラミング(OOP)の旅のこの部分では、クラスが内部の詳細を隠蔽し、明確に定義されたインターフェースを提供し、オブジェクトのライフタイムをどのように管理するかについてさらに深く掘り下げます。具体的には、データ隠蔽のためのアクセス修飾子、メンバーメソッドの実装方法、そしてオブジェクトの生成と破棄におけるコンストラクタ(デフォルトおよびオーバーロードされたものを含む)とデストラクタの基本的な役割について説明します。では、始めましょう。
データを隠蔽し機能を公開する
C++(および他の多くのOOP言語)における最も強力な機能のひとつは、クラスのどの部分が外部からアクセス可能であり、どの部分がクラス内部に隠されるかを明確に分離できることです。これはアクセス修飾子を使って行われます。
public(パブリック):publicとして宣言されたものは、オブジェクトが見える範囲であればどこからでも利用できます。
private(プライベート):プライベートメンバーは、同じクラス内(およびフレンドによるアクセス)でのみアクセス可能です。
protected(プロテクテッド):多くの場合プライベートと似ていますが、継承時に意味を持ちます(後ほど説明します)。
クラスメンバーはアクセス修飾子を指定しない場合、デフォルトでprivateになります。一般的には、機密性の高いデータはprivateに保ち、必要なルールを強制するメソッドのみを公開するようにします。例えば、銀行口座の残高がある上限を超えないようにする場合や、ゲームキャラクターの体力が特定の値を超えないようにする場合など、メンバーをpublicにしてしまうと外部から安全確認をバイパスされる可能性があります。
例えば、以下のようなシンプルな Player クラスを考えます。
class Player {
private:
std::string name;
int health;
int xp;
public:
void talk(std::string text_to_say);
bool is_dead();
};
ここでは、name、health、および xp はprivateのため、外部のコードは以下のように書くことはできません:
Player frank;
// frank.name = "Frank"; // ❌ エラー:nameはprivateです!
しかし、publicメソッドである talk(...) は完全にアクセス可能です:
frank.talk("Hello!"); // ✅ 許可されます
同様に、Account クラスの場合も考えてみましょう:
class Account {
private:
std::string name;
double balance;
public:
bool deposit(double amount);
bool withdraw(double amount);
};
ここでは、balance を直接操作できず、deposit(...) や withdraw(...) のようなメソッドを通じてのみ操作できるようにすることで、安全性を確保しています。この設計は情報隠蔽を促進します。もし口座の残高に予期しない値が入っている場合、その原因はdeposit/withdrawメソッドだけにあると特定できるため、デバッグが容易になります。
クラスメソッドの実装
クラス宣言内での実装
小さく単純なメソッドであれば、クラスの本体内で直接実装することができます:
class Account {
private:
double balance;
public:
void set_balance(double bal) { balance = bal; }
double get_balance() { return balance; }
};
この方法はコードを簡潔にでき、これらのメソッドは暗黙的にインライン化されます。
クラス宣言外での実装
より大きなメソッドの場合は、クラス宣言内で宣言し、クラス宣言の外で定義を記述します。スコープ解決演算子 ClassName:: を使って定義します:
class Account {
private:
std::string name;
double balance;
public:
void set_name(std::string n);
std::string get_name();
bool deposit(double amount);
bool withdraw(double amount);
};
そして、例えば .cpp ファイルやクラス宣言の下で以下のように定義します:
void Account::set_name(std::string n) {
name = n;
}
std::string Account::get_name() {
return name;
}
bool Account::deposit(double amount) {
balance += amount;
return true; // 条件に応じてfalseを返すことも可能
}
bool Account::withdraw(double amount) {
if(balance - amount >= 0) {
balance -= amount;
return true;
}
return false;
}
ヘッダーファイルとソースファイル
大規模なプロジェクトでは、各クラスは通常以下の二つのファイルに分割されます:
ヘッダーファイル(例:Account.h):クラス定義とインクルードガード(コンパイラが一度だけファイルを読み込むようにするための仕組み)を含む。
ソースファイル(例:Account.cpp):メソッドの実装を含む。
例えば、Account.h の内容は以下のようになります:
#ifndef _ACCOUNT_H_
#define _ACCOUNT_H_
#include <string>
class Account {
private:
std::string name;
double balance;
public:
void set_balance(double bal);
double get_balance();
// ...
};
#endif
そして Account.cpp では:
#include "Account.h"
void Account::set_balance(double bal) {
balance = bal;
}
double Account::get_balance() {
return balance;
}
// ...
その後、main.cpp で単に #include "Account.h" とし、コンパイル時にすべてのファイルをリンクします。
コンストラクタとデストラクタ
コンストラクタ:オブジェクトの「誕生」
オブジェクトが作成されると、C++では自動的にコンストラクタが呼び出され、初期化が行われます。コンストラクタの特徴は以下の通りです:
クラスと同じ名前を持つ。
返り値がない(voidさえも指定しません)。
異なる初期化シナリオを可能にするためにオーバーロードされることがあります。
例えば、Player クラスには複数のコンストラクタが用意されるかもしれません:
class Player {
private:
std::string name;
int health;
int xp;
public:
Player(); // 引数なし
Player(std::string name_val);
Player(std::string name_val, int health_val, int xp_val);
};
具体例としては:
Player::Player()
: name{"None"}, health{0}, xp{0} {
// ここでさらに初期化処理を行うことができます
}
Player::Player(std::string name_val)
: name{name_val}, health{0}, xp{0} {
}
Player::Player(std::string name_val, int health_val, int xp_val)
: name{name_val}, health{health_val}, xp{xp_val} {
}
これにより、オブジェクトは以下のように様々な方法で生成できます:
Player empty; // Player() が呼ばれる
Player hero{"Hero"}; // Player(std::string) が呼ばれる
Player villain{"Villain", 100, 55}; // Player(std::string, int, int) が呼ばれる
// ヒープ上での生成
Player *enemy = new Player("Enemy", 1000, 0);
delete enemy; // ヒープで確保したメモリは必ず解放する
デストラクタ:オブジェクトの「死」
オブジェクトのライフタイムが終了すると(ローカル変数の場合はスコープを抜けるとき、またはポインタの場合は delete を呼んだとき)、自動的にそのデストラクタが呼ばれます:
デストラクタはクラス名の前に**~**を付けた名前になります。
引数も返り値も持ちません。
デストラクタは1つだけ定義でき、オーバーロードはできません。
例えば:
class Player {
public:
~Player() {
std::cout << "Destructor called for " << name << std::endl;
}
};
デストラクタは通常、動的に確保されたメモリの解放、ファイルハンドルのクローズ、その他のリソースの解放に使用されます。ローカル変数に対してはデストラクタを明示的に呼び出す必要はなく、C++が自動的に呼び出します。動的に確保されたオブジェクトの場合は、delete を実行するとデストラクタが呼び出されます。
デフォルトコンストラクタ
もしどのコンストラクタも定義しなければ、C++は引数を取らないデフォルトコンストラクタを自動的に提供しますが、これは何もしません(メンバー変数の初期化は行われず、ゴミ値が入る可能性があります)。そのため、自分で引数なしコンストラクタを提供するのが最善です:
class Player {
private:
std::string name;
int health;
int xp;
public:
// 引数なしコンストラクタ
Player()
: name{"None"}, health{100}, xp{3} {
}
// ...
};
こうすることで、引数なしでオブジェクトを生成した場合でも、
Player hero; // 引数なしコンストラクタが呼ばれる
指定した初期状態でオブジェクトが生成され、ランダムな値ではなく確実な初期状態になります。
重要な点: 一度でも任意のコンストラクタ(例えば引数を取るコンストラクタ)を定義すると、C++は自動的なデフォルトコンストラクタの生成を停止します。もし引数なしでの生成が必要な場合は、必ず自分で定義する必要があります。
コンストラクタのオーバーロード
クラスは、パラメータの型、数、順序が異なれば、必要なだけ複数のコンストラクタを持つことができます。これをオーバーロードと呼びます:
class Dog {
private:
std::string name;
int age;
public:
// オーバーロードされたコンストラクタ
Dog() : name{"None"}, age{0} { }
Dog(std::string n, int a) : name{n}, age{a} { }
};
このようにすると、以下のように生成できます:
Dog dog1; // Dog() を使用
Dog dog2{"Fido", 4}; // Dog(std::string, int) を使用
オーバーロードはオブジェクトの初期化方法に柔軟性をもたらしますが、各コンストラクタのシグネチャが一意であることを必ず確認してください。C++は生成時にどのコンストラクタを呼ぶべきか明確に判定できる必要があります。
すべてを統合する
アクセス修飾子、ヘッダーファイルとソースファイルの分離、そして適切に設計されたコンストラクタ(場合によってはデストラクタを含む)を組み合わせることで、以下のようなクラスを作成できます:
内部の実装の詳細を隠蔽し、privateメンバーを通してのみアクセスさせる。
安全なパブリックインターフェースをメソッドとして提供する。
オブジェクトが確実な状態で初期化されるようにする。
オブジェクトのライフタイムの終了時にリソースを適切に解放する(特に動的メモリ、ファイルハンドルなど)。
この設計により、コードの保守性が大幅に向上し、クラスを個別にテストおよびデバッグしてから大規模なプロジェクトに統合することが容易になります。これらの技法が堅牢なC++アプリケーションを書く上でいかに重要であるかをすぐに実感するでしょう!
要点のまとめ:
public と private を使用して、外部コードがクラスデータにアクセスする方法を制御します。
メソッドは、クラス内で実装するか、ClassName::methodName を使用してクラス外で実装します。
コンストラクタを活用して、オブジェクトが適切に初期化されるようにします(引数なしの場合もオーバーロードされた場合も)。
デストラクタを使用して(特にポインタ、ファイルハンドルなどのクリーンアップに)リソースを解放します。
任意のコンストラクタを定義すると、コンパイラは自動的なデフォルトコンストラクタの生成を停止するため、必要であれば自分で定義します。
これらの機能を理解し組み合わせることで、クリーンでモジュラーかつ保守性の高いオブジェクト指向プログラムをC++で設計する道が大いに開かれます!