【C++】コンストラクタと初期化の全解説!5つの実例付属で完全網羅

C++におけるコンストラクタと初期化を解説する画像C++
この記事は約19分で読めます。

 

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

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

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

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

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

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

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

はじめに

この記事では、C++におけるコンストラクタと初期化の全面的な解説を行います。

初心者から上級者まで、C++の深い理解を目指す方々にとって価値ある情報を提供することを目的としています。

コンストラクタと初期化は、C++でのプログラミングにおいて非常に重要な概念です。

この記事を読むことで、C++の基本的な使い方から応用的なテクニックまで、幅広い知識を身につけることができます。

●C++とは

C++は、システムプログラミングやアプリケーション開発に広く使用されるプログラミング言語です。

C言語をベースにオブジェクト指向の概念が導入され、効率的で柔軟なプログラミングが可能になっています。

C++は、高いパフォーマンスを持ちつつも、抽象化のレベルを自在に調節できるため、多くのプログラマーに愛用されています。

また、標準テンプレートライブラリ(STL)などの豊富なライブラリも特徴の一つです。

○C++の基本概念

C++を理解する上で重要なのは、オブジェクト指向プログラミング(OOP)の基本概念です。

クラスとオブジェクト、継承、多態性、カプセル化などの概念は、C++の強力な機能を活用する上で欠かせません。

特に、クラスの設計とそれに伴うオブジェクトの振る舞いは、C++プログラミングの中心をなす部分です。

○コンストラクタと初期化の役割

コンストラクタは、オブジェクトが作成される際に自動的に呼び出される特殊な関数です。

オブジェクトの初期化を行い、リソースの割り当てや初期設定を行います。

初期化とは、変数やオブジェクトに最初の値を設定することを指します。

C++において、コンストラクタと初期化はオブジェクトの状態を安全かつ効率的に管理するために不可欠です。

これらを適切に理解し、使いこなすことが、C++での効果的なプログラミングへの第一歩と言えるでしょう。

●コンストラクタの基本

コンストラクタとは、C++においてクラスのインスタンスが生成される際に自動的に呼び出される特別なメンバ関数です。

この関数の主な役割は、オブジェクトの初期化です。

コンストラクタは、そのクラスのオブジェクトが作成されるたびに、オブジェクトの状態を適切に設定することで、安全かつ効率的なプログラミングを支援します。

コンストラクタは、通常、クラス名と同じ名前を持ち、戻り値を持ちません。

このため、特定の型の値を返す通常の関数とは異なり、オブジェクトの初期化に専念します。

また、コンストラクタは複数定義することができ、これを「オーバーロード」と言います。

○デフォルトコンストラクタとは

デフォルトコンストラクタは、引数を取らないコンストラクタです。

クラスにコンストラクタが一つも定義されていない場合、C++コンパイラはデフォルトコンストラクタを自動的に生成します。

このコンストラクタは、メンバ変数を初期化しないか、デフォルト値で初期化します。

例えば、下記のクラスMyClassにはデフォルトコンストラクタが含まれています。

class MyClass {
public:
    MyClass() {
        // ここで初期化処理を行う
    }
};

このコンストラクタは、MyClassのオブジェクトが生成される際に自動的に呼び出されます。

○明示的なコンストラクタの作成

明示的なコンストラクタは、開発者が特定の初期化処理を定義するために使用します。

これにより、オブジェクトが特定の状態で始まることを保証できます。

引数を取るコンストラクタは、オーバーロードされたコンストラクタの一例です。

class MyClass {
    int x;
public:
    MyClass(int value) : x(value) {
        // ここでxをvalueで初期化
    }
};

この例では、MyClassオブジェクトが生成される際に整数値を受け取り、メンバ変数xをその値で初期化します。

○コンストラクタのオーバーロード

コンストラクタのオーバーロードは、同じクラス内に複数のコンストラクタを定義することを指します。

これにより、異なる初期化オプションを提供することができます。

