2018-01-28

Cプログラマのための、
C++オブジェクト指向簡単裏口入門(10)

C++のクラスは魔法の箱?(2)

前回、オブジェクト指向プログラミングの醍醐味の1つは、自分が提供するクラスに高い機能をスマートで格好よく組み込めることであると書きました。
しかし、それは演算子をクラスに組み込むことだけではありません。

ギミックと言える要素は他にもあります。

初期化と後片付け

クラスの設計では、それが1つの独立したアプリケーションの様に動作させることも多々あります。当然、クラス内部の初期化や終了処理を行うことも多いわけです。

クラスのユーザ側にそれを意識させて、使い始めるときには必ず init() を、使い終わったら term() を呼んでくださいね、さもないとエラーやメモリリークが発生してしまいますよ、と仕様書に注意書きを記すのも1つの方法です。

しかし、初期化と終了処理はもはや普遍的な定型処理と言えるものですから、C++ではこれを自動化できるようになっています。

では、それを組み込んだクラスの例を見てください。

コード1

// 巨大データクラス
class LargeData
{
    size_t m_size;
    char * m_pData;
public:
    // ★コンストラクタ
    LargeData(size_t size)
    {
        m_size = size;
        m_pData = (char *)malloc(size); // エラー処理は省略
    }

    // ★デストラクタ
    ~LargeData()
    {
        free(m_pData);
    }

    // データの設定
    const char * setData(const unsigned char * pData, size_t size)
    {
        return (const char *)memcpy(m_pData, pData,
                                 m_size < size ? m_size : size);
    }
    // データの取得
    const char * getData()
    {
        return m_pData;
    }
}

// クラスを使う
int main()
{
    char myData = "abcdefg";
    LargeData ld(1000);    // 内部で1000バイト取得

    ld.setData(myData);

    return 0;
}
この LargeData クラスには目新しい関数が2種類追加されています。クラス名と同じ名前の関数と、それにチルダ(~)が頭に付いたものです。それぞれ、コンストラクタ、デストラクタと言われるメンバ関数で、クラスがインスタンス化されるとき、および、破棄されるときに自動で呼ばれます。

ここでは main() 関数でクラスを使用しています。自動変数として LargeData をインスタンス化する際にパラメータを渡しています。これがコンストラクタ関数のパラメータの引数になります。dl(1000) という書式は、関数を読んで居ることを強調するものですが、この場合引数が1つなので、dl = 1000 という書き方も可能です。(初期化の書式は他にもありますが、ここでは割愛します)
デストラクタの方はいつ呼ばれるのでしょうか。ld は自動変数ですから、main() 関数を抜ける際に破棄されます。破棄されるタイミングでデストラクタが呼ばれます。

このような作りにしておけば、特に後処理を忘れてメモリリークを起させてしまう心配がとても少なくなるわけです。便利ですね!

ところで、このコードではメモリの取得にCの標準関数でもある malloc() を使用しています。実は、C++では malloc() を使うことは滅多にありません。なぜなら、代りとなる言語組込みの演算子 new が用意されているからです。free() に相当する delete もあります。 早速書き換えてみましょう。

コード2

// 巨大データクラス
class LargeData
{
    size_t          m_size;
    unsigned char * m_pData;
public:
    // コンストラクタ
    LargeData(size_t size)
    {
        m_size = size;
        m_pData = new char [size];  // ★
    }

    // デストラクタ
    ~LargeData()
    {
        delete m_pData; // ★
    }

    // データの設定
    const unsigned char * setData(const unsigned char * pData)
    {
        return (const char *)memcpy(m_pData, pData, m_size);
    }
    // データの取得
    const unsigned char * getData()
    {
        return m_pData;
    }
}
new は確保したメモリのポインタを返します。また、delete には開放するメモリのポインタを渡します。
malloc() や free() の代わりに使うような説明をしましたが、互換性はありませんので、new で取得して free() で開放する、といった使い方はできません。
また、malloc() はメモリが確保できない場合 NULL を返しますが、通常 new はメモリが確保できない場合は例外を発生します。(処理系の設定によって、NULLを返すようにもできる場合があります)ので、直後にいちいちNULLチェックをする必要はありません。(最新のC++ではNULLではなく、nullptr キーワードを使用します)
必要なところで例外をキャッチしてエラー処理を行うようにします。

更に、new と delete は malloc() や free() 関数とは本質的に異なることがあります。
次のコードを見てください。

コード3

// クラスを使う
int main()
{
    char myData = "abcdefg";

    LargeData * pLD = new LargeData(2000);
    pLD->setData(myData);

    delete pLD;

    return 0;
}
これは先ほどの LargeData クラスを使用するところです。今度は自動変数としてインスタンス化しているのではありません。new 演算子を使って LargeData クラスをインスタンス化しているのです。

malloc() は単にメモリを確保するだけでしたが、new は指定したクラスのメモリ領域を確保すると共に、インスタンス化まで行います(厳密には、インスタンス化という言い方は必要なメモリ領域の確保までを含めます)。もちろんコンストラクタも呼ばれます。
また、同様に delete 演算子はメモリを解放するだけではなく、後処理も行い、デストラクタも呼ばれます。

new や delete はC++では頻繁に使用されます。その際気をつけなければならないのは、delete を忘れてしまうことです。
Java や C# ではその必要が無く、言語のランタイム処理によって自動的に不要になったオブジェクトが判断され、開放される仕組みになっています。C++ ではプログラマの責任で開放しなければなりません。
しかし、最新の C++ では、そういった新しい言語の仕様の影響を受けたためか、ある程度の自動化が考慮されるようになっています。標準のライブラリを使うことで実現します。

アイディア

さて、コンストラクタやデストラクタを使って何をすればいいのでしょうか。単に、クラス内部で使用する領域の確保をしたり、開放したり、あるいはメンバ変数の初期化だけでしょうか。
もし、あなたがファイルをリードライトするクラスを設計するとしたら、どうしますか?

コンストラクタでファイルをオープンし、デストラクタでクローズする、といったアイディアが思いつきましたか?
しかし、コンストラクタでファイルをオープンしたら、戻り値が使えないのでエラーになったときどうすればいいのでしょうか。そんな心配をされたなら、むしろ良く理解されているということでもありますね。こういうときのためにも、C++では例外処理機構が用意されています。コンストラクタでのエラーは例外を発生させればいいのです。

コンストラクタで必ずファイルをオープンしなければならないとしたら、場合によっては都合が悪いこともあるでしょう。クラスをインスタンス化だけしておき、あとでオープンすることもできるように、fileOpen() メンバ関数も追加します。また、デストラクタが呼ばれる前にクローズしたい場合も考えて fileClose() も追加します。

なんだかとても便利そうなファイルIOクラスができそうな気がしてきませんか?

更に、テキストファイル用に特化されたクラスを作るなら、元のファイルクラスを継承させて設計すればきっと理に適っているはずです。更に発展させて、UNIX系の考え方にならって、コンソールIOとか、ネットワークのIOもファイルとして扱うなら、どのような継承関係にすればいいのか、ワクワクしてきませんか。


ここまでにご紹介してきた演算子の再定義や、コンストラクタ、デストラクタ、そして継承。センスの善し悪しはこれらの使い方だけが問題ではありません。実は、それは本質ではありません。
これらを駆使したとしても、クラスのユーザには更に複雑な使用方法をお願いしなければならないことも現実には沢山あります。それを、必要以上に複雑にせず、あるいはブラックボックスにしすぎず、様々なバランスを取りながら設計することが大切です。
クラスを作ることは、1つのアプリケーションを作ることに近い作業で、単に関数を作ることよりもずっとやりがいのある作業なのです。

0 件のコメント:

コメントを投稿