C++ クラス設計の基礎:定義からメモリ配置、this ポインタまで

クラス構造とメンバー定義

クラスを用いることで、関連するデータとその操作方法を一つの単位にまとめられます。これがオブジェクト指向プログラミングの基本的な構成要素となります。

1. クラスの構文と構成要素

class キーワードを使用してクラスを宣言します。{} の内部には属性(メンバ変数)と動作(メンバ関数)が記述されます。末尾のセミコロンは必須です。

#include<iostream>
using namespace std;

class BankAccount
{
public:
    void open(int initialAmount)
    {
        balance = initialAmount;
        accountID = 1001;
    }

    // メンバ変数の宣言
    int balance;
    int accountID;
};

慣習として、メンバ変数の先頭にアンダースコア _ を付けるか、m_ を付与して識別しやすくするのが一般的です。これは強制ルールではありませんが、可読性を高めるためのプラクティスです。

2. クラスと構造体の関係

C++ では struct も同様にクラスとして機能します。C言語との互換性を保つため、struct は構造体としての利用も可能ですが、機能拡張によりメソッド定義が可能になりました。ただし、デフォルトのアクセス制限において違いがあります。

struct Node
{
    // 構造体でもメソッド定義が可能
    void reset()
    {
        data = 0;
    }
    int data;
};

重要な差異: デフォルトアクセス限定詞として、class の場合メンバはすべて private 扱いとなり、struct の場合は public 扱いになります。セキュリティ意識の高い設計では、明示的に class を採用することが推奨されます。

3. アクセス限定とスコープ

データの隠蔽を実現するために、公開範囲を制御する指定子を使います。

  • public: クラス外部から直接アクセス可能。
  • private: クラス内部のメソッドからのみアクセス可能。
  • protected: 派生クラスの継承関係で特殊な振る舞いを持つ(ここでは詳細省略)。

メソッドの定義と宣言を分離する場合、二重コロン :: を用いてスコープを指定する必要があります。

class Transaction
{
public:
    void execute();
private:
    double amount;
};

// 実装時に所属クラスを明記
void Transaction::execute()
{
    amount = 10.50;
}

オブジェクトの生成とメモリ管理

1. インスタンス化の概念

クラス単体ではメモリ領域を確保していない「設計図」のような状態です。実際の問題解決で使用するためには、クラス型からメモリ上に実体のオブジェクトを作成する必要があります。これをインスタンス化と呼びます。

各オブジェクトは独立したメンバ変数のコピーを持ちます。

class Item
{
public:
    void setup(const char* name, int id)
    {
        strcpy_s(this->name, name);
        itemId = id;
    }
    void display()
    {
        cout << name << "," << itemId << endl;
    }
private:
    char name[20];
    int itemId;
};

int main()
{
    // メモリ確保が発生
    Item box1;
    Item box2;

    box1.setup("Apple", 1);
    box2.setup("Orange", 2);
    return 0;
}

2. オブジェクトサイズの計算

オブジェクトのサイズは主にメンバ変数が決定します。メンバ関数はコードセグメントに格納されるため、オブジェクトサイズには影響しません(関数ポインタも通常保持されません)。ただし、以下の規則が適用されます。

  1. 開始オフセットは 0 から始まる。
  2. 他の変数はメモリアライメント条件を満たす地址に配置される。
  3. 全体のサイズは最大のフィールドまたはコンパイラデフォルトのアライメント数の倍数となる。
  4. Vitual 関数がない場合、空のクラスでもサイズは 1 バイト(ゼロサイズを防ぐため)。

this ポインタの仕組み

静的関数を呼び出す際、どのオブジェクトの変数にアクセスするかを特定できない問題が存在します。これを解決するために C++ は暗黙的に this ポインタを用意しています。

1. 暗黙的な引数

メンバー関数の呼び出し時に、現在対象となっているオブジェクト自身のアドレスが第 1 引数として渡されます。例えば、上記の Item 型の setup メソッドは、実質的に次のような署名を持っています。

void Item::setup(Item* const this, const char* name, int id)

コード内で _name などの変数へ代入する際は、暗黙的に this->_name と解釈されます。

2. this ポインタの挙動例

明示的に this を使用することも可能です。ただし、ポインタ自体を変更することはできません(const 修飾されているため)。

void Item::setup(const char* name, int id)
{
    this->itemId = id;
    strcpy_s(this->name, name);
}

3. 注意すべきケース

nullptr に -> メンバーアクセスを行う場合、必ずしも即座にクラッシュとは限りません。関数の実体は既にコンパイル時にアドレス解決済みだからです。しかし、メンバ変数(データセグメント)へのアクセスは例外処理を引き起こす可能性があります。

class EmptyTest
{
public:
    void status()
    {
        cout << "status ok" << endl;
        // cout << val << endl; // ここで nullptr ならエラー発生
    }
    int val;
};

int main()
{
    EmptyTest* ptr = nullptr;
    ptr->status(); // コードとしては通る場合がある
    return 0;
}

関数呼び出し自体は、その命令ブロックのアドレスを参照するため安全性がありますが、実際のデータ参照には慎重になる必要があります。

タグ: C++ クラス,オブジェクト指向,メモリアライメント,this ポインタ,構造体

6月20日 20:40 投稿