引数の型や数に応じて、適切なコンストラクタが呼び出されます。

下記の例では、2つのコンストラクタをオーバーロードしています。

class MyClass {
    int x, y;
public:
    MyClass() : x(0), y(0) {
        // 引数なしで両方のメンバを0で初期化
    }

    MyClass(int val) : x(val), y(val) {
        // 引数ありで両方のメンバをvalで初期化
    }
};

このクラスでは、引数がない場合は両方のメンバを0で初期化し、整数値が与えられた場合はその値で両方のメンバを初期化します。

●初期化の方法

C++における初期化の方法は、オブジェクトの安定した状態を保証し、エラーを未然に防ぐために重要です。

初期化は、オブジェクトが使用される前に、そのデータメンバに適切な値を設定するプロセスを指します。

ここでは、C++で一般的に用いられる初期化の技法について、詳しく解説していきます。

○初期化リストを使った初期化

初期化リストは、コンストラクタの本体が実行される前にメンバ変数を初期化する方法です。

この方法を用いると、メンバ変数の初期化をより効率的に、そして明確に行うことができます。

初期化リストは、コンストラクタの後にコロン(:)を付け、初期化するメンバとその値を記述します。

例として、下記のクラスでは2つのメンバ変数を初期化リストで初期化しています。

class MyClass {
    int x, y;
public:
    MyClass(int a, int b) : x(a), y(b) {
        // xとyはここで既にaとbで初期化されている
    }
};

この例では、MyClassオブジェクトのxyが、それぞれabの値で初期化されます。

○コンストラクタ内での初期化

コンストラクタ内での初期化は、コンストラクタの本体内でメンバ変数に値を代入する方法です。

この方法は、初期化リストを使うより直感的であり、複雑な初期化ロジックを必要とする場合に適しています。

class MyClass {
    int x, y;
public:
    MyClass(int a, int b) {
        x = a; // 代入を使ってxを初期化
        y = b; // 代入を使ってyを初期化
    }
};

この方法では、xyはコンストラクタの本体内でabの値に設定されます。

○ユニフォーム初期化(C++11以降)

C++11から導入されたユニフォーム初期化は、さまざまな初期化シナリオに対して一貫した構文を提供します。

この方法では、中括弧 {} を使用して初期化を行います。

ユニフォーム初期化は、基本型、クラス型、配列、コンテナなど、ほとんどすべての型の初期化に使うことができます。

class MyClass {
    int x, y;
public:
    MyClass(int a, int b) : x{a}, y{b} {
        // 中括弧を使用したユニフォーム初期化
    }
};

int main() {
    MyClass obj{5, 10}; // オブジェクトの初期化もユニフォーム初期化を使用
}

この例では、MyClassのコンストラクタとmain関数内のオブジェクトの初期化に、中括弧を用いたユニフォーム初期化が使用されています。

●コンストラクタの応用例

C++プログラミングでは、コンストラクタの応用が重要な役割を果たします。

これにはコピーコンストラクタ、ムーブコンストラクタ、そしてC++11以降で導入された委譲コンストラクタなどが含まれます。

これらのコンストラクタは、オブジェクトの初期化やリソース管理の柔軟性を高め、より効果的なプログラミングを可能にします。

○コピーコンストラクタ

コピーコンストラクタは、既存のオブジェクトから新しいオブジェクトを初期化するために使用されます。

これは、オブジェクトのコピーを作成する際に自動的に呼び出されます。

コピーコンストラクタは、特にオブジェクトがリソースを所有している場合、深いコピーを生成するのに役立ちます。

class MyClass {
public:
    int *data;

    // コピーコンストラクタの定義
    MyClass(const MyClass &source) {
        data = new int; // 新しいメモリを確保
        *data = *source.data; // データのコピーを行う
    }

    // その他のメンバー関数など
};

この例では、MyClassの新しいインスタンスが既存のインスタンスから作成される際、動的に確保されたメモリの内容がコピーされます。

