11

関数の戻り値がオブジェクトである場合のC++の動作をテストしたかったのです。この小さな例を作成して、割り当てられたバイト数を監視し、コンパイラがオブジェクトのコピーを作成するか(オブジェクトがパラメータとして渡される場合など)、代わりに何らかの参照を返すかどうかを判断しました。

しかし、この非常に単純なプログラムを実行できなかったため、理由がわかりません。エラーの内容:dbgdel.cppファイルの「デバッグアサーションに失敗しました!式:BLOCK_TYPE_IS_INVALID」。Projectはwin32コンソールアプリケーションです。しかし、私はこのコードに何か問題があると確信しています。

class Ctest1
{
public:
   Ctest1(void);
   ~Ctest1(void);

   char* classSpace;
};

Ctest1::Ctest1(void)
{
   classSpace = new char[100];
}

Ctest1::~Ctest1(void)
{
   delete [] classSpace;
}

Ctest1 Function(Ctest1* cPtr){
   return *cPtr;    
}

int _tmain(int argc, _TCHAR* argv[])
{
   Ctest1* cPtr;

   cPtr=new Ctest1();


   for(int i=1;i<10;i++)
      *cPtr = Function(cPtr);


   delete cPtr;

   return 0;
   }
4

4 に答える 4

15

三つのルールに違反しました。

具体的には、オブジェクトを返すと、コピー作成されてから破棄されます。つまり、次のような一連のイベントがあります

Ctest1::Ctest1(void);
Ctest1::Ctest1(const Ctest1&);
Ctest1::~Ctest1();
Ctest1::~Ctest1();

つまり、2つのオブジェクトが作成されます。元のオブジェクトの構築と、それに続く暗黙的なコピーコンストラクタです。次に、これらのオブジェクトの両方が削除されます。

これらのオブジェクトには両方とも同じdeleteポインタが含まれているため、同じ値で2回 呼び出すことになります。ブーム


追加のクレジット:「コピーがどのように作成されるのか疑問に思う」などの問題を調査するとき、次のような興味深いクラスメソッドにprintステートメントを配置します。

#include <iostream>

int serial_source = 0;
class Ctest1
{
#define X(s) (std::cout << s << ": " << serial << "\n")
  const int serial;
public:
   Ctest1(void) : serial(serial_source++) {
     X("Ctest1::Ctest1(void)");
   }
   ~Ctest1(void) {
    X("Ctest1::~Ctest1()");
   }
   Ctest1(const Ctest1& other) : serial(serial_source++) {
    X("Ctest1::Ctest1(const Ctest1&)");
    std::cout << " Copied from " << other.serial << "\n";
   }
   void operator=(const Ctest1& other) {
     X("operator=");
     std::cout << " Assigning from " << other.serial << "\n";
   }
#undef X
};

Ctest1 Function(Ctest1* cPtr){
   return *cPtr;    
}

int main()
{
   Ctest1* cPtr;

   cPtr=new Ctest1();


   for(int i=1;i<10;i++)
      *cPtr = Function(cPtr);

   delete cPtr;

   return 0;
}
于 2012-04-05T20:30:49.633 に答える
8

あなたが最初に尋ねようとしていたことに(最終的に)到達すると、簡単な答えはそれが問題になることはめったにないということです。この標準には、コピーコンストラクターに副作用がある場合でも、コンパイラーが戻り値に対して実際にコピーコンストラクターを使用する必要がないことを明確に免除する句が含まれているため、違いは外部からわかります。

変数を返すのか、単に値を返すのかに応じて、これは名前付き戻り値最適化(NRVO)または単に戻り値最適化(RVO)と呼ばれます。最も合理的な最新のコンパイラは両方を実装します(g ++などの一部は最適化をオフにした場合でも実装します)。

戻り値のコピーを回避するために、コンパイラーは、コピーが行われるアドレスを非表示のパラメーターとして関数に渡します。次に、関数はその場所に戻り値を作成するため、関数が戻った後、値はコピーされずにすでにそこにあります。

これは十分に一般的であり、Dave Abrahams(当時はC ++標準委員会のメンバー)が数年前に記事を書いたので、最近のコンパイラでは、余分なコピーを回避しようとすると、実際にはあなたよりも遅いコードが生成されることがよくあります。シンプルでわかりやすいコードを書くだけです。

于 2012-04-05T20:52:38.720 に答える
5

Robが言ったように、C++が使用する3つのコンストラクター/代入演算子すべてを作成していません。彼が言及した三つのルールの意味するところは、デストラクタ、コピーコンストラクタ、または代入演算子(operator=())を宣言する場合、3つすべてを使用する必要があるということです。

これらの関数を作成しない場合、コンパイラーはそれらの独自のバージョンを作成します。ただし、コンパイラーはコンストラクターをコピーし、代入演算子は元のオブジェクトから要素の浅いコピーのみを実行します。これは、戻り値として作成されてからのオブジェクトにコピーされたコピーされたオブジェクトがmain()、最初に作成したオブジェクトと同じアドレスへのポインタを持っていることを意味します。したがって、元のオブジェクトが破棄されてコピーされたオブジェクト用のスペースが確保されると、ヒープ上のclassSpace配列が解放され、コピーされたオブジェクトのポインターが無効になります。

于 2012-04-05T20:39:28.597 に答える
2

オブジェクトのコピーがいつ作成されるかを確認したい場合は、次のようにします。

struct Foo {
    Foo() { std::cout << "default ctor\n"; }
    Foo(Foo const &) { std::cout << "copy ctor\n"; }
    Foo(Foo &&) { std::cout << "move ctor\n"; }
    Foo &operator=(Foo const &) { std::cout << "copy assign\n"; return *this; }
    Foo &operator=(Foo &&) { std::cout << "move assign\n"; return *this; }
    ~Foo() { std::cout << "dtor\n"; }
};

Foo Function(Foo* f){
   return *f;    
}

int main(int argc,const char *argv[])
{
   Foo* f=new Foo;

   for(int i=1;i<10;i++)
      *f = Function(f);

   delete f;
}
于 2012-04-05T20:51:19.090 に答える