コンストラクタとデストラクタ | Programming Place Plus Modern C++編【言語解説】 第7章

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


コンストラクタ 🔗

前章までの Studentクラスでは、SetData というメンバ関数を用意して、メンバ変数^に初期値を与えるという形を取りましたが、本来は、メンバ変数を初期化するためには、コンストラクタを利用するべきです。

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

コンストラクタには、クラスと同じ名前を付けます。また、何か結果を返す目的のメンバ関数ではないため、戻り値はなく、戻り値型の指定もしません(「void」を書くこともできません)。

class X {
    X(仮引数の並び);  // コンストラクタの宣言
};

// コンストラクタの定義
X::X(仮引数の並び)
{
}

クラスと構造体は同一の存在なので(第6章)、struct でもコンストラクタは使えます。

クラス定義内でコンストラクタの定義を記述することも可能です。これは、inlineキーワードを指定したことと同じ意味を持ちます(第6章)。

コンストラクタを constメンバ関数にできませんが、const付きのオブジェクトを定義する場合でもコンストラクタは呼び出されるので、特に必要性もありません。

構文の例なので省いていますが、コンストラクタもアクセス指定子の影響を受けます。publicキーワードを使って「公開」しておかないと、クラスの外側でインスタンス化を行うことができなくなります。

インスタンス化を行うときに、コンストラクタが呼び出せない場合は、コンパイルエラーになります。

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

// Student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

class Student {
public:
    Student();  // コンストラクタ

    void SetData(const char* name, int grade, int score);

    const char* GetName() const;
    int GetGrade() const;
    int GetScore() const;

private:
    char  mName[32];   // 名前
    int   mGrade;      // 学年
    int   mScore;      // 得点
};

#endif
// Student.cpp

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

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

void Student::SetData(const char* name, int grade, int score)
{
    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
}

const char* Student::GetName() const
{
    return mName;
}

int Student::GetGrade() const
{
    return mGrade;
}

int Student::GetScore() const
{
    return mScore;
}
// main.cpp

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

namespace {
    void PrintStudentData(const Student* student)
    {
        std::cout << "name: " << student->GetName() << "\n"
                  << "grade: " << student->GetGrade() << "\n"
                  << "score: " << student->GetScore() << "\n"
                  << std::endl;
    }
}

int main()
{
    Student student;  // インスタンス化される際に、コンストラクタが呼び出される
    PrintStudentData(&student);

    student.SetData("Saitou Takashi", 2, 80);
    PrintStudentData(&student);
}

実行結果:

name: no name
grade: 0
score: 0

name: Saitou Takashi
grade: 2
score: 80

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

コンストラクタには戻り値がないため、コンストラクタ内で発生したエラーを呼び出し元に伝えるには代わりの手段が必要です。1つの策として、エラーをいったん無視しておいて、あとからメンバ関数経由でエラーの有無を調べられるようにしておく方法があります。

もう1つの方法として、例外を使うという手段があります(第19章)。必ずチェックされなければならないエラーを伝えるには、こちらの方が確実です。

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

先ほどのサンプルプログラムのような方法でメンバ変数を初期化しようとすると、結局、代入を使っていることになります。

Student::Student()
{
    // 以下の3つは、初期化ではない
    std::strcpy(mName, "no name");
    mGrade = 0;
    mScore = 0;
}

これの問題点の1つは、無駄があるということです。メンバ変数自身もコンストラクタを持つことに注意しましょう。つまり、オブジェクトがインスタンス化されるとき、コンストラクタの本体が実行される前に、メンバ変数が作られ、コンストラクタが呼び出されています。

そのため、各メンバ変数はコンストラクタによって初期化された後、さらに代入によって上書きされるという無駄な手順を踏んでしまっています。最初から適切な初期値で初期化された方が無駄がありません。

そこで、メンバイニシャライザという機能を使用します。メンバイニシャライザは、コンストラクタでのみ使える機能で、次のような構文になっています。

// コンストラクタの定義
X::X() : メンバ変数名(初期化子),
{
}

厳密には、メンバ変数1つ1つの初期化を行っている部分がメンバイニシャライザで、全体としてはメンバイニシャライザリストと呼びます。