○ムーブコンストラクタ(C++11以降)

ムーブコンストラクタは、C++11で導入された新しい機能で、一時オブジェクトからリソースを「移動」させることができます。

これにより、不要なコピーを避けてパフォーマンスを向上させることができます。

ムーブコンストラクタは、右辺値参照をパラメータとして受け取ります。

class MyClass {
public:
    int *data;

    // ムーブコンストラクタの定義
    MyClass(MyClass &&source) : data(source.data) {
        source.data = nullptr; // 元のオブジェクトをnullに設定
    }

    // その他のメンバー関数など
};

このコードでは、MyClassのインスタンスが別のインスタンスから「移動」される際に、リソースの所有権が新しいインスタンスに移されます。

○委譲コンストラクタ(C++11以降)

委譲コンストラクタは、同じクラス内の別のコンストラクタを呼び出してオブジェクトを初期化する方法です。

これにより、コンストラクタのコードの重複を避けることができます。

class MyClass {
public:
    int x, y;

    // デフォルトコンストラクタから別のコンストラクタを委譲
    MyClass() : MyClass(0, 0) {}

    // 2つのパラメータを取るコンストラクタ
    MyClass(int a, int b) : x(a), y(b) {}
};

このコードでは、デフォルトコンストラクタが、2つの引数を取るコンストラクタに初期化の処理を委譲しています。

●コンストラクタと例外処理

C++プログラミングにおいて、コンストラクタと例外処理は密接に関連しています。

コンストラクタがオブジェクトの初期化中に例外を投げる可能性があるため、これを適切に扱うことは非常に重要です。

例外が投げられた場合、プログラムは安全な状態を維持する必要があります。ここでは、例外を投げるコンストラクタと例外安全性を確保する方法について解説します。

○例外を投げるコンストラクタ

コンストラクタ内で問題が発生した場合、例外を投げることでエラーを通知できます。

例外が投げられると、オブジェクトは正常に構築されないため、そのオブジェクトのデストラクタは呼び出されません。

したがって、リソースの漏れやその他の副作用を避けるためには、リソースの割り当てや初期化に失敗した場合に例外を投げることが推奨されます。

class MyClass {
public:
    MyClass() {
        if (/* エラー条件 */) {
            throw std::runtime_error("初期化エラー");
        }
    }
};

この例では、特定のエラー条件が満たされた場合にstd::runtime_error例外が投げられます。

○例外安全性の確保

例外安全性とは、プログラムが例外を正しく扱い、リソースの漏れやデータの破損を避ける能力のことです。

例外安全性を確保するためには、下記の原則に従うことが重要です。

  1. リソース取得は初期化と同時に行う(RAII: Resource Acquisition Is Initialization)
  2. 不要になったリソースは適切に解放する
  3. コピー演算子と代入演算子を例外安全に実装する

例外安全性を確保するための一般的な方法は、スマートポインタ(例えばstd::unique_ptrstd::shared_ptr)を使用することです。

これらのスマートポインタは、オブジェクトのスコープを抜ける際に自動的にリソースを解放します。

class MyClass {
    std::unique_ptr<int> data;

public:
    MyClass() : data(new int) {
        if (/* エラー条件 */) {
            throw std::runtime_error("初期化エラー");
        }
        // その他の初期化処理
    }
};

この例では、std::unique_ptrが使用されており、例外が投げられても適切にリソースが解放されます。

●初期化の注意点と対処法

C++において初期化は、正確かつ効率的なプログラムの運用に不可欠です。

しかし、初期化にはいくつかの注意点があり、これらを無視するとプログラムにバグや予期しない挙動が発生する可能性があります。

ここでは、初期化における一般的な問題とその対処法について解説します。

○初期化されていない変数の問題

C++では、初期化されていない変数を使用すると、その値は不定となり、プログラムの安定性に影響を及ぼす可能性があります。

