第9章 コピー

 実は、CIntArray クラスには大きな問題点があります。関数に渡してみれば分かると思います。こういうときには、どうすればいいのでしょうか?


 今回の要点は以下の通りです。


 では、いってみましょう。


 第1部第59章で言ったように、構造体やクラスは参照渡しをするのが普通です。ですが、値渡ししたいときもあるでしょう(参照渡し、値渡しについては第1部第34章を参照)。そして、誰かに勝手に値渡しされてしまうことも、自分でうっかり値渡ししてしまうこともあるでしょう。

 では、CIntArray を値渡ししてみましょう。プログラムは、一旦 A から Z までの文字コードを配列に入れておき、それを関数 Disp で表示するというものです。

プログラム
// Copy1.cpp
#include <iostream.h>
#include "IntArray.h"

// CIntArray は IntArray.h で宣言され、
// IntArray.cpp で実装されているとします

void Disp(CIntArray array)
{
    int i;

    for(i = 0; i < array.NumOf(); i++)
        cout << array.Get(i) << ' ';
    cout << endl;
}

int main()
{
    CIntArray arrayAtoZ('Z' - 'A' + 1);
    int       i;

    for(i = 0; i < arrayAtoZ.NumOf(); i++)
        arrayAtoZ.Set(i, 'A' + i);
    Disp(arrayAtoZ);

    return 0;
}

 おや、実行結果が書いてありませんね。そうです。エラー終了してしまいます。

 では、何がいけなかったのでしょうか? それは、引数の仕組みを考えるといいでしょう。第1部第34章の要点はこうでした。

 これです。これが原因なのです。実際に処理を追ってみるとよく分かると思います。

 先ず、Disp 関数に入るときに仮引数 array が作られます。そして、そこに arrayAtoZ の内容がコピーされます。

 ここで注意することは、コピーされるのは m_pnum と m_nNumOf の値であるということです。m_nNumOf は構わないのですが、問題は m_pnum です。

 m_pnum の値とは何でしたか? そうです。new で確保したメモリの先頭アドレスですね。配列がコピーされるわけではないのです。

 で、Disp 関数が終わります。仮引数の寿命はここまでなので、ここで array のデストラクタが呼ばれます。すると、何とメモリが解放されてしまいます

 で、Disp 関数を抜け、今度は main 関数を抜けようとします。すると、ここで arrayAtoZ のデストラクタが呼ばれます。しかし、もう既に m_pnum の指していたメモリは解放されています。なので、delete しようとするとエラーが出てしまうのです。もちろん、delete だけでなく Get や Set を使ったのでもおかしくなってしまいます

 以上が、おかしくなった経過です。


 値渡しにするからには、配列がコピーされることを期待しているのです。そうでなければ参照渡しを使えばいいのです。では、これはどのように解決できるのでしょうか?

 ここで、またコンストラクタの出番です。実は、仮引数が作られるときもコンストラクタが呼ばれるのです。このコンストラクタは、コンストラクタの中でも特殊なコピーコンストラクタというものです。

 コピーコンストラクタは、そのクラス型への参照を引数にとるコンストラクタです。例えば CIntArray なら、CIntArray(const CIntArray& rother) のようなものです。この仮引数 rother ですが、これは実引数への参照になります。上の例では arrayAtoZ への参照になるわけです。

 このコピーコンストラクタでメモリの確保を行い、配列をコピーしてやればいいのです。

 しかし、上の例ではコピーコンストラクタなどを作った覚えはありません。コピーコンストラクタを作らなかった場合は普通の値渡しを行うわけです。普通の値渡しに相当するコピーコンストラクタが自動的に作られる、と言った方が正確でしょうか。

 では、コピーコンストラクタを追加してみましょう。

プログラム1プログラム3
// IntArray.h
#ifndef __INTARRAY_H_INCLUDED__
#define __INTARRAY_H_INCLUDED__

#include <stddef.h>

class CIntArray
{
    // コピーコンストラクタ
public:
    CIntArray(CIntArray& rother);

    // 他のメンバは省略します
};

// インライン関数の実装も省略します

#endif
// Copy1.cpp
#include <iostream.h>
#include "IntArray.h"

void Viss(const int num)
{
    cout << "Viss : No." << num << endl;
}

void Disp(CIntArray array)
{
    Viss(2);

    int i;

    for(i = 0; i < array.NumOf(); i++)
        cout << array.Get(i) << ' ';
    cout << endl;

    Viss(3);
}

int main()
{
    CIntArray arrayAtoZ('Z' - 'A' + 1);
    int       i;

    for(i = 0; i < arrayAtoZ.NumOf(); i++)
        arrayAtoZ.Set(i, 'A' + i);

    Viss(1);
    Disp(arrayAtoZ);
    Viss(4);

    return 0;
}
プログラム2
// IntArray.cpp
#include <iostream.h>
#include <memory.h>
#include <process.h>
#include "IntArray.h"

// コピーコンストラクタ
CIntArray::CIntArray(CIntArray& rother)
{
    if(rother.Success() == false)
    {
        m_pnum   = NULL;
        m_nNumOf = 0;
    }
    else
    {
        m_pnum = new int[rother.NumOf()];
        if(m_pnum == NULL)
        {
            m_nNumOf = 0;
            return;
        }

        // memcpy はメモリの内容をバイト単位でコピーする関数です
        memcpy(m_pnum, rother.m_pnum, rother.SizeOf());
        m_nNumOf = rother.m_nNumOf;
    }

    cout << "コピーコンストラクタが呼ばれました。" << endl;
}

// 他の関数の実装は省略します
実行結果
コンストラクタが呼ばれました。要素数は 26 です。
Viss : No.1
コピーコンストラクタが呼ばれました。
Viss : No.2
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
Viss : No.3
デストラクタが呼ばれました。要素数は 26 でした。
Viss : No.4
デストラクタが呼ばれました。要素数は 26 でした。

 無事、エラーも出ずにプログラムを実行することができました。

 コピーコンストラクタの呼ばれるタイミングも確認して下さい。Viss(1); と Viss(2); の間になっています。ここは仮引数が作られるときですね。仮引数が作られるときにコピーコンストラクタが呼ばれていることがよく分かります。

 そして、仮引数の寿命は Viss(3); と Viss(4); の間、つまり Disp 関数の終わるときに尽きています。こうやって確認のテキストを表示すると、何がどう機能しているかがよく分かりますね。


 このコピーコンストラクタですが、もちろん普通のコンストラクタとしても機能します。例えば、

CIntArray array1(100);
int       i;

for(i = 0; i < array1.NumOf(); i++)
    array1.Set(i, i * 10);

CIntArray array2(array1);

のような感じです。array2 を作るとき、コピーコンストラクタが呼ばれますね。


 では、今回の要点です。


 今回のプログラムを眺めると、何か違和感を感じる人もいるかもしれません。いつもベタベタつけている「アレ」が足りないような気がしませんか? 「アレ」が一体何なのかを見つけてから次回を見て欲しいですね。


第8章 インライン関数・再び | 第10章 不動の構え

Last update was done on 2000.8.30

この講座の著作権はロベールが保有しています