コンストラクタの定義の側で記述します。宣言の方には書けません。

コンストラクタの仮引数の並びの後ろに「:」を置き、メンバ変数名と、それに与える初期化子を指定します。メンバ変数が複数あるのなら、「,」で区切って、指定を繰り返します。

コンストラクタの本体のコードが実行される前に、メンバ変数が作られるタイミングで、メンバイニシャライザが機能します。メンバイニシャライザで記述した初期化子は、メンバ変数のコンストラクタの実引数として渡されます。

メンバ変数のコンストラクタが、引数無しで呼び出せるのであれば、メンバイニシャライザに記述しなくても結果は変わりませんが、空の ( ) を指定して、明示的に初期化できます。明示しておいた方が分かりやすいかもしれません。

なお、メンバ変数が const の場合、メンバイニシャライザで初期化しなければなりません。コンストラクタ内で初期化しようとすると、それは代入ですから、const の性質によってコンパイルエラーになってしまいます。

前の項のサンプルプログラムを、メンバイニシャライザを使って書き換えると、次のようになります。

// Student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

class Student {
public:
    Student();

    void SetData(const char* name, int grade, int score);

    const char* GetName() const;
    int GetGrade() const;
    int GetScore() const;

private:
    char        mName[32];   // 名前
    const int   mGrade;      // 学年
    const int   mScore;      // 得点
};

#endif
// Student.cpp

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

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

void Student::SetData(const char* name, int grade, int score)
{
    std::strcpy(mName, name);
    mGrade = grade;
    mScore = score;
}

const char* Student::GetName() const
{
    return mName;
}

int Student::GetGrade() const
{
    return mGrade;
}

int Student::GetScore() const
{
    return mScore;
}
// main.cpp

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

namespace {
    void PrintStudentData(const Student* student)
    {
        std::cout << "name: " << student->GetName() << "\n"
                  << "grade: " << student->GetGrade() << "\n"
                  << "score: " << student->GetScore() << "\n"
                  << std::endl;
    }
}

int main()
{
    Student student;  // インスタンス化される際に、コンストラクタが呼び出される
    PrintStudentData(&student);

    student.SetData("Saitou Takashi", 2, 80);
    PrintStudentData(&student);
}

実行結果:

name: no name
grade: 0
score: 0

name: Saitou Takashi
grade: 2
score: 80

mGrade や mScore はクラス型ではありませんが、「mGrade(0)」や「mScore(0)」という記述で初期化できます。このような初期化の仕方は、普段の変数定義時でも可能です。後で取り上げます

メンバ変数がクラス型であれば、「mMember(0)」のような記述は、コンストラクタに実引数 0 を渡していることになります。

メンバイニシャライザでメンバ変数を記述する順番は、クラス定義の中でメンバ変数を宣言した順番どおりにするようにします。メンバ変数のコンストラクタが呼び出される順番は、クラス定義の中でメンバ変数を宣言した順番に合わせられるルールだからです。余計な混乱を招かないように、順番を合わせておきましょう。

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

コンストラクタを明示的に定義しなかった場合に限って、コンパイラが自動的にデフォルトコンストラクタを生成します

デフォルトコンストラクタとは、引数無しで呼び出すことができるコンストラクタのことです。デフォルトで生成されるからデフォルトコンストラクタなのではないことに注意してください。

前章までの例では、コンストラクタを定義していませんでしたが、それでもオブジェクトをインスタンス化できていたのは、コンパイラが自動的にデフォルトコンストラクタを生成しているからです。

class Student {
private:
    char        mName[32];   // 名前
    const int   mGrade;      // 学年
    const int   mScore;      // 得点
};

int main()
{
    Student student;  // デフォルトコンストラクタを呼び出している
}

コンパイラが生成したコンストラクタは、特に何も中身がない空実装のコンストラクタです。つまり、このサンプルプログラムでは、次のようなコンストラクタが自動生成されていると考えられます。

Student::Student()
{
}

メンバ変数を明示的に初期化することもしていませんから、クラス型でないメンバ変数は不定値のままです。クラス型であれば、そのデフォルトコンストラクタが呼び出されます。

