C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。
この章の概要です。
前章までの Studentクラスでは、SetData というメンバ関数を用意して、メンバ変数📘に初期値を与えるという形を取りましたが、本来は、メンバ変数を初期化するためには、コンストラクタ📘を利用するべきです。
コンストラクタは、オブジェクト📘がインスタンス化📘されるときに、自動的に呼び出される特殊なメンバ関数です。「自動的に」というのがポイントで、このおかげで、オブジェクトが未初期化な状態になることを確実に防ぐことができ、安全なプログラムが書けます。
コンストラクタには、クラスと同じ名前を付けます。また、何か結果を返す目的のメンバ関数ではないため、戻り値はなく、戻り値型の指定もしません(「void」を書くこともできません)。
class X {
(仮引数の並び); // コンストラクタの宣言
X};
// コンストラクタの定義
::X(仮引数の並び)
X{
}
クラスと構造体は同一の存在なので(第12章)、struct でもコンストラクタは使えます。
クラス定義内でコンストラクタの定義を記述することも可能です。これは、inlineキーワードを指定したことと同じ意味を持ちます(第12章)。
コンストラクタを constメンバ関数にできませんが、const付きのオブジェクトを定義する場合でもコンストラクタは呼び出されるので、特に必要性もありません。
構文の例なので省いていますが、コンストラクタもアクセス指定子📘の影響を受けます。publicキーワードを使って「公開」しておかないと、クラスの外側でインスタンス化をおこなうことができなくなります。
インスタンス化をおこなうときに、コンストラクタが呼び出せない場合は、コンパイルエラーになります。
実際に、コンストラクタを使ってみます。
// student.h
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED
#include <string>
class Student {
public:
(); // コンストラクタ
Student
void SetData(std::string name, int grade, int score);
void Print();
private:
std::string mName; // 名前
int mGrade; // 学年
int mScore; // 得点
};
#endif
// student.cpp
#include "student.h"
#include <iostream>
::Student()
Student{
= "no name";
mName = 0;
mGrade = 0;
mScore }
void Student::SetData(std::string name, int grade, int score)
{
= name;
mName = grade;
mGrade = score;
mScore }
void Student::Print()
{
std::cout << mName << " "
<< mGrade << " "
<< mScore << std::endl;
}
// main.cpp
#include "student.h"
int main()
{
; // インスタンス化される際に、コンストラクタが呼び出される
Student student.Print();
student.SetData("Saitou Takashi", 2, 80);
student.Print();
student}
実行結果:
no_name 0 0
Saitou Takashi 2 80
このサンプルプログラムのように、コンストラクタでは主に、メンバ変数を初期化する作業を行います。ただしできるだけ、次の項で説明する方法を採用してください。
コンストラクタには戻り値がないため、コンストラクタ内で発生したエラーを呼び出し元に伝えるには代わりの手段が必要です。1つの策として、エラーをいったん無視しておいて、あとからメンバ関数経由でエラーの有無を調べられるようにしておく方法があります。この方法は、std::ofstream で、インスタンス化のあとに「if (!ofs)」のようにして問い合わせる形と同じです(第6章)。
もう1つの方法として、例外を使うという手段があります(第32章)。必ずチェックされなければならないエラーを伝えるには、こちらの方が確実です。
先ほどのサンプルプログラムのような方法でメンバ変数を初期化しようとすると、結局、代入を使っていることになります。
::Student()
Student{
// 以下の3つは、すべて代入
= "no name";
mName = 0;
mGrade = 0;
mScore }
これの問題点の1つは、無駄があるということです。メンバ変数自身もコンストラクタを持つことに注意しましょう。つまり、オブジェクトがインスタンス化されるとき、コンストラクタの本体が実行される前に、メンバ変数が作られ、コンストラクタが呼び出されています。
そのため、各メンバ変数はコンストラクタによって初期化された後、さらに代入によって上書きされるという無駄な手順を踏んでしまっています。最初から適切な初期値で初期化された方が無駄がありません。
そこで、メンバイニシャライザという機能を使用します。メンバイニシャライザは、コンストラクタでのみ使える機能で、次のような構文になっています。
// コンストラクタの定義
::X() : メンバ変数名(初期化子), …
X{
}
厳密には、メンバ変数1つ1つの初期化を行っている部分がメンバイニシャライザで、全体としてはメンバイニシャライザリストと呼びます。
コンストラクタの定義の側で記述します。宣言の方には書けません。
コンストラクタの仮引数の並びの後ろに「:」を置き、メンバ変数名と、それに与える初期化子を指定します。メンバ変数が複数あるのなら、「,」で区切って、指定を繰り返します。
コンストラクタの本体のコードが実行される前に、メンバ変数が作られるタイミングで、メンバイニシャライザが機能します。メンバイニシャライザで記述した初期化子は、メンバ変数のコンストラクタの実引数として渡されます。
メンバ変数のコンストラクタが、引数無しで呼び出せるのであれば、メンバイニシャライザに記述しなくても結果は変わりませんが、空の ( ) を指定して、明示的に初期化できます。明示しておいた方が分かりやすいかもしれません。
なお、メンバ変数が const の場合、メンバイニシャライザで初期化しなければなりません。コンストラクタ内で初期化しようとすると、それは代入ですから、const の性質によってコンパイルエラーになってしまいます。
前の項のサンプルプログラムを、メンバイニシャライザを使って書き換えると、次のようになります。
// student.h
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED
#include <string>
class Student {
public:
(); // コンストラクタ
Student
void SetData(std::string name, int grade, int score);
void Print();
private:
std::string mName; // 名前
int mGrade; // 学年
int mScore; // 得点
};
#endif
// student.cpp
#include "student.h"
#include <iostream>
::Student() :
Student("no name"), mGrade(0), mScore(0)
mName{
}
void Student::SetData(std::string name, int grade, int score)
{
= name;
mName = grade;
mGrade = score;
mScore }
void Student::Print()
{
std::cout << mName << " "
<< mGrade << " "
<< mScore << std::endl;
}
// main.cpp
#include "student.h"
int main()
{
; // インスタンス化される際に、コンストラクタが呼び出される
Student student.Print();
student.SetData("Saitou Takashi", 2, 80);
student.Print();
student}
実行結果:
no_name 0 0
Saitou Takashi 2 80
mName は std::string型であり、その正体はクラス型で、コンストラクタを持っています。メンバイニシャライザに記述した「mName(“no name”)」は、std::string のコンストラクタに “no name” を渡していることになります。
一方、mGrade や mScore は int型であり、クラス型ではありませんが、「mGrade(0)」や「mScore(0)」という記述で初期化できます。このような初期化の仕方は、普段の変数定義時でも可能です。後で取り上げます。
メンバイニシャライザでメンバ変数を記述する順番は、クラス定義の中でメンバ変数を宣言した順番どおりにするようにします。メンバ変数のコンストラクタが呼び出される順番は、クラス定義の中でメンバ変数を宣言した順番に合わせられるルールだからです。余計な混乱を招かないように、順番を合わせておきましょう。
コンストラクタを明示的に定義しなかった場合に限って、コンパイラが自動的にデフォルトコンストラクタを生成します。
デフォルトコンストラクタとは、引数無しで呼び出すことができるコンストラクタのことです。デフォルトで生成されるからデフォルトコンストラクタなのではないことに注意してください。
前章までの例では、コンストラクタを定義していませんでしたが、それでもオブジェクトをインスタンス化できていたのは、コンパイラが自動的にデフォルトコンストラクタを生成しているからです。
class Student {
private:
std::string mName; // 名前
int mGrade; // 学年
int mScore; // 得点
};
int main()
{
; // デフォルトコンストラクタを呼び出している
Student student}
コンパイラが生成したコンストラクタは、特に何も中身がない空実装のコンストラクタです。つまり、このサンプルプログラムでは、次のようなコンストラクタが自動生成されていると考えられます。
::Student()
Student{
}
メンバ変数を明示的に初期化することもしていませんから、int型の mGrade と mScore は不定値のままです。mName は std::string のデフォルトコンストラクタが呼び出されるので、空文字列で初期化されています。
デフォルトコンストラクタがないと、オブジェクトの配列が作れません。次のサンプルプログラムでは、コンストラクタを明示的に定義しているため、デフォルトコンストラクタが作られません。
class Student {
public:
(std::string name, int grade, int score);
Student
private:
std::string mName; // 名前
int mGrade; // 学年
int mScore; // 得点
};
::Student(std::string name, int grade, int score) :
Student(name), mGrade(grade), mScore(score)
mName{
}
int main()
{
[10]; // コンパイルエラー
Student students}
【上級】オブジェクトを、動的メモリ割り当て📘の手法を使って生成すれば、デフォルトコンストラクタがなくても、配列を使うことは可能です(第14章)。
なお、コンストラクタのすべての仮引数にデフォルト実引数があり、結果的に引数無しで呼び出せるのなら、それもデフォルトコンストラクタとみなせます。
ここまでのサンプルプログラムでは、コンストラクタの存在意義は、メンバ変数が不定値にならないようにすることでした。結局、適切な値を入れる作業は SetDataメンバ関数に任されており、ある種の2度手間状態は変わっていません。
今度は、オブジェクトがインスタンス化される時点で、適切な値を与えられるようにしてみましょう。そのためには、コンストラクタが引数を持つ必要があります。
コンストラクタに引数を持たせることについては、別段特別に考えるほどのことはありません。コンストラクタ側の宣言や定義は、これといって特別なことはありません。違いはせいぜい、インスタンス化するときに実引数を渡すようになる点ぐらいです。
// 引数付きのコンストラクタを使って、オブジェクトをインスタンス化する
(実引数の並び); クラス名 変数名
実際に試してみましょう。
// student.h
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED
#include <string>
class Student {
public:
(std::string name, int grade, int score);
Student
void Print();
private:
std::string mName; // 名前
int mGrade; // 学年
int mScore; // 得点
};
#endif
// student.cpp
#include "student.h"
#include <iostream>
::Student(std::string name, int grade, int score) :
Student(name), mGrade(grade), mScore(score)
mName{
}
void Student::Print()
{
std::cout << mName << " "
<< mGrade << " "
<< mScore << std::endl;
}
// main.cpp
#include "student.h"
int main()
{
("Saitou Takashi", 2, 80);
Student student.Print();
student}
実行結果:
no_name 0 0
Saitou Takashi 2 80
この形ならば、の無駄がありません。メンバ変数は最初から適切な値で初期化できています。
コンストラクタもオーバーロード📘できます。
// student.h
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED
#include <string>
class Student {
public:
();
Student(std::string name, int grade, int score);
Student
void SetData(std::string name, int grade, int score);
void Print();
private:
std::string mName; // 名前
int mGrade; // 学年
int mScore; // 得点
};
#endif
// student.cpp
#include "student.h"
#include <iostream>
::Student() :
Student("no name"), mGrade(0), mScore(0)
mName{
}
::Student(std::string name, int grade, int score) :
Student(name), mGrade(grade), mScore(score)
mName{
}
void Student::SetData(std::string name, int grade, int score)
{
= name;
mName = grade;
mGrade = score;
mScore }
void Student::Print()
{
std::cout << mName << " "
<< mGrade << " "
<< mScore << std::endl;
}
// main.cpp
#include "student.h"
int main()
{
;
Student student.Print();
student
("Saitou Takashi", 2, 80);
Student student2.Print();
student2}
実行結果:
no_name 0 0
Saitou Takashi 2 80
オブジェクトをインスタンス化するときに与える実引数に応じて、適切なコンストラクタが呼び分けられます。このルールは、関数オーバーロードのところで解説したとおりです(第8章)。
コンストラクタをオーバーロードすると、メンバイニシャライザや、本体のコードがほぼ同じ形になることがあり、共通化を図りたくなりますが、「非公開」のメンバ関数に括り出すぐらいしかできません。代償として、メンバイニシャライザの使用を諦める必要に迫られることがあります。
メンバイニシャライザで「変数名(初期値)」のように、( ) を使って初期化を行いました。この記法はメンバイニシャライザに限らず、変数を宣言して初期化をおこなうときにも使えます。
変数を宣言して、初期値を与える構文には、以下のものがあります。
int num = 0;
int num = {0};
int num(0);
{ } を使う方法は、配列などを初期化するときに複数の初期化子を指定するために用いますが、単独の変数に対してでも使用できます。単独の変数に対して使う場合、1つ目の書き方とまったく同じ意味です。
3つ目の方法では、型がクラス型の場合は複数の初期化子を、クラス型でない場合は単一の初期化子を指定できます。クラス型の場合は、コンストラクタに対する実引数の指定です。
1つ目と2つ目はコピー初期化、3つ目は直接初期化と呼ばれています。
コピー初期化には「=」が使われていて、直接初期化には使われていないというふうに見えますが、この見分け方は正しくありません。たとえば、関数呼び出しの際に実引数を渡して、仮引数を初期化する行為や、戻り値を返すことはコピー初期化です。
【上級】コピー初期化には他に、例外(第32章)を送出することや、例外を補足することが含まれています。
直接初期化には、メンバイニシャライザでの初期化や、関数形式キャストや static_cast による初期化があります。
const_cast や reinterpret_cast がないことを疑問に思うかもしれませんが、これらのキャストは新しいオブジェクトを生み出さないので、初期化という概念が登場しないからです。
【上級】直接初期化の方には他に、new(第14章)による初期化が含まれています。
コピー初期化と直接初期化の違いは、初期化に先立って、ユーザー定義の型変換📘の処理(この後で取り上げます)を適用するかどうかです。コピー初期化では適用しますが、直接初期化では適用しません。
この型変換処理が挟まることによって、実行効率を落としている可能性があります。そのため、変数宣言時の初期化には、コピー初期化よりも直接初期化を選ぶ方が良いかもしれません。
ところで、以下のように、初期化子を与えずに変数を宣言することがあります。
int num;
int num(); // コンパイルエラー
1つ目の方は、この変数が static でないローカル変数であれば、未初期化なように見えます。int型のようにクラス型でない型ならばそのとおりですが、クラス型の場合は、デフォルトコンストラクタが呼び出されます。この初期化方法は、デフォルト初期化と呼ばれています。
2つ目の方法は、コンパイルエラーになります。これは、C++ の文法解析の厄介な問題として有名なのですが、この変数宣言全体の形が、関数を宣言しているように見えてしまうため、エラーになります。つまり、「int」が戻り値型、「num」が関数名、「()」が仮引数のリストに見えるということです。
ただし、メンバイニシャライザのように、文法的に曖昧にならない場面であれば、空の () で初期化することは可能です。この場合、値初期化(第7章)されます。
実引数を1つだけ指定して呼び出すことができるコンストラクタは、変換コンストラクタと呼ばれます。変換コンストラクタは、そのクラスとは異なる型から、クラスをインスタンス化するものです。
引数が2つ以上あるコンストラクタでも、2つ目以降の引数がデフォルト実引数(第8章)を持つならば、やはり変換コンストラクタと呼びます。
たとえば、ファイルを扱う Fileクラスのコンストラクタを、次のように宣言しているとします。
class File {
public:
(const char* fileName);
File};
const char*型の引数を1つだけ指定して呼び出せますから、これは変換コンストラクタとして機能します。これがあると、コピー初期化の構文であれば、次のようにインスタンス化できます。
= "test.bin"; // コピー初期化なら OK
File file ("test.bin"); // 直接初期化ではコンパイルエラー File file
コピー初期化の場合は、ユーザー定義の型変換処理を適用しようするため、変換コンストラクタが機能します。直接初期化では、適用しないためコンパイルエラーになります。
【上級】変換コンストラクタだけがユーザー定義の型変換ということではありません。ほかに変換演算子(第19章)があります。
変換コンストラクタがあると、次のような意図しないであろう使い方が可能になってしまいます。
void func(File f);
("Hello"); // 意図どおり? func
仮引数が File型の関数に、“Hello” という文字列リテラルを渡そうとしています。おそらく、意図したものではないであろう使い方ですが、const char*型からインスタンス化をおこなう変換コンストラクタがあるため、コンパイルに成功します。この場面で、変換コンストラクタが機能してしまうのは、引数や戻り値を使っておこなう初期化はコピー初期化だからです。
このように、変換コンストラクタが定義されていると、暗黙の型変換📘が行われ、コードが分かりづらくなることがあります。暗黙の型変換が行われても分かりづらくならないかどうか、よく検討してください。
暗黙的に型変換されることは望ましくないが、型変換自体は有用であるのなら、暗黙的に機能しないようにできます。そのためには、変換コンストラクタの宣言に、explicit指定子を付加します。
class X {
explicit X(仮引数の並び);
};
::X(仮引数の並び)
X{
}
explicit の付いた変換コンストラクタは、直接初期化のときにだけ使用され、コピー初期化では使用されません。
class File {
public:
explicit File(const char* fileName);
};
void func(File f);
int main()
{
= "test.bin"; // コンパイルエラー(コピー初期化)
File file ("test.bin"); // OK(直接初期化)
File file
// コンパイルエラー(引数の受け渡しはコピー初期化)
("Hello");
func
// OK(static_cast は直接初期化)
(static_cast<File>("Hello"));
func
// OK(関数形式キャストは直接初期化)
(File("Hello"));
func}
実引数を1つ指定するだけで呼び出せるコンストラクタを作る際、それが、変換コンストラクタを作っているというつもりでないのなら(たまたま引数が1個になるというだけならば)、つねに explicit を付けるようにした方が良いです。
確実な初期化を実現する機能がコンストラクタならば、確実な終了処理を実現するのがデストラクタ📘です。デストラクタは、オブジェクトが記憶域期間(C言語編第35章参照)を終えて解体されるときに、自動的に呼び出される特殊なメンバ関数です。
デストラクタには、「~クラス名」という名前を付けます。コンストラクタと同様に、戻り値はないので、戻り値型の指定もしません。また、解体時に自動的に呼び出されるという性質上、情報を渡すこともできないので、引数もありません。
class X {
~X(); // デストラクタの宣言
};
// デストラクタの定義
::~X()
X{
}
クラスと構造体は同一の存在なので(第12章)、struct でもデストラクタは使えます。
クラス定義内でデストラクタの定義を記述することも可能です。これは、inlineキーワードを指定したことと同じ意味を持ちます(第12章)。
デストラクタを constメンバ関数にできませんが、const付きのオブジェクトを定義する場合でもデストラクタは呼び出されるので、特に必要性もありません。
構文の例なので省いていますが、デストラクタもアクセス指定子の影響を受けます。publicキーワードを使って「公開」しておかないと、クラスの外側でオブジェクトが解体できなくなります。
オブジェクトが解体できないような使い方をすると、コンパイルエラーになります。
【上級】デストラクタを「非公開」にしても、メンバ関数やフレンド(第25章)からは解体できるため、使い方次第ではコンパイルエラーは起きません。
実際に、デストラクタを使ってみます。
#include <iostream>
#include <string>
class MyClass {
public:
(std::string s) : mStr(s)
MyClass{
std::cout << "MyClass(" << mStr << ")" << std::endl;
}
~MyClass()
{
std::cout << "~MyClass(" << mStr << ")" << std::endl;
}
private:
std::string mStr;
};
void func()
{
("func");
MyClass c} // ここで c の記憶域期間が終わり、デストラクタが呼び出される
int main()
{
("main");
MyClass c();
func} // ここで c の記憶域期間が終わり、デストラクタが呼び出される
実行結果:
MyClass(main)
MyClass(func)
~MyClass(func)
~MyClass(main)
デストラクタでは主に、動的メモリ割り当て📘を行ったメンバ変数を解放したり、使用中のファイルを close したりといった、確実に行っておく必要がある後片付けを記述します。std::string が内部で動的に確保した文字列の領域を解放📘する仕組みもデストラクタです。
なお、プログラマーがデストラクタを明示的に定義しなければ、コンパイラが自動生成します。自動生成されたデストラクタの実装は空です。
【上級】あまり意味が無さそうに思えますが、デストラクタが呼び出せないと、オブジェクトを破棄できません。たとえば、デストラクタがあっても、それが「非公開」であることが理由で呼び出させなければ、そのオブジェクトは破棄できず、コンパイルエラーになります。自動生成されるデストラクタは「公開」されています。
デストラクタの本体の処理が実行された後、メンバ変数それぞれのデストラクタも呼び出されます。メンバ変数のデストラクタを呼び出すときの順番は、コンストラクタが呼ばれた順番の逆です。コンストラクタは、メンバ変数の宣言順に呼び出されるので(前述)、デストラクタはその逆順で呼ばれます。
#include <iostream>
#include <string>
class MyClass {
public:
(std::string s) : mStr(s)
MyClass{
std::cout << "MyClass(" << mStr << ")" << std::endl;
}
~MyClass()
{
std::cout << "~MyClass(" << mStr << ")" << std::endl;
}
private:
std::string mStr;
};
class Test {
public:
() : mA("A"), mB("B"), mC("C")
Test{
std::cout << "Test()" << std::endl;
}
~Test()
{
std::cout << "~Test()" << std::endl;
}
private:
;
MyClass mA;
MyClass mB;
MyClass mC};
int main()
{
;
Test test}
実行結果:
MyClass(A)
MyClass(B)
MyClass(C)
Test()
~Test()
~MyClass(C)
~MyClass(B)
~MyClass(A)
想定どおりの順番で呼び出されているようです。するべきではありませんが、もしもメンバイニシャライザの記述順序を変えたとしても、この結果は変わりません。
【上級】デストラクタには戻り値がないため、デストラクタ内で発生したエラーを呼び出し元に伝えることができません。そもそも、デストラクタのような終了処理系の関数は、つねに成功するように作るべきです。終了処理が成功しないということは、もはや取り返しが付かない状況に陥っていることになるため、プログラムはただちに異常終了された方が良いでしょう。
問題① Studentクラスが持つ mName の型を std::string でなく、char*型で管理したいとします。どのように実装しますか?
問題② メンバイニシャライザを使った初期化と、コンストラクタ内で代入によって初期値を設定する方法とで、パフォーマンスにどの程度の違いがあるか、計測してください(パフォーマンス測定マクロが、コードライブラリにあります)。
問題③ int型の変数の値を退避(保存)させておき、最後に確実に元の値を復元することをサポートするようなクラスを設計してください。つまり、次のような挙動になるようにしてください (X がクラスとします)。
int value = 10;
// この関数を呼び出したときに value が 10 なら、
// 抜け出した後も確実に 10 であるようにしたい。
void func()
{
(/* 引数は任意 */);
X store
= 50;
value
if (/* なんらかの条件式 */) {
return;
}
= 100;
value }
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
![]() |
管理者情報 | プライバシーポリシー |