C++編【言語解説】 第13章 コンストラクタとデストラクタ

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

コンストラクタ

オブジェクトをインスタンス化したとき、メンバ変数の値は未初期化な状態です。 C言語でも C++ でもそうですが、自動的に初期化されるということはありません。

前章までの Studentクラスでは、SetData というメンバ関数を用意して、 これを呼び出して初期値を与えるという形を取りましたが、 メンバ変数を初期化するという目的であれば、コンストラクタを利用するべきです。

コンストラクタは、オブジェクトがインスタンス化されるときに、自動的に呼び出される特殊なメンバ関数です。 「自動的に」というのがポイントで、このおかげで、オブジェクトが未初期化な状態になることを確実に防ぐことができ、 安全なプログラムが書けるようになります。

実際に、コンストラクタを使ってみます。

// student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>

class Student {
	std::string  mName;   // 名前
	int          mGrade;  // 学年
	int          mScore;  // 得点
	
public:
	Student();  // コンストラクタ

	void SetData(std::string name, int grade, int score);
	void Print();
};

#endif
// student.cpp

#include "student.h"
#include <iostream>

Student::Student()
{
	mName = "no name";
	mGrade = 0;
	mScore = 0;
}

void Student::SetData(std::string name, int grade, int score)
{
	mName = name;
	mGrade = grade;
	mScore = score;
}

void Student::Print()
{
	std::cout << mName << " "
	          << mGrade << " "
	          << mScore << std::endl;
}
// main.cpp

#include "student.h"

int main()
{
	Student student;  // インスタンス化される際に、コンストラクタが呼び出される
	student.Print();
	student.SetData("Saitou Takashi", 2, 80);
	student.Print();
}

実行結果:

no_name 0 0
Saitou Takashi 2 80

コンストラクタには、クラスと同じ名前を付けます。 また、戻り値というものが存在しないので、戻り値の型の指定は省略します(void ではいけません)。
なお、通常のメンバ関数と同様、クラス定義内で処理の中身を記述することも可能です。

このサンプルのように、コンストラクタでは主に、メンバ変数を初期化する作業を行います。 ただ、できるだけ、次の項で説明する方法を採用して下さい。

メンバイニシャライザ(初期化リスト)

コンストラクタでは、メンバ変数へ初期値を代入するようなコードを書くこともできますが、 メンバイニシャライザ(初期化リスト)と呼ばれる、 コンストラクタでだけ使用できる初期化構文を使うこともできます。
前の項のサンプルプログラムを、メンバイニシャライザを使って書き換えると、次のようになります。

// student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>

class Student {
	std::string  mName;   // 名前
	int          mGrade;  // 学年
	int          mScore;  // 得点
	
public:
	Student();  // コンストラクタ

	void SetData(std::string name, int grade, int score);
	void Print();
};

#endif
// student.cpp

#include "student.h"
#include <iostream>

Student::Student() :
	mName("no name"), mGrade(0), mScore(0)
{
}

void Student::SetData(std::string name, int grade, int score)
{
	mName = name;
	mGrade = grade;
	mScore = score;
}

void Student::Print()
{
	std::cout << mName << " "
	          << mGrade << " "
	          << mScore << std::endl;
}
// main.cpp

#include "student.h"

int main()
{
	Student student;  // インスタンス化される際に、コンストラクタが呼び出される
	student.Print();
	student.SetData("Saitou Takashi", 2, 80);
	student.Print();
}

実行結果:

no_name 0 0
Saitou Takashi 2 80

このように、「コンストラクタの名前 : メンバ変数名(初期値)…」のような構文になります。 コンストラクタの内容をクラス定義内に書く場合は、次のようになります。

// student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>

class Student {
	Student() :
		mName("no name"), mGrade(0), mScore(0)
	{}
};

#endif

メンバイニシャライザでのメンバ変数の記述順については、 クラス定義の中でメンバ変数を書いた順番通りにするのが基本です。 これは、メンバ変数それぞれのコンストラクタが呼び出される順番は、 クラス定義の中でメンバ変数を書いた順番と同じだからです。 余計な混乱を招かないように、順番を合わせておきましょう。

メンバイニシャライザの方が、普通に代入によって初期値を与えるよりも効率的なので、 常にメンバイニシャライザを使うようにして下さい。
オブジェクトがインスタンス化される際の流れは、次のような感じになっています。

  1. オブジェクトをインスタンス化する。
  2. クラスが持つメンバ変数のコンストラクタが起動し、中に書かれている処理が実行される。
  3. コンストラクタの内容が実行される。