デフォルトコンストラクタがないと、オブジェクトの配列が作れません。次のサンプルプログラムでは、コンストラクタを明示的に定義しているため、デフォルトコンストラクタが作られません。

#include <cstring>

class Student {
public:
    Student();

private:
    char        mName[32];   // 名前
    const int   mGrade;      // 学年
    const int   mScore;      // 得点
};

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

int main()
{
    Student students[10];  // コンパイルエラー
}

【上級】オブジェクトを、動的メモリ割り当ての手法を使って生成すれば、デフォルトコンストラクタがなくても、配列を使うことは可能です(第15章)。

【上級】コンストラクタのすべての仮引数にデフォルト実引数(第10章)があり、結果的に引数無しで呼び出せるのなら、それもデフォルトコンストラクタとみなせます。

引数付きのコンストラクタ 🔗

ここまでのサンプルプログラムでは、コンストラクタの存在意義は、メンバ変数が不定値にならないようにすることでした。結局、適切な値を入れる作業は SetDataメンバ関数に任されており、ある種の2度手間状態は変わっていません。

今度は、オブジェクトがインスタンス化される時点で、適切な値を与えられるようにしてみましょう。そのためには、コンストラクタが引数を持つ必要があります。

コンストラクタに引数を持たせることについては、別段特別に考えるほどのことはありません。コンストラクタ側の宣言や定義は、これといって特別なことはありません。違いはせいぜい、インスタンス化するときに実引数を渡すようになる点ぐらいです。

// 引数付きのコンストラクタを使って、オブジェクトをインスタンス化する
クラス名 変数名(実引数の並び);

実際に試してみましょう。

// student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

class Student {
public:
    Student(const char* name, int grade, int score);

    void Print();

private:
    std::string  mName;   // 名前
    int          mGrade;  // 学年
    int          mScore;  // 得点
};

#endif
// student.cpp

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

Student::Student(const char* name, int grade, int score) :
    mGrade(grade), mScore(score)
{
    std::strcpy(mName, name);
}

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

#include "student.h"

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

実行結果:

no_name 0 0
Saitou Takashi 2 80

この形の方が無駄がありません。mName は残念ですが、メンバ変数を最初から適切な値で初期化できます。

【上級】文字列が必要なときには、std::basic_string(第18章)を使うことを検討するべきです。メンバイニシャライザでの初期化も可能です。

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

コンストラクタは、与える引数のパターンを変えることで、複数定義できます。同名の関数を複数個定義する機能をオーバーロードと呼びます。

【上級】コンストラクタ以外をオーバーロードする例を、第10章や第28章で取り上げます。

Studentクラスに、志望校を表すメンバ変数 mApplicantSchool を追加することを考えてみます。すでに志望校を決めている生徒もいれば、まだ決めていない生徒もいるでしょう。また、後から決まったり、後から変わったりする可能性もあります。

志望校を文字列でも表現できますが、現実味を出す意味で、ID のようなもので表現することにしましょう。School.h/cpp を作って、学校の情報を管理します。

// School.h

#ifndef SCHOOL_H_INCLUDED
#define SCHOOL_H_INCLUDED

namespace school {

enum ID {
    ID_NONE,
    ID_A,
    ID_B,
    ID_C
};

const char* GetName(ID id);

}

#endif
// School.cpp

#include "School.h"
#include <cassert>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

namespace school {

const char* const SCHOOL_NAMES[] = {
    "none",
    "A School",
    "B School",
    "C School"
};


const char* GetName(ID id)
{
    assert(0 <= id && id < SIZE_OF_ARRAY(SCHOOL_NAMES));

    return SCHOOL_NAMES[id];
}

}

学校を区別する ID を enum で表現し、実際の学校名を取得するための関数を用意しました。両者はセットであるべきものなので、同じ名前空間に入れています。

school::ID を使って、Studentクラスに志望校を表すメンバ変数を追加します。そして、コンストラクタによる初期化、メンバ関数による代入が行えるように対応します。さらに、ゲッターとなるメンバ関数も追加しておきます。

// Student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

#include "School.h"

class Student {
public:
    Student(const char* name, int grade, int score);
    Student(const char* name, int grade, int score, school::ID applicant);

