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

先頭へ戻る

この章の概要

この章の概要です。

コンストラクタ

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

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

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

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

// Student.h

#ifndef STUDENT_H
#define STUDENT_H

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

    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;
}

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);
}

実行結果:

name: no name
grade: 0
score: 0

コンストラクタには、クラスと同じ名前を付けます。 また、オブジェクトを作って返す際に使われるものなので、戻り値を返すことはできず、 戻り値の型の指定は省略します(void ではいけません)。

コンストラクタを追加して、メンバ変数に最低限の初期値を与えるようにしたので、 少なくとも、メンバ変数が未定義なままで、オブジェクトを操作してしまうことは無くなりました。 ただ、このサンプルの作りでは、具体的な生徒の名前や得点で初期化することはできていません。
SetData() を復活させれば、情報を設定することはできますが、それよりも、コンストラクタに引数を追加して、 初期状態として、情報を与えた方がより良い作りになります。

// Student.h

#ifndef STUDENT_H
#define STUDENT_H

class Student {
public:
    Student(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(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 student1("Saitou Takashi", 2, 80);
    PrintStudentData(&student1);

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

実行結果:

name: Saitou Takashi
grade: 2
score: 80

name: Yamamoto Yuko
grade: 1
score: 77

これで、メンバ変数は最初から正しい初期値を持つことができました。 インスタンス化した後で、メンバ変数の値を変更する必要性があるのなら、 SetData() のようなメンバ関数が必要ですが、そうでないのなら、余分なメンバ関数を「公開」しない原則を貫くべきです。

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

コンストラクタを導入した Studentクラスに、更なる改良を加えてみます。

インスタンス化した後で、メンバ変数の値を書き換える必要が無いのだと仮定すると、 メンバ変数は書き換え不可、つまり const にしても構わないと思われます。 そこで、メンバ変数の宣言を以下のように変更します。

class Student {

    // 他のメンバは省略

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

配列については、各要素を初期化しなければならない事情があるので、mName は非const のままにしていますが、 これでもコンパイルエラーになります。 なぜなら、コンストラクタの中で、mGrade や mScore に「代入」しているからです。 つまり、各メンバ変数は一旦、未初期化状態で作られて、その後、コンストラクタ内部で代入されています。 メンバ変数に const を付加するには、本当の意味で「初期化」になるようにしなければなりません。

そこで、メンバイニシャライザ(初期化リスト)という初期化構文を使います。

// Student.h

#ifndef STUDENT_H
#define STUDENT_H

class Student {
public:
    Student(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(const char* name, int grade, int score) :
    mGrade(grade), mScore(score)
{
    std::strcpy(mName, name);
}

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 student1("Saitou Takashi", 2, 80);
    PrintStudentData(&student1);

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

実行結果:

name: Saitou Takashi
grade: 2
score: 80

name: Yamamoto Yuko
grade: 1
score: 77

メンバイニシャライザは、コンストラクタの定義のところに、 「コンストラクタの名前 : メンバ変数名(初期値) …」のような構文で記述します。 コンストラクタの定義を、クラス定義の中に書いてしまう場合は、次のようになります。

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

メンバイニシャライザに、メンバ変数を記述する順番ですが、 クラス定義の中でメンバ変数を書いた順番通りにするのが基本です。
メンバ変数それぞれについて、コンストラクタが呼び出される訳ですが、 その順番は、クラス定義の中でメンバ変数を書いた順番と決められています。 そのため、メンバイニシャライザの方も、その順番通りに並べておいた方が無難です。

このように、コンストラクタの中で初期値を与えると、「初期化」→「代入」という流れになりますが、メンバイニシャライザを使うことで「初期化」だけで済ませられます。constメンバ変数の場合はこの仕組みが必要不可欠ですし、非constメンバ変数の場合にも、効率の向上という恩恵が受けられるので、コンストラクタ内で値を代入する方法は、可能な限り避けるようにして下さい。

唯一 const にできなかった mName についてですが、型を std::string(第17章)に置き換えれば const にすることができます。

初期化の構文

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

変数の初期値を与える構文は、以下のように幾つか存在しています。

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

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

1つ目と3つ目のような初期化方法は、コピー初期化と呼ばれています。他の方法との見た目の上での違いは「=」があることなのですが、関数の引数や戻り値を使って初期化を行うときも、コピー初期化として扱われるため、常に「=」が見えているとも限りません。

class MyClass {
public:
    MyClass(int x) {}
};

void f1(MyClass mc) {}
MyClass f2() { return 100; }  // 100 は戻り値の MyClass をコピー初期化する値

int main()
{
    f1(100);             // 100 は仮引数 mc をコピー初期化する値
    MyClass mc = f2();   // mc は戻り値を使ってコピー初期化される
}

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

一方、2つ目と4つ目の初期化方法のように、「=」を伴わずに () や {} を使って行われる初期化は、直接初期化と呼ばれています。なお、変数名と () や {} はくっつけて書いても、離して書いても構いません。

コピー初期化と直接初期化はいずれにしても、変数がクラス型であれば、初期値を実引数にして、コンストラクタを呼び出します。クラス型でなければ、単に、その型の初期値として与えられます。

コピー初期化と直接初期化の違いは、プログラマが定義した型変換の処理を適用するかどうかです。この話題は、第9章で取り上げます。

変数宣言は、以下のように、初期値の指定が無い形で行われることも考えられます。

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

1つ目は、C言語のときからお馴染みの、いわゆる未初期化な状態のようですが、変数がクラス型の場合は、引数なしで呼び出すことができるコンストラクタが呼び出されます。このようなコンストラクタをデフォルトコンストラクタと言います。

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

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

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

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

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

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

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

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

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

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

例えば、Studentクラスに、順位を表すメンバ変数を追加します。順位は、すべての Student の得点が出揃わないと判定することができないので、コンストラクタの引数を使って初期化することができません。そこで一旦、0 で初期化しておくことにします。

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

private:
    char        mName[32];
    const int   mGrade;
    const int   mScore;
    int         mOrder = 0;  // 0 で初期化
};

手軽ではありますが、すべてのオブジェクトとも同じ値で初期化しなければならないため、使える場面は限られます。

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

複数のコンストラクタ

コンストラクタは、与える引数のパターンを変えることで、複数定義することができます。

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

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

// School.h

#ifndef SCHOOL_H
#define SCHOOL_H

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クラスに志望校を表すメンバ変数を追加します。 そして、コンストラクタによる初期化、メンバ関数による代入が行えるように対応します。 更に、getter となるメンバ関数も追加しておきます。

// Student.h

#ifndef STUDENT_H
#define STUDENT_H

#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];           // 名前
    const int mGrade;         // 学年
    const 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 の初期値が異なるだけで、あとの実装は同じですから、 処理をまとめたいところです。

あるコンストラクタから、同じクラスのコンストラクタへ処理を移譲する(お願いする)ことができます。 この機能は、移譲コンストラクタと呼ばれています。

移譲コンストラクタを使うと、次のように書くことができます。

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);
}

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

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


デストラクタ

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

次のプログラムで確認してみましょう。

// Student.h

#ifndef STUDENT_H
#define STUDENT_H

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

デストラクタは、「~Student()」のように、クラスの名前の頭に「~」を付けた名前で表します。 デストラクタは自動的に呼び出されるものなので、引数はありませんし、戻り値を返すこともできません。

デストラクタに記述した処理が実行された後、メンバ変数それぞれについてもデストラクタが呼び出されます。 このときの順序は、コンストラクタを呼び出した順序の反対と決められています。

先ほどのサンプルプログラムでは、デストラクタの動作を確認するために、標準出力にメッセージを出力していましたが、 もう少し実用的な例を挙げましょう。 典型的な使い方は、動的に確保したメモリを解放することや、開いていたファイルを閉じるといったこと、 すなわち、「最後に確実に行わなければならない後片付け」を記述することです。

例えば、Studentクラスの mName を、要素数が固定的な配列ではなく、動的に確保される配列にしたとしましょう。 その場合、コンストラクタでメモリ確保を行い、デストラクタで解放するようにすれば良いです。

// Student.h

#ifndef STUDENT_H
#define STUDENT_H

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

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

private:
    char* const mName;        // 名前
    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) :
    mName((char*)std::malloc(std::strlen(name) + 1)),
    mGrade(grade), mScore(score)
{
    std::strcpy(mName, name);
}

Student::~Student()
{
    std::free(mName);
}

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

動的なメモリ確保と解放が、コンストラクタとデストラクタで行われていますが、 クラスの使用者側からは、それがまったく見えないことに注目して下さい。 これは、 「いつ確保すればいいか」「何度も確保してしまわないか」「いつ解放すればいいか」 「確保されていないときに、メンバを使おうとしてしまわないか」といったことに注意を払う必要が無いということですから、 非常に安全で分かりやすいプログラムになります。

このサンプルプログラムでは、std::malloc() および std::free() を使用していますが、C++ で動的なメモリを使うには、これらの関数は避け、new、delete(ともに第15章)を使った方が良いです。文字列の場合であれば、std::string(第17章)を使うと、より簡単です。

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

コンストラクタやデストラクタは、プログラマが明示的に実装しなければ、コンパイラが自動生成します。 その場合のコンストラクタは、引数も中身も無いものが生成され、 デストラクタについても、何もしないものが生成されます。

もし、引数があるコンストラクタだけを明示的に実装したとすると、 デフォルトコンストラクタがありませんから、次のようなインスタンス化は行えなくなります。

Student student;  // 引数無しのコンストラクタが必要

コンパイラが自動生成したコンストラクタのことを、デフォルトで作られるコンストラクタというようにイメージして、それのことをデフォルトコンストラクタと呼ぶのだと勘違いしがちですが、そうではありません。コンパイラが自動生成するコンストラクタは、引数無しで呼び出せますから、デフォルトコンストラクタであることは事実ですが、誰が生成したものであろうと、引数無しで呼び出せるのなら、それはデフォルトコンストラクタです。

引数があるコンストラクタを明示的に実装しつつも、コンパイラが自動生成するコンストラクタも必要であれば、 次のように記述できます。

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

コンストラクタの宣言の末尾に「= default」と記述することによって、 コンパイラが自動生成するコンストラクタを使うことを明示できます。 この構文は、デストラクタでも使うことができます。

またしてもややこしいことに、ここでの「default」は、コンパイラがデフォルトで作るものを意味しており、 デフォルトコンストラクタのことではありません。

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


練習問題

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

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;
}


解答ページはこちら

参考リンク



更新履歴

'2018/7/13 サイト全体で表記を統一(「静的メンバ」-->「staticメンバ」)

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

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

'2017/7/17 新規作成。



前の章へ(第6章 カプセル化)

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

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS