閉じる

【C++】コンストラクタの使い方入門|書き方・呼び出し・注意点

C++でクラスを使いこなすためには、コンストラクタの理解が欠かせません。

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

本記事では、コンストラクタの基本から書き方、呼び出し方、よくある落とし穴までを、図解と具体的なコード例を交えながら丁寧に解説します。

【C++】コンストラクタとは何か

コンストラクタの役割と特徴

コンストラクタとは、クラスからオブジェクトを生成するときに、初期化処理を自動で行うための特別なメンバ関数です。

クラス名と同じ名前を持ち、戻り値の型を一切書かないという特徴があります。

コンストラクタの主な役割は次の通りです。

文章で説明すると、クラスの内部状態を正しくセットし、不完全なオブジェクトが存在しないようにするという点が重要です。

例えば、座標クラスであればx座標とy座標、ファイルクラスであればファイルオープン状態など、オブジェクトが使われ始める前に必ず整えておきたい初期状態を、コンストラクタでまとめて設定します。

コンストラクタの主な特徴として、次のような性質があります。

まず、オブジェクト生成時に自動的に呼び出されるため、プログラマが明示的に関数呼び出しを書かなくても初期化が行われます。

また、戻り値を書くことはできず、voidを含めいかなる戻り値型も指定してはいけません。

さらに、複数のコンストラクタを用意して、引数の違いによって異なる初期化方法を提供する「オーバーロード」が可能です。

コンストラクタがない場合との違い

コンストラクタを定義しない場合、クラスのメンバ変数は自動的には初期化されません。

特にローカル変数としてオブジェクトを生成した場合、メンバ変数は不定値を持つ可能性があり、そのまま演算や比較に使うと予測不能な動作になります。

一方でコンストラクタを用意すると、オブジェクト生成と同時に必ず一定の初期状態が保証されるため、安全で読みやすいコードになります。

この「オブジェクトは常に有効な状態で存在する」という考え方は、C++の設計思想において特に重要です。

コンストラクタの基本的な書き方

最小構成のコンストラクタ定義

ここでは、2次元座標を表す簡単なクラスを例にして、最小限のコンストラクタを書いてみます。

C++
#include <iostream>

// 2次元座標を表すクラス
class Point {
private:
    int x; // x座標
    int y; // y座標

public:
    // コンストラクタ(引数なし)
    // オブジェクト生成時に、自動的に一度だけ呼び出される
    Point() {
        x = 0; // xを0で初期化
        y = 0; // yを0で初期化
    }

    void print() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Point p;   // ここでPoint()コンストラクタが自動的に呼び出される
    p.print(); // "(0, 0)" と表示される
    return 0;
}
実行結果
(0, 0)

この例のように、コンストラクタの名前はクラス名と完全に一致し、戻り値の型は一切書かないことがルールです。

もし間違えてvoid Point()と書いてしまうと、コンストラクタではなく普通のメンバ関数になってしまい、意図した初期化が行われなくなります。

デフォルトコンストラクタと暗黙定義

引数を持たないコンストラクタをデフォルトコンストラクタと呼びます。

自分でデフォルトコンストラクタを書かなかった場合、多くの状況ではコンパイラが自動的に「何もしないコンストラクタ」を生成します。

しかし、それはメンバ変数に何か値を入れてくれるわけではなく、結果として未初期化のままになることが多いという点に注意が必要です。

安全なコードを書くには、できる限り自分で初期値を明示する習慣をつけることが重要です。

その手段として、後述するメンバ初期化リストを活用するのが一般的です。

コンストラクタの呼び出し方とタイミング

オブジェクト生成時の自動呼び出し

コンストラクタは、オブジェクトを生成するタイミングで自動的に呼び出されます。

代表的なケースは、次のような3つのパターンです。

1つ目は、ローカル変数として宣言する場合です。

このとき、宣言文の行に到達した瞬間にコンストラクタが実行されます。

