記事内にはプロモーションが含まれています

【C++】クラスのコンストラクタを徹底解説!初期化の書き方から派生クラスの挙動まで

【C++】クラスのコンストラクタを徹底解説!初期化の書き方から派生クラスの挙動まで C++

C++のオブジェクト指向プログラミングにおいて、クラスの使い勝手や安全性を左右する最も重要な要素の一つが「コンストラクタ」です。

オブジェクトが生成される瞬間に自動的に呼び出され、メンバ変数の初期化やリソースの確保を行うこの機能は、正しく理解していないと予期せぬバグやパフォーマンス低下の原因となります。

しかし、C++にはデフォルトコンストラクタ、コピーコンストラクタ、変換コンストラクタなど多くの種類があり、「どの書き方が最適なのか?」「派生クラスではどう動くのか?」と迷うことも少なくありません。

この記事では、C++におけるコンストラクタの基本的な書き方から、メンバ初期化子リストを使った効率的な初期化、そして継承関係における呼び出し順序まで、実務で役立つ知識をサンプルコード付きで詳しく解説します。

【本記事の信頼性】
プロフィール
執筆者:マヒロ
  • 執筆者は元エンジニア
  • SES⇒大手の社内SE⇒独立
  • 現在はこじんまりとしたプログラミングスクールを運営
  • モットーは「利他の精神」

コンストラクタとは?基本的な書き方と役割

コンストラクタは、クラスのインスタンス(オブジェクト)が生成されるときに一度だけ自動的に実行される特別なメンバ関数です。
主に戻り値を持たず、クラス名と同じ名前で定義するという特徴があります。

まずは、最も基本的なコンストラクタの定義方法と、引数を使ったデータの受け渡しについて見ていきましょう。

引数なし・引数ありコンストラクタの定義

コンストラクタは、引数の有無や数によって複数定義(オーバーロード)することができます。

#include <iostream>
#include <string>

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

    // 1. デフォルトコンストラクタ(引数なし)
    User() {
        name = "Guest";
        age = 0;
        std::cout << "デフォルトコンストラクタが呼ばれました" << std::endl;
    }

    // 2. 引数付きコンストラクタ
    User(std::string n, int a) {
        name = n;
        age = a;
        std::cout << "引数付きコンストラクタが呼ばれました" << std::endl;
    }

    void show() {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    User user1;             // デフォルトコンストラクタ呼び出し
    user1.show();

    User user2("Alice", 25); // 引数付きコンストラクタ呼び出し
    user2.show();

    return 0;
}

実行結果

デフォルトコンストラクタが呼ばれました
Name: Guest, Age: 0
引数付きコンストラクタが呼ばれました
Name: Alice, Age: 25

User クラスの中に、クラス名と同じ User という名前の関数を2つ定義しています。

main 関数で User user1; と宣言した際は引数なしのコンストラクタが、User user2("Alice", 25); と宣言した際は引数ありのコンストラクタが自動的に選択され、実行されます。

これにより、オブジェクト生成と同時に適切な初期値を設定することができます。

メンバ初期化子リストを使った効率的な初期化

コンストラクタの中で代入を行うよりも、メンバ初期化子リスト(Member Initializer List) を使用する方が推奨されます。

特に const メンバや参照メンバ、あるいはクラス型のメンバ変数を初期化する際には必須のテクニックです。

初期化子リストの書き方とメリット

コンストラクタの定義の直後にコロン : を記述し、メンバ変数名と初期値を列挙します。

#include <iostream>
#include <string>

class Product {
private:
    std::string name;
    int price;
    const int taxRate; // 定数メンバ(代入では初期化できない)

public:
    // メンバ初期化子リストを使用
    Product(std::string n, int p) : name(n), price(p), taxRate(10) {
        // コンストラクタ本体は空でもOK
        std::cout << "Productを作成しました" << std::endl;
    }

    void display() {
        std::cout << name << ": " << price << "円 (税率" << taxRate << "%)" << std::endl;
    }
};

int main() {
    Product item("Pen", 100);
    item.display();
    return 0;
}

実行結果

Productを作成しました
Pen: 100円 (税率10%)

Product(...) : name(n), price(p), taxRate(10) の部分が初期化子リストです。

コンストラクタの {} 内で name = n; と書くのは「代入」ですが、初期化子リストを使うと変数が生成される瞬間に値が入るため「初期化」となります。

特に const 変数や参照(リファレンス)変数は、生成後の代入が不可能なため、必ずこの方法で初期化する必要があります。

また、無駄なコピー処理が減るためパフォーマンス面でも有利です。

複数のコンストラクタをまとめる「委譲コンストラクタ」

複数のコンストラクタを定義すると、初期化処理のコードが重複してしまうことがあります。

C++11から導入された委譲コンストラクタ(Delegating Constructor)を使うと、あるコンストラクタから別のコンストラクタを呼び出すことが可能です。

