オブジェクト指向の核である「クラス」は、データとそれに対する処理をひとまとめにする仕組みです。
C++では高性能・強力な表現力のクラス機構が提供され、設計の意図をコードに落とし込みやすくなります。
本記事では、クラスの基本概念から構文、コンストラクタとデストラクタ、オブジェクト生成の方法、そして実践コードまで、基礎を丁寧に解説します。
C++クラスとは:データと処理をひとまとめにする基礎概念
クラスとオブジェクトの違い(設計図と実体)
クラスは「型の設計図」です。
どのようなデータを持ち、どのような操作ができるかを定義します。
オブジェクトは、その設計図から作られる「具体的な実体」です。
例えば「Point」というクラスは「x座標とy座標を持ち、移動や距離計算ができる」という設計図であり、Point p(3, 4);
はその具体的な点を表します。
カプセル化のメリットと用途
カプセル化は、データ(メンバ変数)と操作(メンバ関数)をまとめ、内部実装を隠す考え方です。
これにより、以下の利点が得られます。
- 不変条件の保持:外部からの直接代入を防ぎ、メソッド経由で正当性チェックを行えます。
- 実装の隠蔽:内部構造を変えても公開インターフェースが同じなら利用側は影響を受けません。
- 読みやすさと保守性:関連するデータと処理が近くにまとまるため、理解しやすくなります。
classの書き方:C++クラスの基本構文とメンバ定義
メンバ変数(フィールド)とメンバ関数(メソッド)
クラスはデータと処理を定義します。
以下は最小構成の例です。
// 最小限のクラス例
class Counter {
private: // 内部状態(外部から直接は触れさせない)
int value_;
public: // 外部に公開する操作
Counter() : value_(0) {} // デフォルトコンストラクタ
explicit Counter(int start) : value_(start) {} // 引数付きコンストラクタ
void increment() { ++value_; } // 状態を変更するメソッド
int value() const { return value_; } // 状態を取得するメソッド(読み取り専用)
};
private
なメンバ変数value_
は外から直接変更できません。
代わりに、increment
やvalue
といったメソッドを通して操作します。
アクセス指定子(public/private/protected)の使い分け
アクセス指定子は「どこからアクセスできるか」を表します。
基本は以下の方針で十分です。
- public: 外部に見せたい最小限のインターフェース(メソッド、定数、型定義など)
- private: すべての実装詳細(メンバ変数、ヘルパー関数)
- protected: 継承関係で子クラスからアクセスさせたい内部詳細(入門段階では多用しない)
次の表は概要です。
指定子 | クラス外から | 派生クラスから | 同一クラス内 |
---|---|---|---|
public | 可 | 可 | 可 |
protected | 不可 | 可 | 可 |
private | 不可 | 不可 | 可 |
宣言と定義の分離(ヘッダとソース)
C++では型の宣言をヘッダ(.hpp/.h)、実装をソース(.cpp)に分けるのが一般的です。
利用側はヘッダだけをインクルードし、実装の変更による再コンパイルの影響を抑えられます。
// 宣言
#pragma once
class Counter {
private:
int value_;
public:
Counter();
explicit Counter(int start);
void increment();
int value() const;
};
// 定義
#include "counter.hpp"
Counter::Counter() : value_(0) {}
Counter::Counter(int start) : value_(start) {}
void Counter::increment() { ++value_; }
int Counter::value() const { return value_; }
// 利用側
#include <iostream>
#include "counter.hpp"
int main() {
Counter c(10);
c.increment();
std::cout << c.value() << std::endl; // 11
}
11
コンストラクタとデストラクタ:初期化と後始末の基本
デフォルトコンストラクタ/引数付きコンストラクタの作り方
コンストラクタは「生成時の初期化」を担います。
デフォルト(引数なし)と引数付きの両方を用意することが多いです。
class Range {
private:
int begin_;
int end_;
public:
Range() : begin_(0), end_(0) {} // デフォルト
Range(int b, int e) : begin_(b), end_(e) {} // 引数付き
};
必要に応じて「委譲コンストラクタ」で初期化ロジックをまとめられます。
class Range {
private:
int begin_;
int end_;
public:
Range() : Range(0, 0) {} // 委譲:他のコンストラクタを呼ぶ
Range(int b, int e) : begin_(b), end_(e) {}
};
メンバ初期化子リストの書き方と注意点
メンバは「コンストラクタ本体が実行される前」に初期化子リストで初期化されます。
特に以下は初期化子リストが必須です。
- constメンバ
- 参照メンバ(T&)
- メンバが持つ型のコンストラクタに引数が必要な場合
注意点として、初期化の実行順序は「宣言順」です。
初期化子リストの並び順ではありません。
class Person {
private:
std::string name_; // 先に初期化される(宣言が先)
int age_; // 次に初期化される
public:
Person(std::string name, int age)
: age_(age), name_(std::move(name)) // 並びは逆でも、実際はname_→age_の順
{}
};
また、重いオブジェクトや親クラスの初期化にも初期化子リストを使います。
デストラクタの役割と自動後始末
デストラクタはオブジェクト破棄時の後始末を担います。
ファイルやソケット、メモリなど「外部資源」を所有するクラスでは、デストラクタで解放処理を必ず行います(RAII)。
#include <iostream>
class Tracer {
public:
Tracer() { std::cout << "constructed\n"; }
~Tracer() { std::cout << "destructed\n"; } // スコープから抜けると自動で呼ばれる
};
int main() {
std::cout << "enter scope\n";
{
Tracer t;
std::cout << "in scope\n";
} // ここで~Tracer()が呼ばれる
std::cout << "leave scope\n";
}
enter scope
constructed
in scope
destructed
leave scope
基底クラスをポインタ経由で多態的に扱う場合は、デストラクタをvirtual
にするのが安全です。
C++オブジェクトの生成方法:スタックとヒープ、各種初期化
自動記憶域(ローカル変数)でのオブジェクト生成
最も基本的で安全なのはローカル変数として生成する方法です。
スコープを抜けると自動的にデストラクタが呼ばれます。
void f() {
Counter c; // 自動記憶域(しばし「スタック」と表現)
c.increment(); // スコープの終端で自動破棄
}
動的確保(new/delete)とスマートポインタ(unique_ptr)
ヒープ領域に確保するにはnew
/delete
が使えますが、解放忘れや例外でのリークを招きやすいです。
C++11以降はスマートポインタを使うのが原則です。
#include <memory>
// 非推奨:raw new/delete(例としての記述)
Counter* raw = new Counter(42);
raw->increment();
delete raw; // 例外などで漏れやすい
// 推奨:unique_ptr(所有権が一意)
auto ptr = std::make_unique<Counter>(100);
ptr->increment(); // スコープを抜けると自動でdelete
std::unique_ptr
は所有権を一意に保ち、スコープ終了時に自動解放します。
共有所有権が必要ならstd::shared_ptr
もありますが、入門段階ではunique_ptr
を基本にすると安全です。
直接初期化・コピー初期化・統一初期化({})の違い
C++には複数の初期化記法があります。
形式 | 記法例 | 特徴・注意点 |
---|---|---|
直接初期化 | T a(arg); | 最も素直。テンプレート推論や明示コンストラクタと相性良い |
コピー初期化 | T a = arg; | 暗黙変換が関与。explicit コンストラクタは使われない |
統一(リスト) | T a{arg1, arg2}; | ナローイング禁止。std::initializer_list 優先のオーバロード解決に注意 |
値初期化 | T a{}; | 0初期化/デフォルト初期化に便利(未初期化を避けやすい) |
struct Vec2 {
double x, y;
};
Vec2 a1(1.0, 2.0); // 直接初期化
Vec2 a2 = Vec2(1.0, 2.0); // コピー初期化(実効的には同等の結果)
Vec2 a3{1.0, 2.0}; // 統一初期化(ナローイング禁止)
Vec2 a4{}; // 値初期化(0.0, 0.0で初期化)
サンプルコード:クラスを作ってオブジェクトを使う実践
例:Pointクラスの設計と実装
2次元の点を表すPoint
クラスをヘッダとソースに分けて実装します。
座標の取得/設定、平行移動、距離計算を提供します。
#pragma once
#include <cmath>
class Point {
private:
double x_;
double y_;
public:
// コンストラクタ
Point() noexcept; // (0,0) に初期化
Point(double x, double y) noexcept; // 座標を指定して初期化
// アクセサ(読み取り)
double x() const noexcept;
double y() const noexcept;
// 更新
void set_x(double x) noexcept;
void set_y(double y) noexcept;
void translate(double dx, double dy) noexcept; // 平行移動
// 2点間距離
double distance_to(const Point& other) const noexcept;
};
#include "point.hpp"
Point::Point() noexcept : x_(0.0), y_(0.0) {}
Point::Point(double x, double y) noexcept : x_(x), y_(y) {}
double Point::x() const noexcept { return x_; }
double Point::y() const noexcept { return y_; }
void Point::set_x(double x) noexcept { x_ = x; }
void Point::set_y(double y) noexcept { y_ = y; }
void Point::translate(double dx, double dy) noexcept {
x_ += dx;
y_ += dy;
}
double Point::distance_to(const Point& other) const noexcept {
const double dx = x_ - other.x_;
const double dy = y_ - other.y_;
return std::sqrt(dx*dx + dy*dy);
}
オブジェクト生成とメンバ関数の呼び出し
main.cpp
で実際にPoint
オブジェクトを生成し、メンバ関数を呼び出して動作を確認します。
ついでにスマートポインタも利用します。
#include <iostream>
#include <iomanip>
#include <memory>
#include "point.hpp"
int main() {
std::cout << std::fixed << std::setprecision(3);
Point origin; // デフォルト初期化(0,0)
Point p(3.0, 4.0); // 直接初期化
std::cout << "p=(" << p.x() << "," << p.y() << ")\n";
std::cout << "distance(p, origin)=" << p.distance_to(origin) << "\n"; // 5.000
origin.translate(1.5, -2.0); // 平行移動
std::cout << "origin moved to (" << origin.x() << "," << origin.y() << ")\n";
// スマートポインタで動的生成
auto up = std::make_unique<Point>(-1.0, 2.0);
std::cout << "distance(*up, origin)=" << up->distance_to(origin) << "\n";
// 初期化バリエーション
Point a(1.0, 2.0); // 直接初期化
Point b = Point(1.0, 2.0); // コピー初期化
Point c{1.0, 2.0}; // 統一初期化
std::cout << "a=(" << a.x() << "," << a.y() << "), "
<< "b=(" << b.x() << "," << b.y() << "), "
<< "c=(" << c.x() << "," << c.y() << ")\n";
return 0;
}
p=(3.000,4.000)
distance(p, origin)=5.000
origin moved to (1.500,-2.000)
distance(*up, origin)=4.719
a=(1.000,2.000), b=(1.000,2.000), c=(1.000,2.000)
状態の取得・更新と簡単なテスト
上記の出力から、座標の取得/更新(x()
, y()
, translate
)や距離計算(distance_to
)が正しく機能していることが確認できます。
Point
の内部表現(x_
, y_
)はprivate
で隠蔽され、整合性が保たれています。
よくあるつまずきとベストプラクティス(C++クラス/オブジェクト入門)
未初期化を防ぐ設計(必須メンバはコンストラクタで)
未初期化はバグの温床です。
必須メンバは必ずコンストラクタの初期化子リストで初期化し、デフォルトでは無効な状態が作れないように設計します。
引数が必須ならデフォルトコンストラクタを意図的に削除する(Class() = delete;
)選択も有効です。
公開インターフェースの最小化(publicは必要最小限)
public
に出すのは利用者が必要とする最小限の関数・型に留め、メンバ変数は原則private
にします。
内部実装は後から自由に変更できるようにしておくと拡張性が高まります。
new/deleteの直使用を避ける(RAIIとスマートポインタ)
new
/delete
の直使用は例外や分岐で漏れを生みやすいです。
所有権を型で表現するRAIIを徹底し、std::unique_ptr
やstd::vector
、std::string
などの所有コンテナ/スマートポインタを活用します。
これにより、スコープ終了時に自動で確実な後始末が行われます。
まとめ
本稿では、C++のクラスが「データと処理をひとまとめ」にする仕組みであることを確認し、基本構文、アクセス指定子、宣言と定義の分離、コンストラクタ/デストラクタ、そしてオブジェクトの生成方法(自動記憶域・ヒープ、各種初期化)を解説しました。
サンプルのPoint
クラスを通じて、カプセル化の利点やスマートポインタを使った安全なリソース管理も体験いただけたはずです。
まずは小さなクラスから着実に設計し、publicインターフェースを最小化しつつ、未初期化やメモリリークを防ぐRAIIの原則を意識して書き進めてみてください。
設計図(クラス)と実体(オブジェクト)を正しく使い分けられるようになると、コードは強く、読みやすく、保守しやすくなります。