C++での基本的なポインタの理解をテストするための次のコードがあります。
// Integer.cpp
#include "Integer.h"
Integer::Integer()
{
value = new int;
*value = 0;
}
Integer::Integer( int intVal )
{
value = new int;
*value = intVal;
}
Integer::~Integer()
{
delete value;
}
Integer::Integer(const Integer &rhInt)
{
value = new int;
*value = *rhInt.value;
}
int Integer::getInteger() const
{
return *value;
}
void Integer::setInteger( int newInteger )
{
*value = newInteger;
}
Integer& Integer::operator=( const Integer& rhInt )
{
*value = *rhInt.value;
return *this;
}
// IntegerTest.cpp
#include <iostream>
#include <cstdlib>
#include "Integer.h"
using namespace std;
void displayInteger( char* str, Integer intObj )
{
cout << str << " is " << intObj.getInteger() << endl;
}
int main( int argc, char* argv[] )
{
Integer intVal1;
Integer intVal2(10);
displayInteger( "intVal1", intVal1 );
displayInteger( "intVal2", intVal2 );
intVal1 = intVal2;
displayInteger( "intVal1", intVal1 );
return EXIT_SUCCESS;
}
このコードは期待どおりに機能し、次のように出力します。
intVal1 is 0
intVal2 is 10
intVal1 is 10
ただし、コピーコンストラクターを削除すると、次のように出力されます。
intVal1 is 0
intVal2 is 10
intVal1 is 6705152
なぜそうなのかわかりません。私の理解では、コピーコンストラクターは、存在しないオブジェクトへの割り当ての場合に使用されます。ここに intVal1
は存在するのに、なぜ代入演算子が呼び出されないのですか?
コピーコンストラクターは、割り当て中には使用されません。 displayInteger
関数に引数を渡すときに、あなたのケースのコピーコンストラクターが使用されます。 2番目のパラメーターは値で渡されます。つまり、コピーコンストラクターによって初期化されます。
コピーコンストラクターのバージョンは、クラスが所有するデータのdeepコピーを実行します(割り当て演算子と同じように)。そのため、コピーコンストラクタのバージョンですべてが正しく機能します。
独自のコピーコンストラクターを削除すると、コンパイラーはそれを暗黙的に生成します。コンパイラーが生成したコピーコンストラクターは、オブジェクトのshallowコピーを実行します。これは "Rule of Three" に違反し、クラスの機能を破壊します。これは、実験で観察したとおりです。基本的に、displayInteger
への最初の呼び出しはintVal1
オブジェクトに損傷を与え、displayInteger
への2番目の呼び出しはintVal2
オブジェクトに損傷を与えます。その後、両方のオブジェクトが壊れます。そのため、3番目のdisplayInteger
呼び出しはガベージを表示します。
displayInteger
の宣言を次のように変更した場合
void displayInteger( char* str, const Integer &intObj )
明示的なコピーコンストラクターがなくても、コードは "動作"します。しかし、いかなる場合でも "Rule of Three" を無視することはお勧めできません。この方法で実装されたクラスは、「3つのルール」に従うか、コピー不可にする必要があります。
発生している問題は、デフォルトのコピーコンストラクターによって引き起こされます。これは、ポインターをコピーしますが、新しく割り当てられたメモリに関連付けません(コピーコンストラクターの実装のように)。オブジェクトを値で渡すと、コピーが作成され、実行がスコープ外になると、このコピーは破棄されます。デストラクタからのdelete
は、intVal1
オブジェクトのvalue
ポインタを無効にし、それをdangling pointerにして、逆参照しますこのうちundefined behaviorが発生します。
デバッグ出力は、コードの動作を理解するために使用できます。
class Integer {
public:
Integer() {
cout << "ctor" << endl;
value = new int;
*value = 0;
}
~Integer() {
cout << "destructor" << endl;
delete value;
}
Integer(int intVal) {
cout << "ctor(int)" << endl;
value = new int;
*value = intVal;
}
Integer(const Integer &rhInt) {
cout << "copy ctor" << endl;
value = new int;
*value = *rhInt.value;
}
Integer& operator=(const Integer& rhInt){
cout << "assignment" << endl;
*value = *rhInt.value;
return *this;
}
int *value;
};
void foo(Integer intObj) {
cout << intObj.value << " " << *(intObj.value) << endl;
}
このコードの出力:
Integer intVal1;
Integer intVal2(10);
foo( intVal1 );
foo( intVal2 );
intVal1 = intVal2;
foo( intVal1 );
です:
俳優
ctor(int)
コクター
0x9ed4028 0
デストラクタ
コクター
0x9ed4038 10
デストラクター
割り当て
コクター
0x9ed4048 10
デストラクター
デストラクター
デストラクター
これは、値でオブジェクトを渡すときにコピーコンストラクタが使用されることを示しています。ただし、ここで注意する必要があるのは、関数から戻ったときに呼び出されるデストラクタです。コピーコンストラクターの実装を削除すると、出力は次のようになります。
俳優
ctor(int)
x8134008 0
デストラクター
0x8134018 10
デストラクター
割り当て
x8134008 135479296
デストラクター
デストラクター
デストラクター
最初のコピーが、後で3番目のコピーで使用されたのと同じポインター(0x8134008
を指す)でdelete
を呼び出し、このダングリングポインターが指すメモリが使用されたことを示します。
この呼び出しについて考えてみましょう:
displayInteger( "intVal1", intVal1 );
intObj
のdisplayInteger
パラメータにintVal1
のコピーを作成しています:
void displayInteger( char* str, Integer intObj )
{
cout << str << " is " << intObj.getInteger() << endl;
}
そのコピーは、intVal1
と同じint
を指します。 displayInteger
が戻ると、intObj
が破棄され、int
が破棄され、intVal1
のポインターが無効なオブジェクトを指します。その時点で値にアクセスしようとすると、すべてのベットがオフになります(A.K.A.未定義の動作)。 intVal2
でも同様のことが起こります。
より一般的なレベルでは、コピーコンストラクターを削除すると、3つのルールに違反することになります。これは、通常、この種の問題につながります。