C++ コンストラクタ【オブジェクトの未初期化状態を防止しろ!】

C++
この記事は約14分で読めます。

こんにちは、ナナです。

「クラス」とは型のひとつであり、型を元に「オブジェクト」という変数を作ることができます。

「コンストラクタ」とは、このオブジェクトの生成に強く関わるメンバ関数のことです。

コンストラクタの役割と、なぜこのような機構が必要になったのかを解説していきましょう。

本記事で学習できること
  • 未初期化のデータが許されない理由とは?
  • コンストラクタによる強制的な初期化機構とは?
  • コンストラクタの定義方法とは?
  • デフォルトコンストラクタの特徴とは?
スポンサー

未初期化のデータは不定であることの問題

今日のテーマは「コンストラクタ」というものなんですね。初めて聞く言葉です。

と思ったら、「未初期化のデータの問題」ってどういうことですか?「コンストラクタ」はどこに行ったのですか?

ナナ
ナナ

「コンストラクタ」はオブジェクトにおいて非常に大事な役割を持っています。このコンストラクタという仕組みがなぜ生まれたかを知るためには、C言語のとある問題に目を向ける必要があります。

まずは、その問題を知った上で「コンストラクタ」がどのように貢献するのかを学んでいきましょう。

C言語やC++では、データを管理するための「変数」を作成することができます。「変数」とは、とあるタイミングで生まれ、役目が終わると共に死んでいきます。

この「変数」が生まれるタイミングに大きな弱点があるのです。

未初期化のデータが起こす問題

次のC言語のプログラムはある問題を抱えています。どのような問題なのか皆さんわかりますか?

#include <stdio.h>

typedef struct
{
    int x;
    int y;
} POS;

void printPos(POS* pos)
{
    //  X、Y座標を表示
    printf("x:%d y:%d\n", pos->x, pos->y);
}

int main()
{
    POS pos;    //  座標データの作成

    //  座標表示
    printPos(&pos);

    return 0;
}

このプログラムは実際に動かすことができます。最終的に表示されるX、Y座標は次の結果となりました。

x:-858993460 y:-858993460

そうです。この問題は、変数「pos」が初期化されていないため、不定値であるXY座標が表示されているのです。

ナナ
ナナ

このような未初期化の変数というのは初期化、もしくは代入後に参照することが求められます。未初期化の変数とは絶対に参照してはならないということですね。

データが正しく初期化されるかは作成した側の責務であること

先ほどのプログラムを修正してみましょう。次のように初期値を設定します。

int main()
{
    POS pos = {0, 0};  // 構造体メンバの初期化

    printPos(&pos);

    return 0;
}
x:0 y:0

座標をどのような値で初期化するかは場合により異なりますが、少なくとも不定値ではなくなりました。

ここで着目すべきことがあります。

この変数を初期化する作業は、変数を作り出したmain関数側の責務だということです。つまり、main関数側がその責務を放棄すれば、変数は不定値を持った状態で生まれることがあるということです。

初期化するもしないも自由

C言語では、この未初期化である可能性を排除する方法はありません。変数を作り出す側のエンジニアの善意に支えられているのです。

オブジェクトが管理する「メンバ変数」もデータであるということ

本サイトではオブジェクト指向における「オブジェクト」とは、自立性の高いロボットのようなものであると解説しました。

オブジェクトもクラス型として「メンバ変数」というデータを管理しています。

もしも、この「メンバ変数」が不定値であった場合、オブジェクトはいったいどうなってしまうのでしょうか?

不完全なオブジェクト

このようにオブジェクトが生成された時に、「メンバ変数」が不定値であることはオブジェクトが不正な状態にあると言えるのです。

「使う側の気分次第でロボットが不正な状態となる」それは、自立性のあるロボットにおいて看過できない問題です。

この問題をクラスの枠組みが提供する「コンストラクタ」という機能で防止するのです。

ナナ
ナナ

「コンストラクタ」という機能はオブジェクト生成時に不正な状態を避けるために取り入れられた機構です。

次は使い方を解説しますよ。

スポンサー

コンストラクタ:強制力のある初期化機構

未初期化のデータというのは、それ自体が危険なものってことなんですね。

でも、そのことと「コンストラクタ」というものが、いったいどのように関係するんですか?

ナナ
ナナ

「コンストラクタ」とは、建設者という意味です。つまり、「オブジェクトを建設する者」ということですね。

この「コンストラクタ」を定義することで、オブジェクトを生成時に強制的にデータを初期化することが可能となります。

コンストラクタの定義ルール

「コンストラクタ」はメンバ関数の1つです。そのため関数定義を行うことで利用することができます。

このコンストラクタの定義には、他の関数にはない特殊なルールがあります。

コンストラクタ定義時のルール
  • コンストラクタとして定義するメンバ関数名は「クラス名」と同じとする。
  • 戻り値は存在しない。ただし、「void型」ではなく型の指定をしない。