2つ目は、new演算子を使って動的にオブジェクトを生成するときです。

この場合も、newの直後にコンストラクタが呼び出されます。

3つ目は、グローバル変数やstatic変数としてオブジェクトを宣言した場合で、プログラム開始前の静的初期化のタイミングでコンストラクタが動きます。

C++
#include <iostream>

class Sample {
public:
    Sample() {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }
};

Sample globalObj; // グローバルオブジェクト

int main() {
    std::cout << "main開始" << std::endl;

    Sample localObj;      // ローカルオブジェクト
    Sample* heapObj = new Sample(); // ヒープオブジェクト

    delete heapObj; // (デストラクタの説明は本記事では割愛)
    return 0;
}
実行結果
コンストラクタが呼ばれました
main開始
コンストラクタが呼ばれました
コンストラクタが呼ばれました

このように、コンストラクタは明示的に呼び出すものではなく、オブジェクト作成と常にセットで動く仕組みだと理解しておくと良いです。

明示的呼び出しとコンストラクタ呼び出しの違い

C++では、コンストラクタをp.Point()のような形で直接呼び出すことは想定されていません。

そのような記述は、もし存在すれば別のメンバ関数を呼び出しているだけであり、オブジェクトの再初期化にはなりません。

コンストラクタはオブジェクト定義の文脈でのみ意味を持つ特別な構文であると意識しておくと良いでしょう。

引数付きコンストラクタとオーバーロード

複数のコンストラクタを用意する

同じクラスに複数のコンストラクタを定義し、引数の違いによって使い分けることができます。

これをコンストラクタのオーバーロードと呼びます。

C++
#include <iostream>

class Rect {
private:
    int width;
    int height;

public:
    // デフォルトコンストラクタ
    Rect() {
        width = 0;
        height = 0;
    }

    // 幅と高さを指定するコンストラクタ
    Rect(int w, int h) {
        width = w;
        height = h;
    }

    void print() const {
        std::cout << "width=" << width
                  << ", height=" << height << std::endl;
    }
};

int main() {
    Rect r1;        // Rect() が呼ばれる
    Rect r2(10, 5); // Rect(int, int) が呼ばれる

    r1.print();
    r2.print();
    return 0;
}
実行結果
width=0, height=0
width=10, height=5

このように、同じクラス名であっても、引数リストが異なれば別のコンストラクタとして定義可能です。

クラス利用者にとっては「何を指定すればどのように初期化されるか」が直感的にわかりやすくなるため、実用的なクラス設計では頻繁に使われるテクニックです。

デフォルト引数付きコンストラクタ

オーバーロードの代わりに、コンストラクタの引数にデフォルト値を設定する方法もあります。

C++
#include <iostream>

class Rect {
private:
    int width;
    int height;

public:
    // デフォルト引数付きコンストラクタ
    Rect(int w = 0, int h = 0) {
        width = w;
        height = h;
    }

    void print() const {
        std::cout << "width=" << width
                  << ", height=" << height << std::endl;
    }
};

int main() {
    Rect r1;         // Rect(0, 0) と同じ
    Rect r2(10);     // Rect(10, 0) と同じ
    Rect r3(10, 5);  // Rect(10, 5)

    r1.print();
    r2.print();
    r3.print();
    return 0;
}
実行結果
width=0, height=0
width=10, height=0
width=10, height=5

この書き方を使うと、コンストラクタの定義を1つにまとめつつ、さまざまな呼び出し方に対応できます。

ただし、他のコンストラクタオーバーロードと組み合わせると引数解決が複雑になることがあるため、過度な組み合わせは避けるのが安全です。

メンバ初期化リストの使い方

メンバ初期化リストとは

C++のコンストラクタでは、:に続けてメンバ変数を初期化するメンバ初期化リストを使うことができます。

実は、メンバ変数の実際の初期化はコンストラクタ本体に入る前に行われており、そのタイミングで値を設定するのがメンバ初期化リストです。

C++
#include <iostream>
#include <string>