    void SetApplicantSchool(school::ID school);

    const char* GetName() const;
    int GetGrade() const;
    int GetScore() const;
    school::ID GetApplicantSchool() const;

private:
    char mName[32];               // 名前
    int mGrade;                   // 学年
    int mScore;                   // 得点
    school::ID mApplicantSchool;  // 志望校
};

#endif
// Student.cpp

#include "Student.h"
#include <cstring>

Student::Student(const char* name, int grade, int score) :
    mGrade(grade), mScore(score), mApplicantSchool(school::ID_NONE)
{
    std::strcpy(mName, name);
}

Student::Student(const char* name, int grade, int score, school::ID applicant) :
    mGrade(grade), mScore(score), mApplicantSchool(applicant)
{
    std::strcpy(mName, name);
}

void Student::SetApplicantSchool(school::ID school)
{
    mApplicantSchool = school;
}

const char* Student::GetName() const
{
    return mName;
}

int Student::GetGrade() const
{
    return mGrade;
}

int Student::GetScore() const
{
    return mScore;
}

school::ID Student::GetApplicantSchool() const
{
    return mApplicantSchool;
}

コンストラクタを2つ定義しています。一方には school::ID型の引数があり、他方にはありません。最初から志望校が決まっている生徒は、インスタンス化する時点で school::ID を渡してやり、そうでない生徒には渡さないようにします。

実際に、Studentクラスを使う例は次のようになります。

// main.cpp

#include <iostream>
#include "School.h"
#include "Student.h"

namespace {
    void PrintStudentData(const Student* student)
    {
        std::cout << "name: " << student->GetName() << "\n"
                  << "grade: " << student->GetGrade() << "\n"
                  << "score: " << student->GetScore() << "\n"
                  << "applicant: " << school::GetName(student->GetApplicantSchool()) << "\n"
                  << std::endl;
    }
}

int main()
{
    Student student1("Saitou Takashi", 2, 80, school::ID_A);
    PrintStudentData(&student1);

    Student student2("Yamamoto Yuko", 1, 77);
    PrintStudentData(&student2);

    // 志望校が決まった
    student2.SetApplicantSchool(school::ID_B);
    PrintStudentData(&student2);
}

実行結果:

name: Saitou Takashi
grade: 2
score: 80
applicant: A School

name: Yamamoto Yuko
grade: 1
score: 77
applicant: none

name: Yamamoto Yuko
grade: 1
score: 77
applicant: B School

コンストラクタに渡す実引数の指定の仕方によって、どのコンストラクタが呼び出されるかが決まります。実引数の違いによって、どのコンストラクタを呼ぶべきかを判断できない場合は、コンパイルエラーになります。

移譲コンストラクタ 🔗

先ほどのサンプルプログラムで少し気になるところは、2つのコンストラクタの内容です。実質的に、mApplicantSchool の初期値が異なるだけで、あとの実装は同じですから、処理をまとめたいところです。

あるコンストラクタから、同じクラスのコンストラクタへ処理を移譲する(お願いする)ことができます。この機能は、移譲コンストラクタと呼ばれています。移譲コンストラクタは、メンバイニシャライザと同じ構文を使って、次のように記述します。

X::X(仮引数の並び) : X(移譲先のコンストラクタに渡す実引数の並び),
{
}

具体的には次のように使用します。

// School.h

#ifndef SCHOOL_H_INCLUDED
#define SCHOOL_H_INCLUDED

namespace school {

enum ID {
    ID_NONE,
    ID_A,
    ID_B,
    ID_C
};

const char* GetName(ID id);

}

#endif
// School.cpp

#include "School.h"
#include <cassert>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

namespace school {

const char* const SCHOOL_NAMES[] = {
    "none",
    "A School",
    "B School",
    "C School"
};


const char* GetName(ID id)
{
    assert(0 <= id && id < SIZE_OF_ARRAY(SCHOOL_NAMES));

    return SCHOOL_NAMES[id];
}

}
// Student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

#include "School.h"

class Student {
public:
    Student(const char* name, int grade, int score);
    Student(const char* name, int grade, int score, school::ID applicant);