2の部分で、メンバ変数のコンストラクタが呼び出されていて、それぞれ初期化済みになっています。 メンバイニシャライザを使わずに代入で初期化しようとすると、それは3のタイミングになりますから、 2と3とで、処理が重複してしまいます。
メンバイニシャライザの正体は、実のところ、コンストラクタへ引数を渡していることに他なりません。 これは、メンバ変数mName が std::string であることから想像してみると良いでしょう。 std::string の変数を宣言するときに、初期値を与えることができましたね?(第4章、または【標準ライブラリ】第2章参照)。

std::string str("Hello");  // "Hello" で初期化

これは実は、std::string のコンストラクタに、実引数 "Hello" を渡しているのです。 先ほどのサンプルプログラムで「: mName("no name")」と書いているのも同じことで、これも std::string のコンストラクタに、 実引数 "no name" を渡しています。
他のメンバ変数 (mGrade、mScore) は int型ですが、実はこういった組み込み型でも、コンストラクタを呼び出す構文が使えるようになっています。

int num;        // 未初期化
int num = 100;  // 100 で初期化 (C言語でも使える従来の記法)
int num(100);   // 100 で初期化 (C++ でのみ使える)

なお、std::string は引数を与えなくても、空文字列で初期化してくれます。 もし、mName の初期値も空文字列で構わないというのであれば、「mName()」のように書くか、 そもそも、メンバイニシャライザから削除してしまっても構いません。

コンストラクタのオーバーロード

コンストラクタは引数を持つこともできます。 また、コンストラクタをオーバーロードすることも可能なので、 引数の無いコンストラクタと、引数付きのコンストラクタのように、 複数のコンストラクタを持つことができます

// student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>

class Student {
	std::string  mName;   // 名前
	int          mGrade;  // 学年
	int          mScore;  // 得点
	
public:
	Student();  // コンストラクタ
	Student(std::string name, int grade, int score);  // コンストラクタ

	void SetData(std::string name, int grade, int score);
	void Print();
};

#endif
// student.cpp

#include "student.h"
#include <iostream>

Student::Student() :
	mName("no name"), mGrade(0), mScore(0)
{
}

Student::Student(std::string name, int grade, int score) :
	mName(name), mGrade(grade), mScore(score)
{
}

void Student::SetData(std::string name, int grade, int score)
{
	mName = name;
	mGrade = grade;
	mScore = score;
}

void Student::Print()
{
	std::cout << mName << " "
	          << mGrade << " "
	          << mScore << std::endl;
}
// main.cpp

#include "student.h"

int main()
{
	Student student;
	student.Print();
	
	Student student2("Saitou Takashi", 2, 80);
	student2.Print();
}

実行結果:

no_name 0 0
Saitou Takashi 2 80

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

ところで、コンストラクタを明示的に定義しなかった場合は、コンパイラが自動的にデフォルトコンストラクタを生成しますデフォルトコンストラクタとは、引数無しで呼び出すことができるコンストラクタのことです。 すべての引数にデフォルト値があり、省略することで、結果的に引数無しで呼び出せるのなら、それもデフォルトコンストラクタとみなせます。
引数無しでオブジェクトをインスタンス化するためには、デフォルトコンストラクタが必要です

明示的にコンストラクタを定義した場合、デフォルトコンストラクタは作られません。 そのため、次のように定義した Studentクラスには、引数が3つあるコンストラクタだけしか存在しません。

class Student {
public:
	Student(std::string name, int grade, int score);
};

この場合、デフォルトコンストラクタが存在しないので、引数無しでオブジェクトをインスタンス化することができません。

Student student;  // エラー

これが出来ないと、オブジェクトを配列で管理したいときに困ります。

Student students[10];  // 10個のオブジェクトは、それぞれ引数無しのコンストラクタで初期化される

オブジェクトを、動的なメモリ割り当ての手法を使って生成すれば、引数無しのコンストラクタが無くても、配列で管理することは可能です(第14章)。 ただ、1つ1つに渡す引数に一貫性が無い場合は、かなり面倒な記述になってしまいます。

C++11 (委譲コンストラクタ)

C++11

コンストラクタをオーバーロードする場合、結果的にその中で行っている処理は同じで、値だけが違うということがあります。 Studentクラスでも、メンバイニシャライザに与える値が変わるだけでした。 こういうとき、同じ処理を2か所に書くことを避けて、1か所にまとめたいところですが、 C++03以前では、コンストラクタとは異なる、通常のメンバ関数を用意する以外に手がありませんでした。