特に、基本型のローカル変数は自動的に初期化されないため、これらを使用する前に明示的に初期化する必要があります。

対処法としては、変数宣言時に初期値を与えるか、明示的な代入を行うことが推奨されます。

int main() {
    int value = 0; // 明示的な初期化
    // ...
}

この例では、value変数は0で初期化されており、未定義の値を持つことがなくなります。

○初期化の順序と依存関係

クラスや構造体のメンバ変数の初期化順序は、宣言された順序に従います。

これが依存関係を持つメンバ変数に対しては問題を引き起こす可能性があります。

特に、一つのメンバ変数が他のメンバ変数の値に依存している場合、初期化の順序を正しく管理することが重要です。

対処法として、依存関係を持つメンバ変数は初期化リストを使用して初期化し、依存する順序を明確にすることが効果的です。

class MyClass {
    int x;
    int y;

public:
    MyClass(int val) : x(val), y(x * 2) { // x が y の初期化に影響
        // ...
    }
};

この例では、yの初期化がxの値に依存しているため、xが先に初期化されるように構成されています。

●コンストラクタと初期化のカスタマイズ

C++では、コンストラクタと初期化のカスタマイズにより、より柔軟で強力なクラス設計が可能になります。

カスタマイズ可能なコンストラクタは、異なるタイプのオブジェクトの初期化をサポートし、多様なプログラミングシナリオに対応できるようにします。

ここでは、ユーザー定義型の初期化とコンストラクタのテンプレート化について解説します。

○ユーザー定義型の初期化

ユーザー定義型の初期化では、カスタムクラスや構造体に対して特定の初期化ロジックを実装できます。

これは、オブジェクトが異なるデータや状態で作成される必要がある場合に特に有用です。

初期化ロジックは、コンストラクタ内で定義され、オブジェクトが作成される際に自動的に呼び出されます。

例えば、下記のコードではカスタムクラスMyClassに対して特定の初期化ロジックを実装しています。

class MyClass {
public:
    int value;
    std::string name;

    MyClass(int v, std::string n) : value(v), name(std::move(n)) {
        // ここで初期化処理を行う
    }
};

この例では、MyClassの各インスタンスは、整数値と文字列をパラメータとして受け取り、それぞれvaluenameで初期化されます。

○コンストラクタのテンプレート化

コンストラクタのテンプレート化は、異なるタイプのパラメータに対応するために使用されます。

これにより、一つのコンストラクタ定義で複数のデータ型をサポートできるようになり、コードの柔軟性と再利用性が向上します。

class MyClass {
public:
    template <typename T>
    MyClass(T param) {
        // テンプレートパラメータに基づいて初期化を行う
    }
};

この例では、MyClassのコンストラクタはテンプレートパラメータTを受け取り、これに基づいてオブジェクトを初期化します。

これにより、異なる型のパラメータを持つオブジェクトの作成が可能になります。

●サンプルコード集

C++プログラミングにおいて、理論だけではなく実際のコード例を通じて学ぶことは非常に重要です。

ここでは、コンストラクタの基本的な実装から、より高度なテクニックまで、いくつかのサンプルコードを紹介します。

○サンプルコード1:基本的なコンストラクタの実装

C++でクラスを定義する際、コンストラクタを使用してオブジェクトの初期状態を設定することが一般的です。

下記の例では、シンプルなコンストラクタを持つクラスExampleClassを表しています。

class ExampleClass {
public:
    int value;

    // コンストラクタの定義
    ExampleClass(int initialValue) : value(initialValue) {
        // ここで初期化処理を実行
    }
};

int main() {
    ExampleClass example(10);
    // example.value は 10 で初期化される
}

このコードでは、ExampleClassのインスタンスが作成される際に、valueメンバ変数が指定された初期値(この例では10)で初期化されます。

○サンプルコード2:初期化リストの使用例

初期化リストを使用すると、複数のメンバ変数を効率的に初期化できます。