このルールを守った具体的なコンストラクタの定義をプログラムで示しましょう。

class POS
{
public:
    int x;
    int y;

    POS();  //  コンストラクタのプロトタイプ宣言
};

// コンストラクタのメンバ関数定義
POS::POS()
{
    x = 0;
    y = 0;
}

各部品を解説しましょう。

コンストラクタの定義方法
ナナ
ナナ

このようにコンストラクタは命名規則など、特殊なルールが適用される特殊なメンバ関数です。

コンストラクタはオブジェクト生成時に自動で呼び出される関数

コンストラクタの定義は行いました。それではコンストラクタをいつ呼び出せばよいのでしょうか?

なんと、コンストラクタは皆さんが明確に呼び出す必要はありません。オブジェクトが生成されたタイミングで自動的に呼び出されるのです。

それでは、Visual Studioのデバッガ機能を使って、コンストラクタが呼び出される様子を考察しましょう。次のプログラムを皆さんのソースコードに貼り付けて動かしてみましょう。

#include <stdio.h>

class POS
{
public:
    int x;
    int y;

    POS(void);
};

// コンストラクタ定義
POS::POS(void)
{
    x = 0;
    y = 0;
}

int main()
{
    POS pos;

    return 0;
}

次のように、ブレークポイントを張って「ステップイン」を行ってみましょう。

コンストラクタの呼び出し確認

クラス変数の定義がされると同時に「コンストラクタ」に処理がジャンプすることがわかるでしょう。

このように「コンストラクタ」は、オブジェクト生成時に自動で呼び出されるメンバ関数なのです。

デバッガの使い方を習得していない方は、『C言語 デバッグ技術【プログラムが動かないときの悩み解決】』を必ず読んでおきましょう。

ナナ
ナナ

実際に動かしてリアルにプログラムが動くことを確認しましょう。体感を得ることで知識が吸着するのです。

コンストラクタによる初期化の強制力

「コンストラクタ」という機構がクラスに備わっていることの意味を改めて確認しましょう。

そもそもの問題は、C言語におけるデータの初期化は「変数を生成する側の都合により初期化しても、しなくてもよい」というスタンスでした。

C++のクラスでは、オブジェクトを生成したときに自動で呼び出される「コンストラクタ」という機構が導入されました。

これにより、クラス型を定義する側が、オブジェクトが管理するメンバ変数を強制的に初期化できる仕組みを手に入れたことになります。

つまり、オブジェクトを使う側の都合とは関係なく、初期化を強制することができるということなのです。

ナナ
ナナ

「オブジェクトが不正であることを許さない」それがコンストラクタの役割なのです。クラスを提供する側による強制力のある初期化機構が「コンストラクタ」です。

この価値を理解するためには、クラスという型を定義する側と、オブジェクトを利用する側を明確に分離する意識で捉える必要があります。

引数付きのコンストラクタ定義方法

先ほどの例ではXY座標の初期値が「0」でしたが、生成時に別の値で初期化したいこともあることでしょう。

コンストラクタは戻り値を持ちませんが、引数は自由に定義することができます。引数がある場合のコンストラクタの書き方を示しましょう。

#include <stdio.h>

class POS
{
public:
    int x;
    int y;

    POS(void);                  //  引数なしコンストラクタ
    POS(int tmpx, int tmpy);    //  引数ありコンストラクタ
};

// 引数なしのコンストラクタ定義①
POS::POS(void)
{
    x = 0;
    y = 0;
}

// 引数ありのコンストラクタ定義②
POS::POS(int tmpx, int tmpy)
{
    x = tmpx;
    y = tmpy;
}

このようにコンストラクタは1つのクラスの中に複数共存させることも可能です。

オブジェクトの生成側では、次の様に引数を指定してコンストラクタを呼び出します。

int main()
{
    POS pos1;               //  コンストラクタ①で生成
    POS pos2(100, 200);     //  コンストラクタ②で生成
    POS pos3 = {300, 400};  //  コンストラクタ②で生成
    POS pos4[2] = { {500, 600}, {700, 800} };

    return 0;
}

「pos2」の定義方法は丸括弧が付いているので変数定義っぽくないですが、変数定義です。

オブジェクト「pos2」が生成され、メンバ変数「X」「Y」には「100」「200」が設定されることになります。

「pos3」の定義方法は、構造体の初期化と同じ書き方ですね。この書き方は「pos4」のようにオブジェクトの配列の初期化にも対応できる書き方となります。

もしも、次のようにコンストラクタとして定義していない引数パターンの場合は、ビルドエラーが発生することに注意しましょう。

int main()
{
    POS pos1;               //  コンストラクタ①で生成
    POS pos2(100, 200);     //  コンストラクタ②で生成
    POS pos3 = {300, 400};  //  コンストラクタ②で生成
    POS pos4[2] = { {500, 600}, {700, 800} };
    POS pos5(300);          //  存在しないケースはビルドエラー発生

    return 0;
}
ナナ
ナナ

