読み込み中...

C++でクラスをマスターする10の実例付き解説

C++のクラス定義を学ぶための完全ガイドのイメージ C++
この記事は約13分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C++におけるクラスを学ぶことは、プログラミングの世界での大きな一歩です。

この記事では、初心者から上級者まで、C++でのクラスの定義と利用方法を徹底的に解説していきます。

クラスとは何か、その基本的な構造、そして宣言と定義の方法まで、段階を追って理解を深めていきましょう。

こko

では、C++のクラスについて基本的な理解を深めることを目的としています。

プログラミング経験が少ない方でも理解しやすいように、基本から丁寧に解説していきます。

●C++クラスの基本

C++では、クラスはオブジェクト指向プログラミングの基本的な構成要素の一つです。

クラスを理解することは、C++で効率的かつ効果的なプログラミングを行う上で非常に重要です。

クラスはデータとそれを操作する関数をカプセル化することにより、コードの再利用性を高め、メンテナンスを容易にします。

ここでは、クラスの基本概念について詳しく見ていきましょう。

○クラスとは何か?

クラスとは、オブジェクトの設計図とも言えます。

クラスにはデータを保持するための変数(メンバ変数)と、そのデータを操作するための関数(メンバ関数)が含まれています。

オブジェクト指向プログラミングでは、このクラスを基にオブジェクト(インスタンス)を生成し、プログラム内で様々な操作を行います。

クラスを用いることで、データとそのデータに対する操作を一まとめにし、コードの可読性と再利用性を向上させることができます。

○クラスの基本的な構造

C++におけるクラスの基本的な構造は、クラス名、メンバ変数、メンバ関数から構成されます。

クラス名はそのクラスを識別するための名前であり、メンバ変数はそのクラスが保持するデータを表します。

メンバ関数はメンバ変数を操作したり、クラスに関連する処理を行うための関数です。

クラスは通常、ヘッダーファイル(.hまたは.hpp)に宣言され、ソースファイル(.cpp)にその実装が記述されます。

○クラスの宣言と定義

クラスの宣言とは、クラスの構造を定義することです。

クラス宣言には通常、クラス名、メンバ変数のリスト、メンバ関数のプロトタイプが含まれます。

一方、クラスの定義とは、宣言されたメンバ関数の具体的な処理内容を記述することです。

宣言はクラスのインターフェースを定義し、定義はそのインターフェースの実装を提供します。

クラスを使用するには、まずクラスを宣言し、その後でメンバ関数の定義を行う必要があります。

●C++クラスの定義方法

C++でクラスを定義するには、いくつかの基本的なステップがあります。

まず、クラスの名前を決め、その名前を用いてクラスを宣言します。その後、クラス内にメンバ変数とメンバ関数を定義します。

メンバ変数は、クラスが保持するデータを表し、メンバ関数は、そのデータに対する操作を定義します。

クラスの定義には、アクセス修飾子を用いてメンバのアクセスレベルを制御することも重要です。

ここでは、C++でのクラスの基本的な定義方法について詳細に解説していきます。

○サンプルコード1:基本的なクラスの定義

ここでは、C++で最も基本的なクラスの定義方法を見ていきます。

下記のサンプルコードは、単純なクラス「Car」を定義しています。

このクラスには、車の速度を表すメンバ変数「speed」と、速度を設定するメンバ関数「setSpeed」があります。

class Car {
public:
    int speed;

    void setSpeed(int s) {
        speed = s;
    }
};

この例では、Car クラスに public アクセス修飾子を使用しています。

これにより、クラスの外部から speed 変数にアクセスしたり、setSpeed 関数を呼び出したりすることができます。

○サンプルコード2:コンストラクタとデストラクタ

クラスには、コンストラクタとデストラクタという特別な関数を定義することができます。

コンストラクタは、クラスのオブジェクトが作成されるときに自動的に呼び出される関数で、オブジェクトの初期化に使用されます。

デストラクタは、オブジェクトが破棄されるときに呼び出され、必要なクリーンアップ作業を行います。

class Car {
public:
    int speed;

    Car() {
        speed = 0;
    }

    ~Car() {
        // リソース解放などのクリーンアップコード
    }
};

このサンプルコードでは、Car クラスにデフォルトコンストラクタとデストラクタを定義しています。

デフォルトコンストラクタは、speed を 0 に初期化しています。

○サンプルコード3:メンバ関数とメンバ変数

クラス内で定義されるメンバ関数は、そのクラスのメンバ変数にアクセスして操作を行うことができます。