// student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>

class Student {
	std::string  mName;   // 名前
	int          mGrade;  // 学年
	int          mScore;  // 得点
	
public:
	Student();
	Student(std::string name, int grade, int score);

	void SetData(std::string name, int grade, int score);
	void Print();
};

#endif
// student.cpp

#include "student.h"
#include <iostream>

Student::Student()
{
	SetData("no name", 0, 0);
}

Student::Student(std::string name, int grade, int score)
{
	SetData(name, grade, score);
}

void Student::SetData(std::string name, int grade, int score)
{
	mName = name;
	mGrade = grade;
	mScore = score;
}

void Student::Print()
{
	std::cout << mName << " "
	          << mGrade << " "
	          << mScore << std::endl;
}
// main.cpp

#include "student.h"

int main()
{
	Student student;
	student.Print();
	
	Student student2("Saitou Takashi", 2, 80);
	student2.Print();
}

実行結果:

no_name 0 0
Saitou Takashi 2 80

SetDataメンバ関数が目的の処理を行っているので、これを使いまわしていますが、 このように別関数に処理をまとめて、コンストラクタの内部から呼び出すようにするしかありません。 この場合だと、本来、メンバイニシャライザを使いたいところなのに、代入による初期化になってしまうという問題があります。

C++11 では、委譲コンストラクタという機能が加わりました。 これは、あるコンストラクタの実装を、別のコンストラクタに任せるという機能です。 この機能を使うと、次のように書けます(違いは、student.cpp だけなので、ここだけ載せます)

// student.cpp

#include "student.h"
#include <iostream>

Student::Student() : Student("no name", 0, 0)
{
}

Student::Student(std::string name, int grade, int score) :
	mName(name), mGrade(grade), mScore(score)
{
}

void Student::SetData(std::string name, int grade, int score)
{
	mName = name;
	mGrade = grade;
	mScore = score;
}

void Student::Print()
{
	std::cout << mName << " "
	          << mGrade << " "
	          << mScore << std::endl;
}

引数無しのコンストラクタのメンバイニシャライザの部分に、「Student("no name", 0, 0)」と書かれています。 このように、別のコンストラクタを呼び出すことができます。 引数のあるコンストラクタの側は、以前のように、メンバイニシャライザを使った初期化を行っています。
これで、他の処理があったとしても、内容が同じであれば、呼び出し先の方のコンストラクタにだけ記述すれば良いので、 処理を1か所にまとめられます。

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

C++11 (非静的メンバ変数の定義箇所での初期化)

C++11

C++11 では、非静的なメンバ変数であれば、定義時に初期値を与えられるようになりました。 (静的なメンバ変数は、第15章で扱います)。

// student.h

#ifndef STUDENT_H
#define STUDENT_H

#include <string>

class Student {
	std::string  mName = "no name";   // 名前
	int          mGrade = 0;  // 学年
	int          mScore = 0;  // 得点
	
public:
	Student();
	Student(std::string name, int grade, int score);

	void SetData(std::string name, int grade, int score);
	void Print();
};

#endif
// student.cpp

#include "student.h"
#include <iostream>

Student::Student()
{
}

Student::Student(std::string name, int grade, int score) :
	mName(name), mGrade(grade), mScore(score)
{
}

void Student::SetData(std::string name, int grade, int score)
{
	mName = name;
	mGrade = grade;
	mScore = score;
}

void Student::Print()
{
	std::cout << mName << " "
	          << mGrade << " "
	          << mScore << std::endl;
}
// main.cpp

#include "student.h"

int main()
{
	Student student1;
	Student student2("Saitou Takashi", 2, 80);

	student1.Print();
	student2.Print();
}

実行結果:

no_name 0 0
Saitou Takashi 2 80

定義時に与えた初期値は、メンバイニシャライザによる初期化よりも更に前で行われます。 そのため、メンバイニシャライザや、コンストラクタ内で与える値によって上書きすることができます。

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

デストラクタ

確実な初期化を実現するのがコンストラクタならば、確実な終了処理を実現するのがデストラクタです。 デストラクタは、オブジェクトが記憶域期間(C言語編第34章参照)を終えて解体されるときに、 自動的に呼び出される特殊なメンバ関数です。

#include <iostream>

class MyClass {
public:
	MyClass(const char* s) : mStr(s)
	{
		std::cout << "MyClass(" << mStr << ")" << std::endl;
	}