    void SetApplicantSchool(school::ID school);

    const char* GetName() const;
    int GetGrade() const;
    int GetScore() const;
    school::ID GetApplicantSchool() const;

private:
    char mName[32];               // 名前
    int mGrade;                   // 学年
    int mScore;                   // 得点
    school::ID mApplicantSchool;  // 志望校
};

#endif
// Student.cpp

#include "Student.h"
#include <cstring>

Student::Student(const char* name, int grade, int score) :
    Student(name, grade, score, school::ID_NONE)  // 移譲コンストラクタ
{
}

Student::Student(const char* name, int grade, int score, school::ID applicant) :
    mGrade(grade), mScore(score), mApplicantSchool(applicant)
{
    std::strcpy(mName, name);
}

void Student::SetApplicantSchool(school::ID school)
{
    mApplicantSchool = school;
}

const char* Student::GetName() const
{
    return mName;
}

int Student::GetGrade() const
{
    return mGrade;
}

int Student::GetScore() const
{
    return mScore;
}

school::ID Student::GetApplicantSchool() const
{
    return mApplicantSchool;
}
// main.cpp

#include <iostream>
#include "School.h"
#include "Student.h"

namespace {
    void PrintStudentData(const Student* student)
    {
        std::cout << "name: " << student->GetName() << "\n"
                  << "grade: " << student->GetGrade() << "\n"
                  << "score: " << student->GetScore() << "\n"
                  << "applicant: " << school::GetName(student->GetApplicantSchool()) << "\n"
                  << std::endl;
    }
}

int main()
{
    Student student1("Saitou Takashi", 2, 80, school::ID_A);
    PrintStudentData(&student1);

    Student student2("Yamamoto Yuko", 1, 77);
    PrintStudentData(&student2);

    // 志望校が決まった
    student2.SetApplicantSchool(school::ID_B);
    PrintStudentData(&student2);
}

実行結果:

name: Saitou Takashi
grade: 2
score: 80
applicant: A School

name: Yamamoto Yuko
grade: 1
score: 77
applicant: none

name: Yamamoto Yuko
grade: 1
score: 77
applicant: B School

メンバイニシャライザでメンバ変数の初期値を与えるのと同じ構文で、他のコンストラクタと、それに渡す引数を記述します。コンストラクタの名前はつねにクラス名と同じなので、自分のクラス名を記述すれば良いです。

こうすると、同じコードの重複が少なくなり、分かりやすく保守しやすいプログラムになります。

非staticなメンバ変数の初期化 🔗

メンバ変数の宣言時に初期値を与えることも可能です。

ただし、staticメンバ変数(第23章)を除きます。

class X {
    型名 メンバ変数名 = 初期化子;
};

たとえば、次のように記述できます。

class Student {
public:
    Student(const char* name, int grade, int score) :
        mGrade(grade), mScore(score)
    {
        std::strcpy(mName, name);
    }

private:
    char    mName[32] = "no name";
    int     mGrade = 0;
    int     mScore = 0;
};

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

初期化の構文 🔗

メンバイニシャライザで「変数名(初期値)」のように、( ) を使って初期化を行いました。この記法はメンバイニシャライザに限らず、変数を宣言して初期化を行うときにも使えます。

変数を宣言して、初期値を与える構文には、以下のものがあります。

int num = 0;
int num = {0};
int num(0);
int num{0};

{} を使った記法に関しては、他にもいろいろと関連する事項があるため、第16章および第20章であらためて取り上げます。

{ } を使う方法は、配列などを初期化するときに複数の初期化子を指定するために用いますが、単独の変数に対してでも使用できます。

3つ目の方法では、型がクラス型の場合は複数の初期化子を、クラス型でない場合は単一の初期化子を指定できます。クラス型の場合は、コンストラクタに対する実引数の指定です。

1つ目と2つ目はコピー初期化、3つ目と4つ目は直接初期化と呼ばれています。

コピー初期化には「=」が使われていて、直接初期化には使われていないというふうに見えますが、この見分け方は正しくありません。たとえば、関数呼び出しの際に実引数を渡して、仮引数を初期化する行為や、戻り値を返すことはコピー初期化です。

