コンストラクタ

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

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


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(char* s) { name = s; }
};

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


int main()
{
    SimpleClass sc;

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

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

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

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


#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(char* s) { name = s; }
};

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

コンストラクタは

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

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

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

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

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

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

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


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

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

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

public:
    SimpleClass(){}
};


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

メンバイニシャライザ

コンストラクタでの初期化はメンバ変数にそのまま値を代入して行っても良いですが、メンバイニシャライザという方法で行われることも多いです。


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

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

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

この方法による初期化は代入による初期化よりも効率の良い処理が行われます。
まだクラスについてほとんど説明していないので構文が良くわからないと思います。
効率化のための方法なので今はあまり気にしなくても良いですが、「これはメンバ変数の初期化だ」という程度では知っておいてください。

非静的メンバ変数の初期化

さらに、メンバ変数は宣言と同時に初期化をすることもできます。


class SimpleClass
{
private:
    //宣言と同時に初期化
    int number = 0;
    std::string name = "no name";

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

この方法による初期化は、コンストラクタやメンバイニシャライザなどによる初期化よりも前に行われます。
コンストラクタやメンバイニシャライザによって値を上書きすることも可能です。

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

引数付きコンストラクタ

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


#include <iostream>
#include <string>

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

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

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

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

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

int main()
{
    //デフォルトコンストラクタの呼び出し
    SimpleClass sc1;
    //number: 0 name:
    std::cout << "number: " << sc1.getNumber();
    std::cout << "\nname: " << sc1.getName() << std::endl;

    //引数付きコンストラクタの呼び出し
    SimpleClass sc2(1, "John");
    //number: 1 name: John
    std::cout << "number: " << sc2.getNumber();
    std::cout << "\nname: " << sc2.getName() << std::endl;

    std::cin.get();
}

引数付きコンストラクタに対して、引数なしコンストラクタのことをデフォルトコンストラクタと言います。

引数付きコンストラクタは、受け取った値を利用してメンバ変数を初期化することができます。
呼び出し側では、すでに初期化された状態のインスタンスを生成することができます。

引数付きコンストラクタの呼び出しは関数呼び出し演算子()を使用しますが、デフォルトコンストラクタを呼び出す場合には()は付けないように気を付けてください。
()を付けるとエラーになります。

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

引数付きコンストラクタを定義し、デフォルトコンストラクタを省略した場合、デフォルトコンストラクタは自動的に生成されなくなります。
「インスタンスの生成時にメンバ変数の初期値の指定を強制したい」というような場合に有効です。

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


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

委譲コンストラクタ

コンストラクタを複数定義しても、内容自体は結局メンバ変数の初期化という同じ処理になることが多いです。
そのような場合は、関数にしてまとめてしまう方法が考えられます。


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

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

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

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

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


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

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

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

引数なしコンストラクタが呼び出された時は、引数付きのコンストラクタに処理を任せてしまいます。
この方法はメンバイニシャライザと全く同じで、呼び出されたコンストラクタ自体は何もせず、処理を任せてしまうのです。
こうすることで、効率の良い初期化ができます。

new演算子はコンストラクタを呼び出す

new演算子はメモリを動的に確保するための演算子ですが、クラスのインタンスを生成することもできます。


//インスタンスの宣言と同時にコンストラクタが実行される
SimpleClass sc1;

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

//new演算子の働きによりコンストラクタが実行される
sc2 = new SimpleClass();

//何か処理...

//sc2を破棄
delete sc2;
//これ以降sc2は使えない

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

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

new演算子を使わない、通常のインスタンスの場合は、変数の宣言と同時にコンストラクタが実行されます。
ポインタでインスタンスを宣言した場合は、それだけではコンストラクタは実行されていません。
ポインタ変数に対してはnew演算子でメモリ領域を確保し、その戻り値を代入します。

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

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

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