class User {
private:
    std::string name;
    int age;

public:
    // メンバ初期化リストを使ったコンストラクタ
    User(const std::string& n, int a)
        : name(n), // nameメンバをnで初期化
          age(a)   // ageメンバをaで初期化
    {
        // ここに来る時点で、nameとageはすでに初期化済み
    }

    void print() const {
        std::cout << name << " (" << age << ")" << std::endl;
    }
};

int main() {
    User u("Taro", 20);
    u.print();
    return 0;
}
実行結果
Taro (20)

メンバ初期化リストを使うことで、メンバ変数を「生成と同時に」狙った値で初期化できるため、パフォーマンスや安全性の面で有利になります。

特に、参照メンバやconstメンバはメンバ初期化リストでしか初期化できないため、必須のテクニックになります。

代入との違いと注意点

メンバ初期化リストを使わずに、コンストラクタ本体で代入する書き方も可能ですが、いくつかの面で不利になります。

まず、クラス型のメンバやstd::stringなどでは、一度デフォルトコンストラクタで生成されてから、後で代入によって値が変更されることになります。

これは、初期化が2段階になり、場合によっては余計なコストが発生します。

さらに、参照メンバやconstメンバは、コンストラクタ本体の中で代入することができません

これらは必ずメンバ初期化リストで初期値を指定しなければならないという仕様になっています。

したがって、実務的には「基本的にすべてのメンバをメンバ初期化リストで初期化する」スタイルを身につけておくと、安全で一貫したコードが書けます。

コンストラクタでよくある間違いと注意点

戻り値型を書いてしまう

コンストラクタは戻り値型を一切書かないというルールがありますが、慣れないうちはvoidを先頭に付けてしまいがちです。

この場合、コンパイラはそれを「void型のメンバ関数」と解釈し、コンストラクタとして扱ってくれません。

その結果、オブジェクト生成時に自動呼び出しされず、メンバ変数が未初期化のままになるというバグの原因になります。

コンストラクタ名は必ず「クラス名のみ」で宣言するという点を、意識的にチェックする習慣をつけておくと安心です。

デフォルトコンストラクタが消えるケース

クラスに引数付きコンストラクタだけを定義した場合、コンパイラはデフォルトコンストラクタを自動生成しなくなります。

つまり、次のようなコードはコンパイルエラーになります。

C++
class Box {
public:
    Box(int size) {
        // 何らかの初期化
    }
};

int main() {
    Box b;      // エラー! Box() が存在しない
    Box c(10);  // OK
    return 0;
}

このように、自分でコンストラクタを1つでも定義すると、暗黙のデフォルトコンストラクタは用意されないという仕様があります。

引数なしで生成したい場面がありそうなクラスでは、明示的にデフォルトコンストラクタを定義しておく必要があります。

メンバの初期化順序の誤解

メンバ初期化リストに書く順番と、実際に初期化される順番は必ずしも一致しません。

実際の初期化順序は、クラスの中でメンバ変数が宣言された順番になります。

したがって、初期化リストの順番を変えても、意味は変わりません。

この仕様を知らずに、他のメンバを使って初期化するようなコードを書くと、まだ初期化されていないメンバを参照してしまう危険があります。

可読性と安全性のためにも、メンバ初期化リストは宣言順と同じ並びにするのが一般的なコーディングスタイルです。

まとめ

コンストラクタは、C++でクラスを正しく設計するための中心的な仕組みです。

クラス名と同じ名前で戻り値を持たず、オブジェクト生成時に一度だけ自動で呼び出されるという性質を持ちます。

本記事では、デフォルトコンストラクタや引数付きコンストラクタの書き方、メンバ初期化リストの重要性、そしてありがちな間違いを解説しました。

「オブジェクトは必ず有効な状態で生成する」という意識でコンストラクタを設計すれば、バグを防ぎ、読みやすく安全なC++コードに近づくことができます。

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

URLをコピーしました!