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

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

先頭へ戻る

この章と同じ(または似た)情報を扱うページが、Modern C++編 (C++11/14/17 対応) の以下の章にあります。

この章の概要

この章の概要です。


コンストラクタ

オブジェクトをインスタンス化したとき、メンバ変数の値は未初期化な状態です。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

(→Modern C++編

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

C++11

(→Modern C++編


デストラクタ

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

#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

(→Modern C++編

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

C++11

(→Modern C++編


練習問題

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

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

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

int value = 10;

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

    value = 50;

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

    value = 100;
}


解答ページはこちら

参考リンク



更新履歴

'2018/7/29 「C++11 (委譲コンストラクタ)」「C++11 (非staticなメンバ変数の定義箇所での初期化)」「C++11 (自動生成されるメンバ関数の明示)」「C++11 (関数定義の削除)」の項の内容を削除。同じ内容を解説している Modern C++編のページへのリンクだけを残した。

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

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

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

≪更に古い更新履歴を展開する≫



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

次の章へ(第14章 動的なオブジェクトの生成)

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS