プロシージャルプログラミングとオブジェクト指向の違い
C言語はプロシージャル(手続き型)プログラミングを採用しており、問題解決の流れを関数の呼び出しで表現します。一方、C++はオブジェクト指向プログラミング(OOP)に基づいており、現実世界の「物事」をモデル化したオブジェクトとして捉え、それらの相互作用によって処理を完遂します。
クラスの基本構文
C++では、classキーワードを使ってクラスを定義できます。構造体(struct)も同様にメンバ変数やメンバ関数を持てますが、デフォルトのアクセス権限が異なります。
class MyClass {
private:
int value;
public:
void setValue(int v);
int getValue() const;
};
上記のように、クラスはデータとそれを操作する関数を一つの単位としてまとめます。メンバ関数の実装はヘッダファイル(.h)に宣言、ソースファイル(.cpp)に定義を分けるのが一般的です。
アクセス制御とカプセル化
C++のクラスには以下の3つのアクセス指定子があります:
- public:外部から自由にアクセス可能
- protected:派生クラスからのみアクセス可
- private:クラス内部からのみアクセス可
デフォルトでは、classはprivate、structはpublicとなります。これはCとの互換性のためです。
カプセル化とは、内部の実装を隠蔽し、外部には必要なインターフェースのみを公開する設計思想です。これにより、ユーザーは複雑な内部構造を知らなくてもクラスを使いこなせます。
オブジェクトのインスタンス化とメモリ配置
クラス自体は設計図であり、実際のメモリ領域はインスタンス化されたオブジェクトに割り当てられます。たとえば:
MyClass obj; // objがスタック上に生成される
オブジェクトのサイズはメンバ変数の合計に、アラインメント調整を加えたものになります。メンバ関数はコードセグメントに1つだけ存在し、すべてのオブジェクトが共有します。
thisポインタの仕組み
非静的メンバ関数は暗黙的にthisポインタを受け取ります。thisは現在のオブジェクトのアドレスを指すconstポインタで、関数内でのメンバアクセスはすべてthis->経由で行われます。
void MyClass::setValue(int v) {
this->value = v; // 'this->'は省略可能
}
thisはレジスタ(通常はecx)を通じて渡され、オブジェクト自身の一部ではありません。
特殊メンバ関数:6大関数
空のクラスであっても、コンパイラは以下の6つの特殊メンバ関数を自動生成します:
- デフォルトコンストラクタ
- デストラクタ
- コピーコンストラクタ
- ムーブコンストラクタ
- コピー代入演算子
- ムーブ代入演算子
コンストラクタ
オブジェクト生成時に自動呼び出され、初期化を行います。名前はクラス名と同じで、戻り値はありません。引数なしまたはすべての引数にデフォルト値がある場合、そのコンストラクタは「デフォルトコンストラクタ」と見なされます。
MyClass::MyClass() : value(0) {} // 初期化リスト使用
デストラクタ
オブジェクトの寿命が尽きたときに自動実行され、リソースの解放を行います。名前は~ClassName()です。動的メモリを扱うクラスでは明示的な定義が必須です。
コピーコンストラクタ
既存のオブジェクトから新しいオブジェクトを生成する際に使われます。引数は自分自身のconst参照でなければなりません。
MyClass(const MyClass& other) : value(other.value) {}
値渡しにすると無限再帰が発生するため、参照渡しが必須です。
演算子のオーバーロード
C++では、operatorキーワードを使って演算子の動作をカスタマイズできます。例えば代入演算子は次のように定義します:
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 自己代入チェック
value = other.value;
}
return *this;
}
このようにして、連鎖代入(a = b = c)をサポートできます。
constメンバ関数
メンバ関数の末尾にconstを付けることで、「この関数はオブジェクトの状態を変更しない」ことを宣言できます。このような関数はconstオブジェクトからも呼び出せます。
int getValue() const { return value; }
逆に、const関数内では非constメンバ関数を呼び出すことはできません。
静的メンバ
staticキーワードで宣言されたメンバは、すべてのインスタンス間で共有されます。静的メンバ変数はクラス外で定義・初期化が必要です。
// ヘッダ内
static int count;
// ソースファイル内
int MyClass::count = 0;
静的メンバ関数はthisポインタを持たず、非staticメンバに直接アクセスできません。
フレンド関数とフレンドクラス
カプセル化を一時的に解除したい場合、friendキーワードを使用できます。これにより、外部関数や他のクラスがprivateメンバにアクセス可能になります。
class AnotherClass {
friend void helper(MyClass& obj); // フレンド関数
friend class UtilityClass; // フレンドクラス
};
ただし、過剰な使用は結合度を高め、保守性を損なうため注意が必要です。
ネストされたクラス(内部クラス)
クラス内に定義されたクラスは「内部クラス」と呼ばれ、外部クラスの非publicメンバにアクセス可能です。しかし、逆は成り立ちません。
class Outer {
static int shared;
public:
class Inner {
public:
void access() { shared = 100; } // OK: staticメンバへアクセス
};
};
内部クラスのサイズは外部クラスのサイズに含まれません。
初期化リストとexplicitキーワード
コンストラクタ内ではなく、初期化リストでメンバを初期化すべきです。特に以下の場合必須です:
- const変数
- 参照型変数
- デフォルトコンストラクタのないクラス型メンバ
また、explicitをコンストラクタに付けることで、意図しない暗黙の型変換を防げます。
explicit MyClass(int v);
コンパイラ最適化の影響
近年のコンパイラは、不要なコピーを削減するために「コピーエリミネーション」などの最適化を行います。たとえば、関数の戻り値がローカルオブジェクトでも、ムーブやインライン展開によってパフォーマンスが大幅に向上します。