【上級】コピー初期化には他に、例外(第19章)を送出することや、例外を補足することが含まれています。

【上級】「コピー」初期化という名前ですが、実際にはムーブ(第14章)になることもあります。

メンバイニシャライザでの初期化は直接初期化です。

【上級】直接初期化の方には他に、関数形式キャスト(第9章)や static_cast(第9章)、new(第15章)による初期化が含まれています。const_cast や reinterpret_cast がないことを疑問に思うかもしれませんが、これらのキャストは新しいオブジェクトを生み出さないので、初期化という概念が登場しないからです。

コピー初期化と直接初期化の違いは、初期化に先立って、ユーザー定義の型変換の処理(第9章)を適用するかどうかです。コピー初期化では適用しますが、直接初期化では適用しません。

この型変換処理が挟まることによって、実行効率を落としている可能性があります。そのため、変数宣言時の初期化には、コピー初期化よりも直接初期化を選ぶ方が良いかもしれません

ところで、以下のように、初期化子を与えずに変数を宣言することがあります。

int num;
int num();     // コンパイルエラー
int num = {};
int num{};

1つ目の方は、この変数が static でないローカル変数であれば、未初期化なように見えます。int型のようにクラス型でない型ならばそのとおりですが、クラス型の場合は、デフォルトコンストラクタが呼び出されます。この初期化方法は、デフォルト初期化と呼ばれています。

2つ目の方法は、コンパイルエラーになります。これは、C++ の文法解析の厄介な問題として有名なのですが、この変数宣言全体の形が、関数を宣言しているように見えてしまうため、エラーになります。つまり、「int」が戻り値型、「num」が関数名、「()」が仮引数のリストに見えるということです。ただし、メンバイニシャライザのように、文法的に曖昧にならない場面であれば、空の () で初期化することは可能です。この場合、値初期化(後述)されます。

3つ目、4つ目の場合は、文法上曖昧になることはありません。空の () の場合と同様に、空の {} の場合も、値初期化(後述します)が行われます

初期化のルール 🔗

前の項で、値初期化という用語が登場しました。この項では、こういった初期化のルールについて解説します。ただし、初期化のルールは非常に複雑です。ほとんどの人は、ここに書かれていることのすべてを理解する必要はないと思われます。

値初期化 🔗

値初期化は、以下のように初期化を行います。初期化する対象の型のことを T と表現します。

  1. T が、明示的に定義したコンストラクタを持っている場合、デフォルトコンストラクタを呼ぶ。ただし、デフォルトコンストラクタにアクセスできなければコンパイルエラーになる
  2. T が、明示的に定義したコンストラクタを持っていないクラス型の場合、コンパイラが自動生成したデフォルトコンストラクタが非トリビアルであればそれを呼ぶ。そうでなければ、ゼロ初期化を行う。
  3. T が、配列型の場合は、それぞれの要素に対して、値初期化を行う
  4. その他の場合は、ゼロ初期化を行う

1番目のところに注意が必要です。明示的に定義したコンストラクタを持っている場合に、そのコンストラクタを呼ぶとは言っていません。たとえば、次のプログラムはコンパイルエラーになります。

#include <iostream>

class MyClass {
public:
    // 明示的なコンストラクタを定義
    MyClass(int x) {}
};

int main()
{
    // 値初期化を行う。
    // MyClass は明示的なコンストラクタを定義しているので、
    // デフォルトコンストラクタを呼ぼうとするが、
    // 該当するコンストラクタがないため、コンパイルエラーになる。
    MyClass mc = {};
}

2番目のところに出てくる非トリビアルとは、トリビアルではないということです。トリビアル (trivial) とは「自明」や「取るに足らない」といった意味の単語です。ここでは「デフォルトコンストラクタが非トリビアルの場合」と言っていますが、トリビアルという言葉を使う場面は他にもいくつかあります(トリビアルなクラス、トリビアルなコピー操作など)。

トリビアルなデフォルトコンストラクタについては、この後取り上げます

2番目と4番目のところで、ゼロ初期化という言葉も登場しますが、これも初期化のルールの1つです。これはこの後取り上げます