下記の例では、2つのメンバ変数を持つクラスのコンストラクタで初期化リストを使用しています。

class MyClass {
public:
    int a, b;

    // 初期化リストを使用したコンストラクタ
    MyClass(int x, int y) : a(x), b(y) {}
};

int main() {
    MyClass obj(1, 2);
    // obj.a は 1、obj.b は 2 で初期化される
}

このコードでは、MyClassのインスタンスobjが作成される際に、abがそれぞれ1と2で初期化されます。

○サンプルコード3:コピーコンストラクタの実装

コピーコンストラクタは、既存のオブジェクトをコピーして新しいオブジェクトを作成する際に使用されます。

下記の例では、コピーコンストラクタを持つクラスを表しています。

class CopyClass {
public:
    int data;

    // デフォルトコンストラクタ
    CopyClass(int d) : data(d) {}

    // コピーコンストラクタ
    CopyClass(const CopyClass &other) : data(other.data) {
        // コピー元オブジェクトのデータで初期化
    }
};

int main() {
    CopyClass original(10);
    CopyClass copy = original; // コピーコンストラクタが呼ばれる
    // copy.data は original.data と同じ値(10)になる
}

このコードでは、CopyClassの新しいインスタンスが既存のインスタンスからコピーされ、dataメンバ変数がコピー元のインスタンスと同じ値で初期化されます。

○サンプルコード4:ムーブコンストラクタの実装

C++11から導入されたムーブセマンティクスは、リソースの効率的な管理に役立ちます。

ムーブコンストラクタを使用すると、一時オブジェクトのリソースを新しいオブジェクトに移動できます。

下記の例は、ムーブコンストラクタの基本的な実装を表しています。

class MoveClass {
public:
    int* data;

    // コンストラクタ
    MoveClass(int d) : data(new int(d)) {}

    // ムーブコンストラクタ
    MoveClass(MoveClass&& other) : data(other.data) {
        other.data = nullptr;
    }

    // デストラクタ
    ~MoveClass() {
        delete data;
    }
};

int main() {
    MoveClass a(10);
    MoveClass b = std::move(a); // ムーブコンストラクタが呼ばれる
    // a.data は nullptr、b.data は 10 の値を持つ
}

このコードでは、MoveClassのオブジェクトaからbへのムーブ操作が行われ、aのリソースがbに移動されます。

その結果、adataメンバはnullptrになり、bdataメンバが以前のaのリソースを指すようになります。

○サンプルコード5:例外を投げるコンストラクタ

コンストラクタ内で例外を投げることは、オブジェクトの初期化中に問題が発生した場合に役立ちます。

下記の例は、コンストラクタ内で例外を投げるクラスの実装を表しています。

class ExceptionClass {
public:
    int* data;

    // コンストラクタ
    ExceptionClass(int size) {
        if (size <= 0) {
            throw std::invalid_argument("サイズは正でなければなりません");
        }
        data = new int[size];
    }

    // デストラクタ
    ~ExceptionClass() {
        delete[] data;
    }
};

int main() {
    try {
        ExceptionClass example(-1); // 例外が投げられる
    } catch (const std::invalid_argument& e) {
        std::cout << "例外発生: " << e.what() << std::endl;
    }
}

このコードでは、無効なサイズ(この例では-1)でExceptionClassのインスタンスを作成しようとすると、std::invalid_argument例外が投げられます。

この例外はmain関数内のtry-catchブロックで捕捉され、エラーメッセージが表示されます。

まとめ

この記事では、C++におけるコンストラクタと初期化について、基本的な概念から応用技術までを詳しく解説しました。

明示的なコンストラクタの作成からデフォルトコンストラクタ、ムーブコンストラクタ、例外を投げるコンストラクタに至るまで、多様な実装例を通すことでC++の深い理解を促進できたと思います。

これらの知識は、C++プログラミングの効率と信頼性を高める上で不可欠です。