下記のサンプルコードでは、メンバ関数 drivestop を追加しています。

class Car {
public:
    int speed;

    void setSpeed(int s) {
        speed = s;
    }

    void drive() {
        speed = 60;
    }

    void stop() {
        speed = 0;
    }
};

drive 関数は speed を 60 に設定し、stop 関数は speed を 0 に設定します。

これにより、Car クラスのオブジェクトで簡単な運転と停止の操作ができるようになります。

○サンプルコード4:アクセス修飾子の使用

アクセス修飾子は、クラスのメンバに対するアクセスレベルを制御するために使用されます。

C++には publicprivateprotected の3種類のアクセス修飾子があります。

public 修飾子は、そのメンバがクラスの外部からアクセス可能であることを表します。

private 修飾子は、メンバがクラス内部からのみアクセス可能であることを表し、protected 修飾子は、そのクラスおよび派生クラスからのみアクセス可能であることを表します。

下記のサンプルコードでは、speed 変数を private とし、外部から直接アクセスできないようにしています。

class Car {
private:
    int speed;

public:
    void setSpeed(int s) {
        if (s >= 0) {
            speed = s;
        }
    }

    int getSpeed() {
        return speed;
    }
};

このコードでは、setSpeed 関数を通じてのみ speed 変数に安全に値を設定でき、getSpeed 関数を使用してその値を取得できます。

これにより、Car クラスのオブジェクトの状態を適切に管理することが可能になります。

●クラスの応用

C++のクラスを応用することで、より高度なプログラミングが可能になります。

ここでは、クラスの応用として特に重要な継承、ポリモーフィズム、抽象クラスとインターフェイスについて説明します。

これらの概念を理解し適切に使いこなすことで、C++プログラミングの幅が大きく広がります。

○サンプルコード5:継承の基礎

継承は、既存のクラス(基底クラス)のプロパティやメソッドを新しいクラス(派生クラス)が受け継ぐ機能です。

これにより、コードの再利用性が向上し、クラス階層を通じた関連性を表現できます。

下記のサンプルコードでは、Vehicleクラスを基底クラスとして、Carクラスがこれを継承しています。

class Vehicle {
public:
    void move() {
        // 移動の基本的な実装
    }
};

class Car : public Vehicle {
public:
    void move() {
        // 車特有の移動の実装
    }
};

この例では、CarクラスはVehicleクラスからmoveメソッドを継承しています。

Carクラスはmoveメソッドをオーバーライドし、車特有の移動の実装を提供します。

○サンプルコード6:ポリモーフィズムの実装

ポリモーフィズムは、異なるクラスのオブジェクトが同じインターフェースを共有することを可能にします。

これにより、異なる型のオブジェクトを同じ方法で操作できるようになります。

下記のサンプルコードでは、Vehicleクラスに仮想関数moveを定義し、CarクラスとBikeクラスでこれをオーバーライドしています。

class Vehicle {
public:
    virtual void move() = 0; // 純粋仮想関数
};

class Car : public Vehicle {
public:
    void move() override {
        // 車の移動の実装
    }
};

class Bike : public Vehicle {
public:
    void move() override {
        // 自転車の移動の実装
    }
};

この例では、Vehicleクラスは抽象クラスであり、CarクラスとBikeクラスはそれぞれ異なる方法でmoveメソッドを実装しています。

○サンプルコード7:抽象クラスとインターフェイス

抽象クラスとは、インスタンス化できないクラスのことで、一つ以上の純粋仮想関数を持ちます。

インターフェイスは、特定のメソッドを提供することを約束する役割を持ちます。

下記のサンプルコードでは、IDrawableというインターフェイスを定義し、これを実装するクラスを表しています。

class IDrawable {
public:
    virtual void draw() = 0; // 純粋仮想関数
};

class Circle : public IDrawable {
public:
    void draw() override {
        // 円を描画する実装
    }
};

class Rectangle : public IDrawable {
public:
    void draw() override {
        // 四角形を描画する実装
    }
};

この例では、IDrawableインターフェイスはdrawメソッドを持ち、CircleクラスとRectangleクラスはこれを異なる方法で実装しています。

これにより、異なる形状の描画方法を一つのインターフェイスで表現しています。

●クラスのカスタマイズ

C++におけるクラスのカスタマイズは、より高度なプログラミングを可能にします。

オペレータオーバーロード、テンプレートクラス、例外処理といった機能を利用することで、クラスはより柔軟かつ強力なツールとなります。