ゼロ初期化 🔗

ゼロ初期化は、以下のように初期化を行います。初期化する対象の型のことを T と表現します。

  1. T がスカラ型の場合、0 を T型に変換して与える
  2. T が 共用体以外のクラス型の場合、非staticなメンバ変数と、基底クラス(第35章)の非staticなメンバ変数はそれぞれゼロ初期化される。なお、パディングも 0 のビットで初期化される
  3. T が 共用体の場合、最初の非static かつ名前が付いているメンバが、ゼロ初期化される。なお、パディングも 0 のビットで初期化される
  4. T が配列型の場合は、それぞれの要素に対して、ゼロ初期化を行う
  5. T が参照型(第12章)の場合は、初期化を行わない

スカラ型は、整数型、浮動小数点数型、enum型、ポインタ型(std::nullptr_t型を含む)のことです。const や volatile のような修飾子が付いていても構いません。

非staticなメンバ変数というのは、これまでの章で取り上げてきたような普通のメンバ変数のことです。第19章で登場しますが、staticメンバ変数というものがあり、それ以外のことをいいます。

トリビアルなデフォルトコンストラクタ 🔗

デフォルトコンストラクタがトリビアルであるとは、次の条件をすべて満たしている場合のことをいいます。

  1. デフォルトコンストラクタが明示的に定義されておらず、削除(第10章)もされていない
  2. クラスが仮想関数や、仮想基本クラスを持たない
  3. 非staticなメンバ変数の初期化子(本章)を持たない
  4. クラスの直接の基底クラスが、トリビアルなデフォルトコンストラクタを持つ
  5. 非staticなメンバ変数が、トリビアルなデフォルトコンストラクタを持つ

大ざっぱな言い方をすると、何もしないコンストラクタということですが、空実装であっても、明示的に定義した場合はトリビアルにならないなど、多少注意すべき点があります。


デストラクタ 🔗

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

デストラクタには、「~クラス名」という名前を付けます。コンストラクタと同様に、戻り値はないので、戻り値型の指定もしません。また、解体時に自動的に呼び出されるという性質上、情報を渡すこともできないので、引数もありません。

class X {
    ~X();  // デストラクタの宣言
};

// デストラクタの定義
X::~X()
{
}

クラスと構造体は同一の存在なので(第6章)、struct でもデストラクタは使えます。

クラス定義内でデストラクタの定義を記述することも可能です。これは、inlineキーワードを指定したことと同じ意味を持ちます(第6章)。

デストラクタを constメンバ関数にできませんが、const付きのオブジェクトを定義する場合でもデストラクタは呼び出されるので、特に必要性もありません。

構文の例なので省いていますが、デストラクタもアクセス指定子の影響を受けます。publicキーワードを使って「公開」しておかないと、クラスの外側でオブジェクトが解体できなくなります。

オブジェクトが解体できないような使い方をすると、コンパイルエラーになります。

【上級】デストラクタを「非公開」にしても、メンバ関数やフレンド(第29章)からは解体できるため、使い方次第ではコンパイルエラーは起きません。

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

// Student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

class Student {
public:
    Student(const char* name, int grade, int score);
    ~Student();

    const char* GetName() const;
    int GetGrade() const;
    int GetScore() const;

private:
    char mName[32];           // 名前
    const int mGrade;         // 学年
    const int mScore;         // 得点
};

#endif
// Student.cpp

#include "Student.h"
#include <cstring>
#include <iostream>

Student::Student(const char* name, int grade, int score) :
    mGrade(grade), mScore(score)
{
    std::strcpy(mName, name);
}

Student::~Student()
{
    std::cout << "call destructor: " << mName << std::endl;
}

const char* Student::GetName() const
{
    return mName;
}

int Student::GetGrade() const
{
    return mGrade;
}

int Student::GetScore() const
{
    return mScore;
}
// main.cpp

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

namespace {
    void PrintStudentData(const Student* student)
    {
        std::cout << "name: " << student->GetName() << "\n"
                  << "grade: " << student->GetGrade() << "\n"
                  << "score: " << student->GetScore() << "\n"
                  << std::endl;
    }
}

