クラスの設計ではデータ抽象とカプセル化が基本原則です。
データ抽象はインターフェース(宣言)と実装(定義)を分離するプログラミング技法です。クラスデザイナーは、クラスの利用者向けにインターフェースと実装を設計します。ここでいう利用者はアプリケーションの最終ユーザーではなく、他のプログラマを指します。
メンバは必ずクラス内で宣言され、その定義はクラス内部または外部で可能です。constオブジェクト、const参照、constポインタを持つオブジェクトは、constメンバ関数のみを呼び出せます。
コンパイラはクラスを2段階で処理します:
- 最初にメンバ宣言をコンパイル
- その後メンバ関数本体を処理
クラス外部で定義されたメンバ名はクラス名を含める必要があります。
double 販売データ::平均価格() const {
if (販売数 > 0) {
return 総収入 / 販売数;
} else {
return 0.0;
}
}
「this」オブジェクトを返す関数の例:
販売データ& 販売データ::統合(const 販売データ& 右辺値) {
販売数 += 右辺値.販売数;
return *this; // 現在のオブジェクトを返す
}
左辺値を返すには参照を返す必要があります。インターフェースに属するメンバ関数は、クラス自身のヘッダファイル内で宣言するべきです。
std::istream& 読込(std::istream& 入力, 販売データ& 品目) {
入力 >> 品目.書籍番号 >> 品目.販売数;
return 入力;
}
std::ostream& 表示(std::ostream& 出力, const 販売データ& 品目) {
出力 << 品目.ISBN();
return 出力;
}
I/Oクラスはコピー不可であるため、参照渡しになります。ストリーム操作は状態を変えるためconst参照ではなく通常の参照を受け取ります。
販売データ 合計(const 販売データ& 左辺, const 販売データ& 右辺) {
販売データ 結果 = 左辺; // 左辺のメンバをコピー
結果.統合(右辺);
return 結果;
}
C++ではオブジェクトのコピーはメンバー単位で行われます。Javaとは異なり、オブジェクト参照ではなく実際のデータがコピーされます。
コンストラクタを明示的に宣言しない場合、コンパイラはデフォルトコンストラクタを自動生成します。動的メモリが必要なクラスはvectorやstringを使用することでメモリ管理を簡略化できます。
アクセス修飾子(private/public)は継続的に有効となり、次の修飾子またはクラス終了まで続きます。structとclassの唯一の違いはデフォルトアクセスレベルです。
フレンド機能は非公開メンバへのアクセスを許可します。
class 販売データ {
friend 販売データ 合計(const 販売データ&, const 販売データ&);
public:
販売データ() = default;
private:
std::string 書籍番号;
};
// インタフェースに属するメンバの宣言
販売データ 合計(const 販売データ&, const 販売データ&);
フレンド宣言はクラスの冒頭または末尾に集約するのが良い実践です。フレンド関数は通常、クラスと同じヘッダで宣言されます。
カプセル化の利点:
- ユーザコードによるオブジェクト状態の破壊を防ぐ
- 実装変更時のユーザコード変更を不要とする
クラス定義が変更されても、利用側のソースファイルは再コンパイルが必要です。小さな関数はインライン化することでパフォーマンスを向上させます。
mutable修飾子を持つメンバはconstオブジェクト内でも変更可能です。
class ディスプレイ {
public:
void アクセスカウンタ増加() const;
private:
mutable size_t アクセス回数;
};
void ディスプレイ::アクセスカウンタ増加() const {
アクセス回数++;
}
クラス内初期化子は=または{}で指定します。
class ウィンドウマネージャ {
private:
std::vector<ディスプレイ> ウィンドウリスト{ディスプレイ(40, 80, ' ')}; // デフォルトで1つの画面
}
*thisを返す例:
class ディスプレイ {
public:
ディスプレイ& 文字設定(char);
ディスプレイ& 位置移動(int, int);
};
inline ディスプレイ& ディスプレイ::位置移動(int x, int y) {
カーソル = x * 幅 + y;
return *this;
}
inline ディスプレイ& ディスプレイ::文字設定(char c) {
内容[カーソル] = c;
return *this;
}
連続呼び出しの例:
myDisplay.位置移動(4,0).文字設定('#');
これは以下と同等:
myDisplay.位置移動(4,0);
myDisplay.文字設定('#');
参照ではなく値を返す場合、動作が異なります。
ディスプレイ 一時 = myDisplay.位置移動(4,0); // コピーが発生
一時.文字設定('#'); // 元のmyDisplayは変化しない
クラスは自身の型をメンバとして持てませんが、ポインタや参照は可能です。
class リンク販売 {
ディスプレイ ウィンドウ;
リンク販売* 次;
リンク販売* 前;
};
フレンド関係は推移的ではありません。Window_mgrのフレンドはScreenに特別なアクセス権を持ちません。
オーバロード関数は個別にフレンド宣言が必要です。friend関数のルックアップ規則:
struct X {
friend void f();
X() { f(); } // エラー:f()が宣言されていない
void g();
void h();
};
void X::g() { return f(); } // エラー
void f(); // X()で定義される関数を宣言
void X::h() { return f(); } // OK
一部コンパイラはfriendのルックアップを厳密に検出しません。
class ウィンドウマネージャ {
public:
size_t 画面追加(const ディスプレイ&);
};
// 戻り値の型をWindow_mgrスコープで確認
size_t ウィンドウマネージャ::画面追加(const ディスプレイ& s) {
画面リスト.push_back(s);
return 画面リスト.size() - 1;
}
メンバ関数はクラス内のすべての宣言処理後に定義されるため、任意のメンバを参照可能です。
隠蔽されたメンバはthisポインタやクラス名でアクセス可能:
void ディスプレイ::ダミー関数(size_t 高さ) {
カーソル = 幅 * this->高さ;
// または
カーソル = 幅 * ディスプレイ::高さ;
}
グローバル変数へのアクセスはスコープ演算子で:
void ディスプレイ::ダミー関数(size_t 高さ) {
カーソル = 幅 * ::高さ; // グローバル変数
}
参考文献: C++ Primer 第5版 第7章 クラス