C++入門:クラス(class)の作り方とオブジェクトの生成方法を基礎から解説【データと処理をひとまとめに】

オブジェクト指向の核である「クラス」は、データとそれに対する処理をひとまとめにする仕組みです。

C++では高性能・強力な表現力のクラス機構が提供され、設計の意図をコードに落とし込みやすくなります。

本記事では、クラスの基本概念から構文、コンストラクタとデストラクタ、オブジェクト生成の方法、そして実践コードまで、基礎を丁寧に解説します。

目次
  1. C++クラスとは:データと処理をひとまとめにする基礎概念
  2. classの書き方:C++クラスの基本構文とメンバ定義
  3. コンストラクタとデストラクタ:初期化と後始末の基本
  4. C++オブジェクトの生成方法:スタックとヒープ、各種初期化
  5. サンプルコード:クラスを作ってオブジェクトを使う実践
  6. よくあるつまずきとベストプラクティス(C++クラス/オブジェクト入門)
  7. まとめ

C++クラスとは:データと処理をひとまとめにする基礎概念

クラスとオブジェクトの違い(設計図と実体)

クラスは「型の設計図」です。

どのようなデータを持ち、どのような操作ができるかを定義します。

オブジェクトは、その設計図から作られる「具体的な実体」です。

例えば「Point」というクラスは「x座標とy座標を持ち、移動や距離計算ができる」という設計図であり、Point p(3, 4); はその具体的な点を表します。

カプセル化のメリットと用途

カプセル化は、データ(メンバ変数)と操作(メンバ関数)をまとめ、内部実装を隠す考え方です。

これにより、以下の利点が得られます。

  • 不変条件の保持:外部からの直接代入を防ぎ、メソッド経由で正当性チェックを行えます。
  • 実装の隠蔽:内部構造を変えても公開インターフェースが同じなら利用側は影響を受けません。
  • 読みやすさと保守性:関連するデータと処理が近くにまとまるため、理解しやすくなります。

classの書き方:C++クラスの基本構文とメンバ定義

メンバ変数(フィールド)とメンバ関数(メソッド)

クラスはデータと処理を定義します。

以下は最小構成の例です。

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_は外から直接変更できません。

代わりに、incrementvalueといったメソッドを通して操作します。

アクセス指定子(public/private/protected)の使い分け

アクセス指定子は「どこからアクセスできるか」を表します。

基本は以下の方針で十分です。

  • public: 外部に見せたい最小限のインターフェース(メソッド、定数、型定義など)
  • private: すべての実装詳細(メンバ変数、ヘルパー関数)
  • protected: 継承関係で子クラスからアクセスさせたい内部詳細(入門段階では多用しない)

次の表は概要です。

指定子クラス外から派生クラスから同一クラス内
public
protected不可
private不可不可

宣言と定義の分離(ヘッダとソース)

C++では型の宣言をヘッダ(.hpp/.h)、実装をソース(.cpp)に分けるのが一般的です。

利用側はヘッダだけをインクルードし、実装の変更による再コンパイルの影響を抑えられます。

C++counter.hpp
// 宣言
#pragma once

class Counter {
private:
    int value_;

public:
    Counter();
    explicit Counter(int start);
    void increment();
    int value() const;
};
C++counter.cpp
// 定義
#include "counter.hpp"

Counter::Counter() : value_(0) {}
Counter::Counter(int start) : value_(start) {}
void Counter::increment() { ++value_; }
int Counter::value() const { return value_; }
C++use_counter.cpp
// 利用側
#include <iostream>
#include "counter.hpp"

int main() {
    Counter c(10);
    c.increment();
    std::cout << c.value() << std::endl; // 11
}
実行結果
11

コンストラクタとデストラクタ:初期化と後始末の基本

デフォルトコンストラクタ/引数付きコンストラクタの作り方

コンストラクタは「生成時の初期化」を担います。

デフォルト(引数なし)と引数付きの両方を用意することが多いです。

C++
class Range {
private:
    int begin_;
    int end_;

public:
    Range() : begin_(0), end_(0) {}                 // デフォルト
    Range(int b, int e) : begin_(b), end_(e) {}     // 引数付き
};

必要に応じて「委譲コンストラクタ」で初期化ロジックをまとめられます。

C++
class Range {
private:
    int begin_;
    int end_;

public:
    Range() : Range(0, 0) {}                // 委譲:他のコンストラクタを呼ぶ
    Range(int b, int e) : begin_(b), end_(e) {}
};

メンバ初期化子リストの書き方と注意点

メンバは「コンストラクタ本体が実行される前」に初期化子リストで初期化されます。

特に以下は初期化子リストが必須です。

  • constメンバ
  • 参照メンバ(T&)
  • メンバが持つ型のコンストラクタに引数が必要な場合

注意点として、初期化の実行順序は「宣言順」です。

初期化子リストの並び順ではありません。

C++
class Person {
private:
    std::string name_;  // 先に初期化される(宣言が先)
    int age_;           // 次に初期化される

public:
    Person(std::string name, int age)
        : age_(age), name_(std::move(name)) // 並びは逆でも、実際はname_→age_の順
    {}
};

また、重いオブジェクトや親クラスの初期化にも初期化子リストを使います。

デストラクタの役割と自動後始末

デストラクタはオブジェクト破棄時の後始末を担います。

ファイルやソケット、メモリなど「外部資源」を所有するクラスでは、デストラクタで解放処理を必ず行います(RAII)。

C++
#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++オブジェクトの生成方法:スタックとヒープ、各種初期化

自動記憶域(ローカル変数)でのオブジェクト生成

最も基本的で安全なのはローカル変数として生成する方法です。

スコープを抜けると自動的にデストラクタが呼ばれます。

C++
void f() {
    Counter c;         // 自動記憶域(しばし「スタック」と表現)
    c.increment();     // スコープの終端で自動破棄
}

動的確保(new/delete)とスマートポインタ(unique_ptr)

ヒープ領域に確保するにはnew/deleteが使えますが、解放忘れや例外でのリークを招きやすいです。

C++11以降はスマートポインタを使うのが原則です。

C++
#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初期化/デフォルト初期化に便利(未初期化を避けやすい)
C++
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クラスをヘッダとソースに分けて実装します。

座標の取得/設定、平行移動、距離計算を提供します。

C++point.hpp
#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;
};
C++point.cpp
#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オブジェクトを生成し、メンバ関数を呼び出して動作を確認します。

ついでにスマートポインタも利用します。

C++main.cpp
#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_ptrstd::vectorstd::stringなどの所有コンテナ/スマートポインタを活用します。

これにより、スコープ終了時に自動で確実な後始末が行われます。

まとめ

本稿では、C++のクラスが「データと処理をひとまとめ」にする仕組みであることを確認し、基本構文、アクセス指定子、宣言と定義の分離、コンストラクタ/デストラクタ、そしてオブジェクトの生成方法(自動記憶域・ヒープ、各種初期化)を解説しました。

サンプルのPointクラスを通じて、カプセル化の利点やスマートポインタを使った安全なリソース管理も体験いただけたはずです。

まずは小さなクラスから着実に設計し、publicインターフェースを最小化しつつ、未初期化やメモリリークを防ぐRAIIの原則を意識して書き進めてみてください。

設計図(クラス)と実体(オブジェクト)を正しく使い分けられるようになると、コードは強く、読みやすく、保守しやすくなります。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!