int main()
{
    Student student1("Saitou Takashi", 2, 80, school::ID_A);
    PrintStudentData(&student1);

    Student student2("Yamamoto Yuko", 1, 77);
    PrintStudentData(&student2);
}

実行結果:

name: Saitou Takashi
grade: 2
score: 80

name: Yamamoto Yuko
grade: 1
score: 77

call destructor: Yamamoto Yuko
call destructor: Saitou Takashi

デストラクタでは主に、動的メモリ割り当てを行ったメンバ変数を解放したり、使用中のファイルを close したりといった、確実に行っておく必要がある後片付けを記述します。std::string が内部で動的に確保した文字列の領域を解放する仕組みもデストラクタです。

なお、プログラマーがデストラクタを明示的に定義しなければ、コンパイラが自動生成します。自動生成されたデストラクタの実装は空です。

【上級】あまり意味が無さそうに思えますが、デストラクタが呼び出せないと、オブジェクトを破棄できません。たとえば、デストラクタがあっても、それが「非公開」であることが理由で呼び出させなければ、そのオブジェクトは破棄できず、コンパイルエラーになります。自動生成されるデストラクタは「公開」されています。

デストラクタの本体の処理が実行された後、メンバ変数それぞれのデストラクタも呼び出されます。メンバ変数のデストラクタを呼び出すときの順番は、コンストラクタが呼ばれた順番の逆です。コンストラクタは、メンバ変数の宣言順に呼び出されるので(前述)、デストラクタはその逆順で呼ばれます。

#include <iostream>
#include <string>

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

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

private:
    std::string  mStr;
};

class Test {
public:
    Test() : mA("A"), mB("B"), mC("C")
    {
        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)

想定どおりの順番で呼び出されているようです。するべきではありませんが、もしもメンバイニシャライザの記述順序を変えたとしても、この結果は変わりません。

【上級】デストラクタには戻り値がないため、デストラクタ内で発生したエラーを呼び出し元に伝えることができません。そもそも、デストラクタのような終了処理系の関数は、つねに成功するように作るべきです。終了処理が成功しないということは、もはや取り返しが付かない状況に陥っていることになるため、プログラムはただちに異常終了された方が良いでしょう。

自動生成されるコンストラクタとデストラクタの明示 🔗

コンストラクタやデストラクタは、プログラマーが明示的に実装しなければ、コンパイラが自動生成します。

明示的にコンストラクタを実装しつつも、コンパイラが自動生成するコンストラクタも必要であれば、デフォルトで生成されるコンストラクタの存在を明示すればよいです。そのためには、「= default」を使って、以下の記述を行います。

class X {
    X() = default;
};

コンパイラが自動生成する関数と同じように、名前と仮引数の並び、戻り値の指定をしなければなりません。コンパイラが自動生成するコンストラクタはデフォルトコンストラクタなので、仮引数はありません。

また、実装は自動生成されたものを使うのですから、定義は不要です。

デストラクタでも同様の構文が使えます。

class X {
    ~X() = default;
};

【上級】コンパイラが自動生成するメンバ関数には他に、コピーコンストラクタとコピー代入演算子(第13章)、ムーブコンストラクタとムーブ代入演算子(第14章)があります。これらのいずれでも、「= default」を使えます。

具体的には、次のように記述します。

class Student {
public:
    Student() = default;
    Student(const char* name, int grade, int score);
};


練習問題 🔗

問題① 次のようにコンストラクタを定義することには問題があります。指摘してください。

class Student {
public:
    Student(const char* name, int grade, int score);
    Student(const char* name, int score, int grade);
};

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

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

int value = 10;

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

    value = 50;

    if (flag) {
        return;
    }

    value = 100;
}


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 C++編【言語解説】第13章「コンストラクタとデストラクタ」の修正に合わせて、内容更新。

 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

 「初期化のルール」の項を追加。
初期化の構文」の項の内容を一部移動したうえで、大幅に加筆した。

 「初期化の構文」の項を追加。
メンバイニシャライザ」の項にあった話題の一部を切り出してきて、大幅に加筆した。

 新規作成。



前の章へ (第6章 アクセス指定子)

次の章へ (第8章 クラステンプレート)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る