共通処理を1つのコンストラクタに集約する

#include <iostream>
#include <string>

class Log {
public:
    std::string message;
    int level;

    // メインとなるコンストラクタ
    Log(std::string msg, int lvl) : message(msg), level(lvl) {
        std::cout << "ログを初期化: " << msg << " (Lv." << lvl << ")" << std::endl;
    }

    // 引数が1つの場合、メインのコンストラクタに処理を丸投げ(委譲)する
    Log(std::string msg) : Log(msg, 1) { 
        // 追加の処理があればここに書く
    }
    
    // 引数なしの場合も委譲
    Log() : Log("None", 0) {}
};

int main() {
    Log log1("Error Occurred", 3);
    Log log2("Info Message");
    Log log3;
    return 0;
}

実行結果

ログを初期化: Error Occurred (Lv.3)
ログを初期化: Info Message (Lv.1)
ログを初期化: None (Lv.0)

Log(std::string msg) : Log(msg, 1) のように、初期化子リストの位置で自身のクラスの別のコンストラクタを呼び出しています。

これにより、初期化ロジックを一箇所(ここでは引数2つのコンストラクタ)に集中させることができ、コードの重複を防ぎ、保守性を高めることができます。

派生クラス(継承)におけるコンストラクタの呼び出し

クラスを継承した場合、子クラス(派生クラス)のコンストラクタが実行される前に、親クラス(基底クラス)のコンストラクタが必ず実行されます

親クラスに引数付きコンストラクタしかない場合、明示的に呼び出す必要があります。

親クラスのコンストラクタを明示的に呼ぶ方法

#include <iostream>
#include <string>

// 親クラス
class Animal {
public:
    std::string name;

    Animal(std::string n) : name(n) {
        std::cout << "Animalコンストラクタ: " << name << std::endl;
    }
};

// 子クラス
class Dog : public Animal {
public:
    std::string breed;

    // 親クラスのコンストラクタに引数を渡す
    Dog(std::string n, std::string b) : Animal(n), breed(b) {
        std::cout << "Dogコンストラクタ: " << breed << std::endl;
    }
};

int main() {
    Dog myDog("Pochi", "Shiba");
    return 0;
}

実行結果

Animalコンストラクタ: Pochi
Dogコンストラクタ: Shiba

Dog クラスのコンストラクタ初期化子リストで Animal(n) と記述することで、親クラス Animal のコンストラクタに引数 n を渡して実行させています。

もし Animal クラスにデフォルトコンストラクタ(引数なし)がない場合、この記述を省略するとコンパイルエラーになります。

実行結果から、親クラスのコンストラクタが先に完了し、その後に子クラスのコンストラクタが動いていることがわかります。

コピーコンストラクタと暗黙の定義

オブジェクトを別のオブジェクトで初期化する場合(例:User u2 = u1;)、コピーコンストラクタが呼ばれます。

通常はコンパイラが自動的に生成しますが、ポインタ変数をメンバに持つ場合などは自前で定義する必要があります。

コピーコンストラクタの挙動

#include <iostream>

class Box {
public:
    int width;

    Box(int w) : width(w) {}

    // コピーコンストラクタの定義
    Box(const Box& other) : width(other.width) {
        std::cout << "コピーコンストラクタが呼ばれました" << std::endl;
    }
};

int main() {
    Box b1(10);
    Box b2 = b1; // ここでコピーコンストラクタが動く

    std::cout << "b2.width: " << b2.width << std::endl;
    return 0;
}

実行結果

コピーコンストラクタが呼ばれました
b2.width: 10

Box(const Box& other) がコピーコンストラクタです。
自分自身と同じ型の参照を引数に取ります。

動的メモリ確保を行っているクラスの場合、デフォルトのコピー(浅いコピー)ではメモリの二重解放などのバグを引き起こすため、ここで新しいメモリを確保して値をコピーする「深いコピー」を実装する必要があります。

C++のスキルを活かして年収を上げる方法

以上、C++のクラスのコンストラクタについて解説してきました。

なお、C++のスキルがある場合には、「転職して年収をアップさせる」「副業で稼ぐ」といった方法を検討するのがおすすめです。

C++を扱えるエンジニアは希少価値が高いため、転職によって数十万円の年収アップはザラで、100万円以上年収が上がることも珍しくありません。

なお、転職によって年収を上げたい場合は、エンジニア専門の転職エージェントサービスを利用するのが最適です。

今すぐ転職する気がなくとも、とりあえず転職エージェントに無料登録しておくだけで、スカウトが届いたり、思わぬ好待遇の求人情報が送られてきたりするというメリットがあります。

併せて、副業案件を獲得できるエージェントにも登録しておくと、空いている時間を活かして稼げるようなC++の案件を探しやすくなります。

転職エージェントも副業エージェントも、登録・利用は完全無料なので、どんな求人や副業案件があるのか気になる方は、気軽に利用してみるとよいでしょう。