これらの機能を理解し活用することで、C++プログラミングの可能性が大きく広がります。

○サンプルコード8:オペレータオーバーロード

オペレータオーバーロードにより、標準的なオペレータ(例えば + や -)をクラスに対してカスタムの振る舞いを持たせることができます。

下記のサンプルコードでは、Vector2Dクラスに対して加算オペレータをオーバーロードしています。

class Vector2D {
public:
    int x, y;

    Vector2D(int x, int y) : x(x), y(y) {}

    Vector2D operator+(const Vector2D& rhs) const {
        return Vector2D(x + rhs.x, y + rhs.y);
    }
};

このコードでは、二つのVector2Dオブジェクトを加算するためのオペレータ+が定義されています。

これにより、Vector2Dオブジェクト同士の加算が直感的な方法で行えるようになります。

○サンプルコード9:テンプレートクラス

テンプレートクラスを使用すると、さまざまなデータ型で再利用可能なクラスを作成できます。

下記のサンプルコードでは、任意の型の要素を保持できる汎用的なBoxクラスを定義しています。

template <typename T>
class Box {
private:
    T contents;

public:
    Box(T newContents) : contents(newContents) {}

    T getContents() {
        return contents;
    }
};

このBoxクラスは、intdoublestringなど、どのような型のオブジェクトでも保持することができます。

これにより、汎用性の高いコードの記述が可能になります。

○サンプルコード10:例外処理とクラス

C++では、例外処理を使用してエラーに対処することが推奨されています。

クラス内で例外を投げる(throw)ことにより、エラーを呼び出し元に通知し、適切に処理することができます。

下記のサンプルコードでは、エラーが発生する可能性のあるdivide関数を表しています。

class Calculator {
public:
    double divide(double a, double b) {
        if (b == 0) {
            throw std::invalid_argument("Division by zero");
        }
        return a / b;
    }
};

この例では、0で割る場合にstd::invalid_argument例外を投げます。

これにより、呼び出し元はtryブロックとcatchブロックを使用して、この例外をキャッチし適切に処理することができます。

●注意点と対処法

C++でクラスを扱う際、特に注意すべき点はメモリ管理の重要性とクラス設計のベストプラクティスです。

これらを理解し適切に対応することで、効率的で安全なプログラミングが可能になります。

メモリ管理では、動的メモリの適切な管理が重要であり、クラス設計では、単一責任原則、カプセル化、コンポジションの利用、明確なインターフェースの提供などが求められます。

これらの原則を守ることで、メモリリークやその他のリソース関連の問題を防ぎ、読みやすく保守しやすい、そして拡張可能なクラスを設計することができます。

○メモリ管理の重要性

C++におけるメモリ管理は、プログラムの安定性と効率性に大きく影響します。動的メモリの確保と解放は、特に注意が必要です。

メモリリークを避けるためには、コンストラクタで確保されたメモリはデストラクタで必ず解放することが重要です。

また、コピーコンストラクタと代入演算子の適切な実装により、ディープコピーを行うことで、予期せぬ問題を防ぐことができます。

さらに、RAII(Resource Acquisition Is Initialization)原則を適用することで、リソースの確保と解放をオブジェクトのライフサイクルに結びつけ、より安全なコードの実現が可能になります。

○クラス設計のベストプラクティス

クラスを設計する際には、いくつかのベストプラクティスを心掛けることが推奨されます。

単一責任原則に基づき、クラスは一つの機能のみを持つべきであり、複数の責任を持つクラスは避けるべきです。

カプセル化を通じてクラスの内部実装を隠蔽し、外部からの直接的なアクセスを制限することで、クラスの利用者は内部の複雑さを気にせずに済みます。

また、継承よりもコンポジションを優先し、クラス間の依存関係を減らすことで、より柔軟な設計を可能にします。

最後に、クラスの公開インターフェースは明確で一貫性を持つべきであり、他の開発者がクラスを簡単に理解し使用できるようにすることが重要です。

これらの原則を適用することで、保守性と拡張性の高いクラス設計が実現できます。

まとめ

この記事を通じて、C++におけるクラスの定義から応用、さらにはカスタマイズに至るまでの幅広い側面を詳細に解説しました。

メモリ管理やクラス設計のベストプラクティスについての知識は、効率的で安全なC++プログラミングのために不可欠です。

この記事が、C++におけるクラスの深い理解と実践的な使用に役立つことを願っています。