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

トップページC++編

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クラスが持つ mName の型を std::string でなく、char*型で管理したいとします。どのように実装しますか?


単純に、std::string型を char*型に変更することは可能です。ここでは、const char*型を使います。

// student.h

#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

class Student {
    const char*  mName;   // 名前
    int          mGrade;  // 学年
    int          mScore;  // 得点

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

    void SetData(const char* 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(const char* name, int grade, int score) :
    mName(name), mGrade(grade), mScore(score)
{
}

void Student::SetData(const char* 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

const修飾子を付けたのは、Studentクラス内から、このポインタを通じての書き換えを行うことがないからです。名前をポインタで渡したということは、名前は Studentクラスの外側でも使用している可能性があるので、Studentクラス内で書き換えないことをはっきりさせるべきです。

逆に、const修飾子を付けなければ、Studentクラスの利用者へ、渡された名前は Studentクラス内で書き換えることがあるのだと宣言していることになります。

さて、このように渡されたメモリアドレスをそのまま保持するという実装も1つも方法ですが、この実装では、次のように使われるとエラーになってしまいます。

// main.cpp

void func(Student* student);

int main()
{
    Student student;

    func(&student);
    student.Print();
}

void func(Student* student)
{
    char name[] = "Saitou Takashi";

    student->SetData(name, 2, 80);
}

ローカル変数 name のメモリアドレスを渡すような使い方をしていますが、Studentオブジェクトとは生存期間が異なっているため、Studentオブジェクトより先に、name がメモリから消えてしまいます。そのため、Printメンバ関数は正しい名前を出力できません。

このような使い方をされることが予想されるのなら、Studentクラスの設計を見直さなければいけません。もちろん、std::string型を使うようにすれば、このような問題は起こりませんが、メモリと処理時間をそれぞれ少し多めに使うことになります。

設計に正解はありません。使われ方をよく考えて、適切な実装方法を見つけるしかありません。

問題② 🔗

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


std::string型のメンバ変数^を持ったクラスを2つ用意して、片方はメンバイニシャライザを使い、他方はコンストラクタ内で代入するようにします。

#include <string>
#include "ppp_perform.h"

class Test1 {
    std::string  mStr1;
    std::string  mStr2;
    std::string  mStr3;

public:
    Test1();
};

Test1::Test1()
{
    mStr1 = "Test String1";
    mStr2 = "Test String2";
    mStr3 = "Test String3";
}


class Test2 {
    std::string  mStr1;
    std::string  mStr2;
    std::string  mStr3;

public:
    Test2();
};

Test2::Test2() :
    mStr1("Test String1"),
    mStr2("Test String2"),
    mStr3("Test String3")
{
}


int main()
{
    PPP_CHECK_PERFORM_BEGIN(10000000);
    Test1 t1;
    PPP_CHECK_PERFORM_END("don't use member_initializer");

    PPP_CHECK_PERFORM_BEGIN(10000000);
    Test2 t2;
    PPP_CHECK_PERFORM_END("use member_initializer");
}

実行結果:

don't use member_initializer: 0.554000 seconds
use member_initializer: 0.454000 seconds

わずかですが、メンバイニシャライザを使う方が高速になることが分かります。メンバ変数の個数がもっと多かったり、もっと複雑な型であったり、インスタンス化するオブジェクトの数が多かったりすると、さらにこの差が大きくなっていきますから、可能である限り、つねにメンバイニシャライザを使うようにしてください。

問題③ 🔗

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

int value = 10;

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

    value = 50;

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

    value = 100;
}

このクラスは、コンストラクタで int型の変数を指定すれば、その時点での変数の値を保存しておき、デストラクタでその値へ確実に復元してくれます。ここでは、ValueStore という名前のクラスにしてみます。

// ValueStore.h

#ifndef VALUE_STORE_H_INCLUDED
#define VALUE_STORE_H_INCLUDED


// int型の変数の値を退避し、破棄時に復元する
class ValueStore {
public:
    ValueStore(int* ptr);
    ~ValueStore();

private:
    int*    mPtr;        // 対象の変数のメモリアドレス
    int     mSaveValue;  // 元の値
};

#endif
// ValueStore.cpp

#include "ValueStore.h"

ValueStore::ValueStore(int* ptr) :
    mPtr(ptr), mSaveValue(*ptr)
{
}

ValueStore::~ValueStore()
{
    *mPtr = mSaveValue;
}
// main.cpp

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

int value = 10;

void func(bool flag);

int main()
{
    func(true);
    std::cout << value << std::endl;

    func(false);
    std::cout << value << std::endl;
}

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

    value = 50;

    if (flag) {
        return;
    }

    value = 100;
}

実行結果:

10
10

デストラクタを利用することによって、func関数内をどのような形で抜け出すとしても気にすることなく、確実に復元処理が行われます。この例でいえば、途中で return する経路もありますが、ここを通っても通らなくても問題ありません。

このように、デストラクタは必ずしも、メモリの解放やファイルクローズといった、後片付けのような処理にしか使えないわけではありません。「確実に行ってほしいこと」を書くことができる、非常に役に立つ機能です。

なお、2つのメンバ変数は、コンストラクタで初期化された後、書き換えることがないので、const修飾子を付けられます。

// ValueStore.h

#ifndef VALUE_STORE_H_INCLUDED
#define VALUE_STORE_H_INCLUDED


// int型の変数の値を退避し、破棄時に復元する
class ValueStore {
public:
    ValueStore(int* ptr);
    ~ValueStore();

private:
    int* const    mPtr;        // 対象の変数のメモリアドレス
    const int     mSaveValue;  // 元の値
};

#endif

また、引数が1つだけのコンストラクタには、explicit指定子(第19章)を付けるのが良いです。


参考リンク 🔗


更新履歴 🔗

 全体的に見直し修正。

 問題③のサンプルプログラムで、const を付ける位置が間違っていたのを修正。
リンク先の間違いを修正。

 新規作成。



第13章のメインページへ

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

Programming Place Plus のトップページへ



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