コンストラクタ

メンバ変数の初期化を自動化する

前ページのコードのクラス定義は、実はやや問題があります。


class SimpleClass
{
private:
    int number;
    std::string name;

public:
    int getNumber() { return number; }
    void setNumber(int n) { number = n; }

    std::string getName() { return name; }
    void setName(const char* s) { name = s; }
};

このクラスは、メンバ変数が初期化される前にアクセスしてしまう危険性があります。


int main()
{
    SimpleClass sc;

    //初期化していないメンバ変数にアクセス
	//意図した通りに動かない
    std::cout << sc.getNumber();
}

これはバグの原因になります。
メンバ変数の読み取りの前に値を代入すれば済むのですが、インスタンスを生成するごとに毎回代入していたのでは面倒ですし、忘れてしまう可能性もあります。

インスタンスの生成と同時に自動的にメンバ変数を初期化するにはコンストラクタを使用します。

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

コンストラクタは以下のように定義される関数です。


class クラス名
{
public:
    //コンストラクタ
    クラス名()
    {
        //初期化処理
    }
};

#include <iostream>
#include <string>

class SimpleClass
{
private:
    int number;
    std::string name;

public:
    //コンストラクタ
    SimpleClass()
    {
        number = 0;
    }

    int getNumber() { return number; }
    void setNumber(int n) { number = n; }

    std::string getName() { return name; }
    void setName(const char* s) { name = s; }
};

int main()
{
    SimpleClass sc;
	//この時点でsc.numberは0で初期化されている
	
	//0を表示
	std::cout << sc.getNumber();
}

コンストラクタは

  • 戻り値を持たない(記述しない。voidも書かない)
  • クラス名と同じ名前にする

という決まりがあります。

コンストラクタはメンバ関数の一種ですが、クラスのインスタンスが生成される直前に自動的に呼び出されます。
つまり、このSimpleClassのインスタンスが使えるようになった時点でメンバ変数number0で初期化された状態になっています。

コンストラクタは通常のメンバ関数とは違い、後から呼び出すことはできません。
インスタンスの生成時にのみ呼び出せる特殊な関数です。
コンストラクタ内の処理は自由ですが、インスタンス生成毎に呼び出されるので、メンバ変数の初期化等に留め、あまり重い処理はしないようにしましょう。

ちなみに、メンバ変数nameはstringクラスなので、明示的に初期化しなくても空の文字で自動的に初期化されます。
これはstringクラスが持っているコンストラクタの働きによるものです。
そのため、空文字の状態で構わないならばSimpleClassのコンストラクタでは再度初期化する必要はありません。
何か特定の文字列で初期化したい場合や、stringクラスではなくchar型配列による文字列であればコンストラクタ内で初期化が必要となります。

コンストラクタを省略した場合

クラス内にコンストラクタをひとつも定義しない場合、コンパイラが暗黙的(自動的)に「何もしない」コンストラクタを生成します。


class SimpleClass
{
private:
    int number;
    std::string name;
};

//↓自動的にこう解釈される

class SimpleClass
{
private:
    int number;
    std::string name;

public:
    SimpleClass(){}
};

実際にはもう少し多くのコンストラクタが自動的に生成されますが、ここでは省略します。

正確には、上のコードの一つ目のクラスと二つ目のクラスは初期化時の動作が微妙に異なるので全く同一ではありません。
この辺は結構細かいルールがありますが、あまり神経質にならなくても問題になることはほぼありません。
詳しくはオブジェクトの初期化で説明しています。

引数のあるコンストラクタ

コンストラクタには引数を指定することもできます。


#include <iostream>
#include <string>

class SimpleClass
{
private:
    int number;
    std::string name;

public:
    //引数なしコンストラクタ
    //(デフォルトコンストラクタ)
    SimpleClass()
    {
        number = 0;
    }

    //引数付きコンストラクタ
    SimpleClass(int n, const char *s)
    {
        number = n;
        name = s;
    }

    int getNumber() { return number; }
    void setNumber(int n) { number = n; }

    std::string getName() { return name; }
    void setName(const const char* s) { name = s; }
};

int main()
{
    //デフォルトコンストラクタの呼び出し
    SimpleClass sc1;

    std::cout << "number: " << sc1.getNumber();
    std::cout << "\nname: " << sc1.getName() << std::endl;
    //number: 0 name:

    //引数ありコンストラクタの呼び出し
    SimpleClass sc2(1, "John");

    std::cout << "number: " << sc2.getNumber();
    std::cout << "\nname: " << sc2.getName() << std::endl;
    //number: 1 name: John

    std::cin.get();
}

引数ありコンストラクタに対して、引数なしコンストラクタのことをデフォルトコンストラクタ(既定のコンストラクタ)と言います。
引数ありコンストラクタは、受け取った値を利用してメンバ変数を初期化することができます。

C++では引数の数や種類が異なれば同じ名前の関数を複数定義することができますが、コンストラクタも複数同時に定義することができます。
(関数のオーバーロード。関数の新機能を参照)

デフォルトコンストラクタを定義せず、デフォルトコンストラクタ以外のコンストラクタを定義した場合、そのクラスにデフォルトコンストラクタは存在しなくなります。
(暗黙的に生成されない)
「インスタンスの生成時にメンバ変数の初期値の指定を強制したい」というような場合に有効です。

なお、以下のようにすれば「引数なし」「引数あり」のコンストラクタを一度に定義することができます。
(関数のデフォルト引数。関数の新機能を参照)


class SimpleClass
{
private:
    int number;
 
public:
    SimpleClass(int num = 0)
    {
        number = num;
    }
};

メンバイニシャライザ

メンバ変数の初期化はコンストラクタ内の処理でも可能ですが、メンバイニシャライザ(メンバー初期化子)という方法で行われることも多いです。


class SimpleClass
{
private:
    int number;
    std::string name;

public:
    //メンバイニシャライザ
    SimpleClass() : number(0), name("no name")
    {
        //メンバ変数は既に初期化されているので
        //コンストラクタ内では何もしなくて良い
    }
	//アクセサ省略
};

「コンストラクタ名() :」の後に記述されているのがメンバイニシャライザです。
メンバイニシャライザはメンバ変数名を定義した順番に記述し、丸括弧の中に初期化値を指定します。

この方法による初期化は代入による初期化よりも効率の良い処理が行われます。
コンストラクタ内で同じ変数に対して代入した場合は値が上書きされます。
(メンバイニシャライザ→コンストラクタの順)

コンストラクタに引数がある場合、メンバイニシャライザではその引数を使用できます。


class SimpleClass
{
private:
    int number;
    std::string name;

public:
    SimpleClass()
        : number(0), name("no name") {}
    SimpleClass(int n, const char* s)
        : number(n), name(s) {}
};

配列の初期化

メンバ変数に配列を持つ場合、初期化はメンバイニシャライザを使用する必要があります。


class SimpleClass
{
private:
    int arr[];

public:
    SimpleClass() : arr{ 1, 2, 3 }
    {
        //これはコンパイルエラー
        //arr = { 1, 2, 3 };
    }
};

コンストラクタのブロック内でのメンバ変数に対する初期化は、正確には初期化ではなく代入です。
配列の初期化子リストは名前の通り初期化時に使用できる方法で、代入時にはできません。

ただしこの書き方ができるのはC++11(2011年版のC++)からです。
それ以前は定数で必要な要素数を確保して、コンストラクタ内で各要素にひとつずつ代入する必要があります。
(面倒なのでvectorクラスを使ったほうが良いです)

デォルトメンバ初期化子

C++11以降では、メンバ変数は宣言と同時に初期化をすることもできます。


class SimpleClass
{
private:
    //宣言と同時に初期化
    int number = 0;
	char arr[3]{ 1, 2, 3 };
    std::string name = "no name";

	//配列の要素数の省略はできない
	//char arr2[]{ 1, 2, 3 };

public:
    SimpleClass()
    {
        //メンバ変数は既に初期化されているので
        //コンストラクタ内では何もしなくて良い
    }
};

これはデフォルトメンバ初期化子といいます。
同じメンバ変数に対してデフォルトメンバ初期化子とメンバイニシャライザの両方が指定されている場合はメンバイニシャライザが使用されます。

静的メンバ変数(static)はこの方法で初期化はできません。

委譲コンストラクタ

コンストラクタは複数定義できますが、それぞれの処理内容はほとんど同じになることが多いです。
そのような場合は、関数にしてまとめてしまう方法が考えられます。


class SimpleClass
{
private:
    int number;
    std::string name;

public:
    SimpleClass()
    {
        setData(0, "no name");
    }

    SimpleClass(int n, const char *s)
    {
        setData(n, s);
    }

    //メンバ変数の初期化関数
    void setData(int n, const char *s)
    {
        number = n;
        name = s;
    }
};

これは上で説明したメンバイニシャライザの時と同じく、効率の悪い初期化方法となります。
効率の良い初期化を行うには委譲コンストラクタを使用します。


class SimpleClass
{
private:
    int number;
    std::string name;

public:
    SimpleClass() : SimpleClass(0, "no name") //委譲コンストラクタ
    {
        //引数付きコンストラクタに処理を任せるので
        //ここでは何もしない
    }

    SimpleClass(int n, const char *s) : 
        number(n), name(s)
    {
        //メンバイニシャライザで初期化するので
        //ここでは何もしない
    }
};

委譲コンストラクタは、コンストラクタ名の後ろに「: コンストラクタ(引数)」という形式で、呼び出したい引数を持つコンストラクタを指定します。
上記コードでは、引数なしコンストラクタが呼び出された時は、引数付きのコンストラクタに処理を任せています。
こうすることで効率の良い初期化ができます。

なお、コンストラクタ内に処理を書いた場合の実行順は

  • 委譲されたコンストラクタのメンバイニシャライザ
  • 委譲されたコンストラクタ内の処理
  • 委譲元のコンストラクタ内の処理

となります。

new演算子とインスタンス

new演算子はクラスのインタンスをポインタで扱う場合にも使用されます。


//変数の宣言と同時にコンストラクタが実行される
//インスタンスは変数が直接持っている
SimpleClass sc1;

//ポインタ変数の宣言
//まだコンストラクタは実行されていない
//(メモリ確保されていない)
SimpleClass *sc2;

//コンストラクタが実行される
//インスタンスはポインタの参照先にある
sc2 = new SimpleClass();

//何か処理...

//sc2のインスタンスを破棄
delete sc2;
//これ以降sc2は使えない

//コンストラクタが実行される
//一度破棄したポインタ変数に再割り当ても可能
sc2 = new SimpleClass(1, "John");

//mallocはコンストラクタが実行されないのでNG
//sc2 = (SimpleClass*)(malloc(sizeof(SimpleClass)));

通常のクラス型変数を宣言した場合は、同時にデフォルトコンストラクタが実行されインスタンスが生成されます。
(デフォルトコンストラクタがない場合はコンパイルエラー)
クラスをポインタ変数で宣言した場合は、それだけではコンストラクタは実行されません。
ポインタ変数に対してはnew演算子でメモリ領域を確保し、その戻り値を代入します。
このとき、たとえ引数がなくてもクラス名の後ろに丸括弧()が必要です。

ポインタ変数でない方は、スコープを抜けるまでメモリを破棄することはできません。
スコープを抜けるということは変数自体にアクセスできなくなるということですから、同じ変数に別のインスタンスを再度割り当てることはできません。

ポインタ変数の場合は、delete演算子で任意のタイミングでインスタンスを破棄することができます。
インスタンスを破棄した後も、同じスコープ内であれば変数にアクセスすることができます。
new演算子で再度メモリを確保すれば、同じポインタ変数に別のインスタンスを割り当てることができます。
ただし、deleteで破棄する前に別のインスタンスを再割り当てすると以前のインスタンスを破棄する手段がなくなる(メモリリークが発生する)ので注意してください。

C言語のmalloc関数でメモリを確保した場合はコンストラクタは呼び出されません。
malloc関数は、あくまでもそのクラスで必要な分のメモリ領域を確保するだけの関数です。
クラスはコンストラクタが実行されることを前提として作られているものがほとんどなので、malloc関数でクラスのメモリを確保してはなりません。