第3章:クラスとオブジェクト
はじめに
クラスはC++の中核となる概念です。第1章で学んだように、クラスの概念はSimula(1967年)で生まれ、C++によってシステムプログラミングの世界に持ち込まれました。
本章では、クラスの設計背景から始め、コンストラクタ、デストラクタ、アクセス制御などの基本機能を学びます。
---
1. クラスの概念
1.1 構造体からクラスへ
C言語の構造体はデータをグループ化するだけでした:
/* C言語の構造体 */
struct Point {
int x;
int y;
};
/* 関連する関数は別に定義 */
void point_init(struct Point* p, int x, int y) {
p->x = x;
p->y = y;
}
double point_distance(struct Point* p1, struct Point* p2) {
int dx = p2->x - p1->x;
int dy = p2->y - p1->y;
return sqrt(dx*dx + dy*dy);
}
問題点:
- データと関数が分離している
- 初期化を忘れる可能性がある
- 内部状態を誰でも変更できる
1.2 カプセル化の概念
David Parnasは1972年の論文「On the Criteria To Be Used in Decomposing Systems into Modules」で情報隠蔽の重要性を説きました:
> "Each module should hide a design decision from the others." > > (各モジュールは設計上の決定を他のモジュールから隠すべきである)
クラスはこの原則を実現します:
class Point {
private:
int x; // 内部実装を隠蔽
int y;
public:
// 公開インターフェース
Point(int x, int y) : x(x), y(y) {}
int getX() const { return x; }
int getY() const { return y; }
double distanceTo(const Point& other) const {
int dx = other.x - x;
int dy = other.y - y;
return std::sqrt(dx*dx + dy*dy);
}
};
1.3 structとclassの違い
C++ではstructとclassはほぼ同じですが、デフォルトのアクセス制御が異なります:
struct S {
int x; // デフォルトでpublic
};
class C {
int x; // デフォルトでprivate
};
慣習として:
struct: データの集まり(Plain Old Data)class: 振る舞いを持つオブジェクト
---
2. アクセス制御
2.1 public, private, protected
class MyClass {
public:
// 誰からでもアクセス可能
void publicMethod();
protected:
// このクラスと派生クラスからのみアクセス可能
void protectedMethod();
private:
// このクラス内からのみアクセス可能
void privateMethod();
int privateData;
};
2.2 なぜアクセス制御が必要か
不変条件(invariant)の維持:
class BankAccount {
private:
double balance; // 残高は常に0以上であるべき
public:
BankAccount() : balance(0) {}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const { return balance; }
};
もしbalanceがpublicだったら:
account.balance = -1000; // 不正な状態になりうる
2.3 フレンド関数とフレンドクラス
特定の関数やクラスにプライベートメンバへのアクセスを許可できます:
class Vector {
private:
double x, y;
public:
Vector(double x, double y) : x(x), y(y) {}
// フレンド関数:ストリーム演算子
friend std::ostream& operator<<(std::ostream& os, const Vector& v);
// フレンドクラス
friend class VectorOperations;
};
std::ostream& operator<<(std::ostream& os, const Vector& v) {
return os << "(" << v.x << ", " << v.y << ")"; // privateにアクセス可能
}
注意: フレンドはカプセル化を弱めるため、必要最小限に留めるべきです。
---
3. コンストラクタ
3.1 コンストラクタの役割
コンストラクタは、オブジェクトが生成される際に自動的に呼ばれる特殊なメンバ関数です。
class Point {
private:
int x, y;
public:
// デフォルトコンストラクタ
Point() : x(0), y(0) {
std::cout << "Default constructor called" << std::endl;
}
// パラメータ付きコンストラクタ
Point(int x, int y) : x(x), y(y) {
std::cout << "Parameterized constructor called" << std::endl;
}
};
int main() {
Point p1; // デフォルトコンストラクタ
Point p2(10, 20); // パラメータ付きコンストラクタ
return 0;
}
3.2 初期化リスト
初期化リストを使うべき理由:
- const メンバの初期化
- 参照メンバの初期化
- 基底クラスの初期化
- 効率性
class Example {
private:
const int id; // constメンバ
int& ref; // 参照メンバ
std::string name;
public:
// 初期化リストが必須
Example(int id, int& r, const std::string& n)
: id(id), ref(r), name(n) {
// ここでは代入しかできない(初期化は終わっている)
}
// NG: 以下はコンパイルエラー
// Example(int id, int& r) {
// this->id = id; // constに代入できない
// this->ref = r; // 参照に代入できない
// }
};
3.3 初期化順序
メンバは宣言順に初期化されます(初期化リストの順序ではない):
class Order {
int a;
int b;
int c;
public:
// 初期化リストの順序に関わらず、a → b → c の順で初期化
Order() : c(3), a(1), b(2) {
// 実際は a=1, b=2, c=3 の順で初期化される
}
};
警告: 初期化リストの順序と宣言順序が異なると、コンパイラが警告を出します。
3.4 デフォルトコンストラクタの自動生成
コンストラクタを一つも定義しない場合、コンパイラがデフォルトコンストラクタを自動生成します:
class A {
int x; // コンストラクタなし
};
A a; // OK: 自動生成されたデフォルトコンストラクタが呼ばれる
class B {
int x;
public:
B(int x) : x(x) {} // パラメータ付きのみ定義
};
B b; // エラー: デフォルトコンストラクタがない
---
4. デストラクタ
4.1 デストラクタの役割
デストラクタは、オブジェクトが破棄される際に自動的に呼ばれます。
class Resource {
private:
int* data;
public:
Resource(int size) {
data = new int[size];
std::cout << "Resource acquired" << std::endl;
}
~Resource() {
delete[] data;
std::cout << "Resource released" << std::endl;
}
};
int main() {
{
Resource r(100); // コンストラクタ呼び出し
// ... 使用 ...
} // スコープ終了: デストラクタ自動呼び出し
return 0;
}
4.2 RAII(Resource Acquisition Is Initialization)
デストラクタを活用したリソース管理パターン:
class FileHandle {
private:
FILE* file;
public:
FileHandle(const char* filename) {
file = fopen(filename, "r");
if (!file) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandle() {
if (file) {
fclose(file); // 自動的にファイルを閉じる
}
}
// コピー禁止
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void processFile() {
FileHandle fh("data.txt");
// ... ファイル処理 ...
// 例外が発生しても、デストラクタでファイルは閉じられる
}
4.3 デストラクタの呼び出しタイミング
class Logger {
std::string name;
public:
Logger(const std::string& n) : name(n) {
std::cout << name << " constructed" << std::endl;
}
~Logger() {
std::cout << name << " destroyed" << std::endl;
}
};
Logger global("global"); // プログラム開始時に構築
int main() {
std::cout << "main start" << std::endl;
Logger local("local"); // ローカル変数
Logger* heap = new Logger("heap"); // ヒープ上
{
Logger block("block"); // ブロックスコープ
} // blockのデストラクタ呼び出し
delete heap; // 明示的に削除
// localのデストラクタはmain終了時
std::cout << "main end" << std::endl;
return 0;
}
// globalのデストラクタはプログラム終了時
出力:
global constructed
main start
local constructed
heap constructed
block constructed
block destroyed
heap destroyed
main end
local destroyed
global destroyed
---
5. thisポインタ
5.1 thisポインタとは
thisは、メンバ関数内で自分自身のオブジェクトを指すポインタです。
class Counter {
private:
int value;
public:
Counter(int value) {
this->value = value; // メンバ変数と引数を区別
}
Counter& increment() {
value++;
return *this; // 自分自身を返す
}
};
5.2 メソッドチェーン
thisを返すことで、メソッドチェーンが可能になります:
class Builder {
int a, b, c;
public:
Builder() : a(0), b(0), c(0) {}
Builder& setA(int val) { a = val; return *this; }
Builder& setB(int val) { b = val; return *this; }
Builder& setC(int val) { c = val; return *this; }
void print() const {
std::cout << a << ", " << b << ", " << c << std::endl;
}
};
int main() {
Builder b;
b.setA(1).setB(2).setC(3).print(); // 1, 2, 3
return 0;
}
---
6. staticメンバ
6.1 静的メンバ変数
クラスの全インスタンスで共有される変数:
class Counter {
private:
static int count; // 宣言のみ
int id;
public:
Counter() : id(++count) {
std::cout << "Object " << id << " created" << std::endl;
}
static int getCount() { return count; }
};
// 定義(クラス外で必要)
int Counter::count = 0;
int main() {
Counter c1; // Object 1 created
Counter c2; // Object 2 created
Counter c3; // Object 3 created
std::cout << "Total: " << Counter::getCount() << std::endl; // 3
return 0;
}
6.2 静的メンバ関数
インスタンスなしで呼び出せる関数:
class Math {
public:
static double PI() { return 3.14159265359; }
static double square(double x) { return x * x; }
static double abs(double x) { return x < 0 ? -x : x; }
};
int main() {
std::cout << Math::PI() << std::endl;
std::cout << Math::square(5) << std::endl; // 25
return 0;
}
注意: 静的メンバ関数内ではthisを使えません。
---
7. オブジェクトのライフサイクル
7.1 生成方法による違い
class Object {
public:
Object() { std::cout << "Constructed" << std::endl; }
~Object() { std::cout << "Destroyed" << std::endl; }
};
// 1. 自動変数(スタック上)
void automatic() {
Object obj; // スコープ終了時に自動破棄
}
// 2. 動的割り当て(ヒープ上)
void dynamic() {
Object* obj = new Object;
// ... 使用 ...
delete obj; // 明示的に削除が必要
}
// 3. 静的変数
void staticVar() {
static Object obj; // 最初の呼び出しで構築、プログラム終了時に破棄
}
7.2 一時オブジェクト
class Temp {
public:
Temp() { std::cout << "Temp created" << std::endl; }
~Temp() { std::cout << "Temp destroyed" << std::endl; }
};
Temp createTemp() {
return Temp(); // 一時オブジェクト
}
int main() {
createTemp(); // 作成され、すぐに破棄される
std::cout << "After function call" << std::endl;
return 0;
}
---
まとめ
本章で学んだこと:
- クラスの概念: データと振る舞いのカプセル化
- アクセス制御: public, private, protectedによる情報隠蔽
- コンストラクタ: オブジェクトの初期化、初期化リスト
- デストラクタ: リソースの解放、RAII
- thisポインタ: 自己参照、メソッドチェーン
- staticメンバ: クラス共有のデータと関数
- 以下のクラスに適切なコンストラクタとデストラクタを追加してください:
次章では、メモリ管理とRAIIについてさらに深く学びます。
---
練習問題
class DynamicArray {
private:
int* data;
size_t size;
public:
// コンストラクタ、デストラクタを追加
};
- 初期化リストを使うべき場合を3つ挙げてください。
- 以下のコードの出力を予測してください:
class Test {
public:
Test() { std::cout << "C"; }
~Test() { std::cout << "D"; }
};
int main() {
Test t1;
Test* t2 = new Test;
delete t2;
return 0;
}
- staticメンバ変数とstaticメンバ関数の違いを説明してください。