	~MyClass()
	{
		std::cout << "~MyClass(" << mStr << ")" << std::endl;
	}

private:
	const char* mStr;
};


void func()
{
	MyClass c("func");
}  // ここで c の記憶域期間が終わり、デストラクタが呼び出される

int main()
{
	MyClass c("main");
	func();
}  // ここで c の記憶域期間が終わり、デストラクタが呼び出される

実行結果:

MyClass(main)
MyClass(func)
~MyClass(func)
~MyClass(main)

デストラクタは、「~MyClass()」のように、クラスの名前の頭に「~」を付けた名前で表します。 このサンプルプログラムから分かるように、デストラクタが呼び出される箇所には、これといった処理を記述できる隙はないので、 引数を渡すこともできません。 つまり、デストラクタに引数はありません

デストラクタでは主に、動的なメモリ割り当てを行ったメンバ変数を解放したり、 使用中のファイルを close したりといった、確実に行っておく必要がある後片付けを記述します。

なお、メンバ変数のデストラクタは、コンストラクタが呼ばれた順番と正反対の順番で呼び出されます。 コンストラクタは、クラス内でメンバ変数を宣言した順番に呼び出されるので、デストラクタは宣言順の逆になるということです。

C++11 (自動生成されるメンバ関数の明示)

C++11

コンストラクタやデストラクタは、プログラマが明示的に実装しなければ、コンパイラが自動生成します。 C++11 では、コンパイラが生成するコンストラクタやデストラクタを使用することを、明示的に表す手段が追加されました。

class Student {
public:
	Student() = default;
	~Student() = default;
};

このように、宣言時に末尾に「= default;」を追加します。 もちろん、中身は自動生成されたものを使うので、自分で書くことはできません。

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。

コンパイラが自動生成するメンバ関数には、コンストラクタ、デストラクタの他に、 コピーコンストラクタ(第16章)、代入演算子(第16章)があります。 また、C++11 からは、ムーブコンストラクタとムーブ代入演算子が追加されています。

C++11 (関数定義の削除)

C++11

C++11 では、コンストラクタやデストラクタを、コンパイラに自動生成させないように制御することができます。

class Student {
public:
	Student() = delete;
};

このように、宣言時に末尾に「= delete;」を追加します。
この例の場合、唯一のコンストラクタであるデフォルトコンストラクタが実装されないため、 結果的にオブジェクトをインスタンス化できなくなります。 デストラクタに対して使うことも可能ですが、その場合、破棄することができないということになり、恐らく価値がありません。

また、この機能は、通常の関数やメンバ関数に対しても使用できます。 例えば、

class Data {
public:
	void SetValue(int num);
	void SetValue(double num) = delete;
};

この場合、SetValueメンバ関数を double型の実引数を渡して呼び出そうとすると、コンパイルエラーになります。 double型から int型への暗黙の型変換が行われて、int型バージョンの SetValueメンバ関数が呼び出されるということもありません。

この機能の意味合いとしては、メンバ関数の宣言はあるが、定義は無いという状態を作るということです。 そのため、実際に呼び出そうとしたり、関数のアドレスを取ろうとしたりすると、その時点でコンパイルエラーになります。

この機能は、VisualC++ 2013/2015/2017、clang 3.7 のいずれでも使用できます。


練習問題

問題@ Studentクラスが持つ mName の型を std::string でなく、char*型で管理したいとします。 どのように実装しますか?

問題A メンバイニシャライザを使った初期化と、コンストラクタ内で代入によって初期値を設定する方法とで、 パフォーマンスにどの程度の違いがあるか、計測して下さい。 (パフォーマンス測定マクロが、コードライブラリにあります)

問題B int型の変数の値を退避(保存)させておき、最後に確実に元の値を復元することをサポートするようなクラスを設計して下さい。 つまり、次のような挙動になるようにして下さい (X がクラスとします)。

int value = 10;

// この関数を呼び出したときに value が 10 なら、
// 抜け出した後も確実に 10 であるようにしたい。
void func()
{
	X store(/* 引数は任意 */);

	value = 50;
	
	if (/* 何らかの条件式 */) {
		return;
	}
	
	value = 100;
}


解答ページはこちら

参考リンク

更新履歴

'2017/5/20 「コンストラクタ」の項を分離して、 「メンバイニシャライザ」、「コンストラクタのオーバーロード」を追加。 同時に、解説を追加。

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/4/29 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