オブジェクトを生成する側が、引数を使ってメンバ変数の初期値を決めさせる構成も可能ということですね。

スポンサー

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

「デフォルトコンストラクタ?」また、新しい言葉が出てきましたね。

「デフォルト」って「初期設定」とか「標準」みたいな意味合いですよね。

ナナ
ナナ

そうだね、「デフォルトコンストラクタ」はコンストラクタが未定義の状態でも生成される特別なコンストラクタなんですよ。

皆さんが作成したクラスに「コンストラクタ」の定義を明記しなかった場合、どのようになるのでしょう?

ここで登場するのが「デフォルトコンストラクタ」です。

デフォルトコンストラクタの定義方法

「デフォルトコンストラクタ」とは、引数が「void型」のコンストラクタを示します。

つまり、最初にお見せした次のコンストラクタは「デフォルトコンストラクタ」ということです。

class POS
{
public:
    int x;
    int y;

    POS();  //  デフォルトコンストラクタ
};

POS::POS()
{
    x = 0;
    y = 0;
}

C++では引数の指定を省略した場合、「void型」として扱われます。

ナナ
ナナ

C言語で関数の引数が省略された場合は「引数を明確化しない」という意味であり、「void型」ではありません。

この部分はC言語とC++の言語仕様に違いがあることに注意しましょう。

デフォルトコンストラクタは自動生成される特別なコンストラクタ

実はデフォルトコンストラクタは特定の条件を満たすと、皆さんが定義をせずとも自動的にバックグラウンドで自動定義される特別なコンストラクタです。

次のようにクラスを定義すると、メンバ関数としてコンストラクタは存在していませんが、実はデフォルトコンストラクタがコンパイラによって自動生成されます。

class POS
{
public:
    int x;
    int y;
};

デフォルトコンストラクタが自動生成される条件とは

他のコンストラクタが1つも定義されていない時

です。

そのため、引数付きのコンストラクタのみを定義すると、オブジェクト生成時に引数を指定しない生成はできなくなります。

class POS
{
public:
    int x;
    int y;

    POS(int tmpx, int tmpy);    //  引数ありコンストラクタ
};

POS::POS(int tmpx, int tmpy)
{
    x = tmpx;
    y = tmpy;
}

int main()
{
    POS pos;    //  デフォルトコンストラクタが未定義のためエラー

    return 0;
}

このように引数が2つのコンストラクタが定義されているため、デフォルトコンストラクタが生成されず、初期値指定のないオブジェクトの生成はできなくなります。

ナナ
ナナ

このように、意図的にデフォルトコンストラクタを生成させないようにして、引数付きのオブジェクト生成しかできないようにするということも可能です。

この例でいえば、オブジェクトを生成する側は必ず初期値として「X」「Y」の値を指定させることを強制することができるということですね。

注意:自動生成されたデフォルトコンストラクタは何もしてくれない

「自動生成されるなんて便利!」と思うかもしれませんが、注意点があります。

自動生成されたデフォルトコンストラクタは、メンバ変数を初期化してくれません。つまり、次のように処理が空っぽのコンストラクタということです。

POS::POS()
{

}

この状態で生成されたPOSオブジェクトのX・Y座標は不定値となります。

ナナ
ナナ

自動生成されたデフォルトコンストラクタは、未定義でもビルドエラーを起こさないための救済処置ということです。

オブジェクトの初期化をしてくれるわけではないので注意しましょう。

スポンサー

Q&A:コンストラクタに関するよくある質問

Q:コンストラクタは自動定義されるなら定義しないケースはあるの?

「コンストラクタ」は自分で書かなくても自動で定義されるんですよね。定義しなくてよいケースはあるのですか?

ナナ
ナナ

クラスの作りによっては「コンストラクタ」が不要なケースもありますが、基本的には定義が必要と考えておいてください。

皆さんがクラスを定義したときには、コンストラクタは用意するものと考えておきましょう。

「メンバ変数」を持たない特殊なクラスを定義するときは、コンストラクタが不要となるケースもありますが、あくまでも例外です。

Q:int型の変数はコンストラクタで初期化されないの?

C++にはint型とかchar型とかってデータ型もありますよね。これらの型は変数を作成した時にコンストラクタって呼ばれないんですか?

ナナ
ナナ

はい、呼ばれません。int型やchar型といったC++で用意されている組み込みデータ型はコンストラクタの定義ができないのです。

「コンストラクタ」はあくまでも「クラス」という枠組みに適用された新しい機能なのです。

そのため、C言語からも使える「組み込み型」に関しては「コンストラクタ」という機能は適用されないのです。

int main()
{
    int num;
    char moji;

    return 0;
}

このような「num」「moji」といった組み込み型の変数は、やはりC言語と同様に作成する側が初期化をする必要があります。

スポンサー
C++C++入門
スポンサー
モノづくりC言語塾