c++ - c プログラミング




3つのルールは何ですか? (6)

  • オブジェクトをコピーするとはどういう意味ですか?
  • コピーコンストラクタコピー代入演算子は何ですか?
  • 自分で宣言する必要はいつですか?
  • 私のオブジェクトがコピーされないようにするにはどうすればいいですか?

自分で宣言する必要はいつですか?

3つのルールは、あなたが

  1. コピーコンストラクタ
  2. コピー代入演算子
  3. デストラクタ

あなたは3つすべてを宣言する必要があります。 それは、コピー操作の意味を引き継ぐ必要性は、ほとんどの場合、何らかの種類のリソース管理を実行するクラスから生じたものであり、それはほとんど常に

  • 1つのコピー操作で実行されていたリソース管理が他のコピー操作で実行されている可能性があります。

  • クラスのデストラクタもリソースの管理に参加しています(通常はそれを解放します)。 管理する古典的なリソースはメモリであったため、メモリを管理するすべての標準ライブラリクラス(ダイナミックメモリ管理を実行するSTLコンテナなど)はすべて、「ビッグ3」を宣言していました(コピー操作とデストラクタの両方)。

ルール3の結果 、ユーザーが宣言したデストラクタが存在すると、単純なメンバワイズコピーがクラス内のコピー操作に適切でない可能性があることが示されます。 それは、クラスがデストラクタを宣言した場合、コピー操作が正しいことをしないため、自動的に生成されるべきではないことを示唆しています。 C ++ 98が採用された時点で、この推論の意味は完全には理解されていなかったので、C ++ 98では、宣言されたユーザの存在は、コピー操作を生成するコンパイラの意向に影響を与えませんでした。 これはC ++ 11では引き続き起こりますが、コピー操作が生成される条件を制限すると、あまりにも多くのレガシーコードが破損するためです。

私のオブジェクトがコピーされないようにするにはどうすればいいですか?

コピーコンストラクタ&コピー代入演算子をプライベートアクセス指定子として宣言します。

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

C ++ 11以降では、コピーコンストラクタと代入演算子を削除することも宣言できます

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

前書き

C ++では、ユーザー定義型の変数を値の意味論で扱います。 これは、オブジェクトがさまざまなコンテキストで暗黙的にコピーされていることを意味し、「オブジェクトのコピー」が実際に何を意味するのかを理解する必要があります。

簡単な例を考えてみましょう。

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(あなたがname(name), age(age)部分に困惑している場合、これはメンバー初期化子リストと呼ばれます)。

特別なメンバー関数

personオブジェクトをコピーするのはどういう意味ですか? main機能は、2つの異なるコピーシナリオを示しています。 初期化person b(a); コピーコンストラクタによって実行されます 。 その仕事は、既存のオブジェクトの状態に基づいて新しいオブジェクトを構築することです。 代入b = aは、 代入代入演算子によって実行されます 。 ターゲットオブジェクトはすでに処理が必要な有効な状態になっているため、その作業は一般的にもう少し複雑です。

コピーコンストラクタも代入演算子も(デストラクタも)宣言していないので、これらは暗黙的に定義されています。 標準からの引用:

[...]コピーコンストラクタとコピー代入演算子、[...]とデストラクタは特殊なメンバ関数です。 [ 注意実装では、プログラムが明示的に宣言していない場合、これらのメンバー関数を暗黙的に宣言します。 実装が使用される場合、実装は暗黙的にそれらを定義します。 [...] end note ] [n3126.pdfセクション12§1]

デフォルトでは、オブジェクトをコピーするということは、そのメンバをコピーすることです。

非共用クラスXの暗黙的に定義されたコピーコンストラクタは、そのサブオブジェクトのメンバーワイズコピーを実行します。 [n3126.pdfセクション12.8§16]

非ユニオンクラスXの暗黙的に定義されたコピー代入演算子は、そのサブオブジェクトのメンバーワイズコピー割り当てを実行します。 [n3126.pdfセクション12.8§30]

暗黙の定義

暗黙的に定義されたperson特別なメンバー関数は、次のようになります。

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

メンバーワイズコピーは、 nameageがコピーされるので、私たちは自己完結型の独立したpersonオブジェクトを取得するので、この場合に必要なものです。 暗黙的に定義されたデストラクタは、常に空です。 このケースでは、コンストラクタ内のリソースを取得していないため、これも問題ありません。 メンバーのデストラクタは、デストラクタが終了した後、暗黙的に呼び出されます。

デストラクタの本体を実行し、本体内に割り当てられた任意の自動オブジェクトを破棄した後、クラスXのデストラクタは、Xのdirect [...]メンバのデストラクタを呼び出します[n3126.pdf 12.4§6]

リソースの管理

だから、いつ特別なメンバー関数を明示的に宣言すべきですか? 私たちのクラスがリソースを管理するとき、つまりクラスのオブジェクトがそのリソースを担当するとき。 これは、通常、リソースがコンストラクタで取得される(またはコンストラクタに渡される)ことを意味し、デストラクタで解放されます。

時間の経過とともに、標準C ++以前のものに戻りましょう。 std::stringようなものはなく、プログラマーはポインタを愛していました。 personクラスは次のように見えます。

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

今日でも、人々はこのスタイルでクラスを作成し、問題に陥る。「 人をベクトルに押し込んだら、今や私は狂ったメモリエラーに陥る! 」デフォルトでは、オブジェクトのコピーはメンバのコピーを意味するが、それがポイントする文字配列ではなく 、単にポインタをコピーします。 これにはいくつかの不快な影響があります。

  1. bを介してb変化を観察aことができる。
  2. bが破壊されると、 a.nameはぶら下がりポインタです。
  3. aが破壊された場合aぶら下がっているポインタを削除すると、 未定義の動作が発生します。
  4. 割り当てには、割り当て前にどのname指されているかを考慮していないため、まもなくメモリリークが発生します。

明示的な定義

メンバワイズコピーは目的の効果を持たないので、コピーコンストラクタとコピー代入演算子を明示的に定義して、文字配列のディープコピーを作成する必要があります。

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

初期化と代入の違いに注意してください。メモリリークを防ぐためにnameに代入する前に古い状態を解凍する必要があります。 また、 x = xという形式の自己割り当てから保護する必要があります。 そのチェックがなければ、 delete[] nameソース文字列を含む配列を削除します。なぜなら、 x = xを書くx = xthis->namethat.name両方に同じポインタが含まれているからです。

例外安全性

残念ながら、 new char[...]がメモリ枯渇のために例外をスローすると、この解決策は失敗します。 考えられる解決策の1つは、ローカル変数を導入してステートメントの順序を変更することです。

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

これはまた、明示的なチェックなしに自己割り当てを処理します。 この問題のさらに堅牢な解決策はコピー&スワップイディオムですが、ここでは例外安全性の詳細については取り上げません。 私は次の点を確認するために例外を述べました: リソースを管理するクラスを書くのは難しいです。

コピー不可能なリソース

ファイルハンドルやミューテックスなど、一部のリソースをコピーすることもコピーしないこともできません。 その場合、単にコピーコンストラクタとコピー代入演算子を宣言せずにprivateとして宣言してください:

private:

    person(const person& that);
    person& operator=(const person& that);

あるいは、 boost::noncopyableから継承したり、削除して宣言したりすることもできます(C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

3つのルール

場合によっては、リソースを管理するクラスを実装する必要があります。 (1つのクラスで複数のリソースを管理することは決してありませんが、これは苦痛につながります)。その場合、 3つルールを覚えておいてください。

デストラクタ、コピーコンストラクタ、またはコピー代入演算子を明示的に宣言する必要がある場合、おそらく3つすべてを明示的に宣言する必要があります。

(残念なことに、この "ルール"はC ++標準や私が知っているコンパイラによって強制されません)。

助言

ほとんどの場合、 std::stringなどの既存のクラスはすでにそれを実行しているため、リソースを自分で管理する必要はありません。 単純なコードをstd::stringメンバーを使って、 char*を使って畳み込まれたエラーの起こりやすい代替と比較すると、確信が持てます。 未処理のポインタメンバーから離れている限り、3つのルールは自分のコードに関係する可能性は低いです。


3つルールは C ++の経験則であり、基本的に言っています

あなたのクラスが

  • コピーコンストラクタ
  • 代入演算子
  • またはデストラクタ

明らかに定義されていれば、 それらの3つすべてが必要になる可能性が高い。

その理由は、通常3つすべてがリソースの管理に使用され、クラスがリソースを管理する場合、通常はコピーを管理するだけでなく解放する必要があるからです。

クラスが管理するリソースをコピーするための良いセマンティクスがない場合は、コピーコンストラクタと代入演算子をprivateとして宣言( definingしない)してコピーを禁止することを検討してください。

(C ++の新バージョンであるC ++ 11では、C ++にmoveセマンティクスが追加される予定であることに注意してください.3つのルールが変更される可能性がありますが、C ++ 11のセクション3つのルールについて。)


オブジェクトをコピーするとはどういう意味ですか? オブジェクトをコピーするには、いくつかの方法があります。ディープコピーとシャローコピーの2種類についてお話しましょう。

私たちはオブジェクト指向言語なので(または少なくとも仮定しているので)、割り当てられたメモリがあるとしましょう。 それはOO言語なので、私たちが割り当てるメモリのまとまりは、通常、独自の型とプリミティブで作成された基本変数(int、chars、bytes)またはクラスであるため、簡単に参照できます。 それで、次のようなクラスのCarがあるとしましょう:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

ディープコピーとは、オブジェクトを宣言してオブジェクトの完全に別個のコピーを作成する場合です.2つのオブジェクトを2つの完全なメモリセットにまとめます。

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

さあ、変なことをしよう。 car2が誤ってプログラムされているか、意図的にcar1が作成されている実際のメモリを共有しようとしているとしましょう。 (これは、通常、これを行う間違いで、クラスでは通常、下で議論されている毛布です。)car2について尋ねると、car1のメモリ空間へのポインタを実際に解決しているようなふりをします。です。

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

したがって、あなたが書いている言語にかかわらず、大部分の時間をコピーしたいので、オブジェクトをコピーすることに何を意味するのかに非常に注意してください。

コピーコンストラクタとコピー代入演算子は何ですか? 私はすでに上記を使用しています。 Car car2 = car1;などのコードを入力すると、コピーコンストラクターが呼び出されますCar car2 = car1; 基本的に、変数を宣言して1行に割り当てると、コピーコンストラクタが呼び出されます。 代入演算子は、等号を使用すると何が起こるかですcar2 = car1; 。 通知car2は同じステートメントで宣言されていません。 これらの操作のために書く2つのコードは非常によく似ています。 実際、典型的なデザインパターンには、最初のコピー/割り当てが正当であると満足したら、すべてを設定する別の機能があります。私が書いた長文コードを見ると、機能はほぼ同じです。

自分で宣言する必要はいつですか? 共有するコードやプロダクション用のコードを何らかの方法で記述していない場合は、実際に必要なときに宣言するだけです。 あなたはあなたのプログラム言語があなたが「偶然」それを使用することを選択した場合、あなたがプログラムの言語が何をするのかを意識する必要があります。 たとえば、コピーコンストラクタはほとんど使用しませんが、代入演算子のオーバーライドは非常に一般的です。 どのような加算や減算なども同様に上書きできることを知っていますか?

私のオブジェクトがコピーされないようにするにはどうすればいいですか? プライベート関数を使ってオブジェクトにメモリを割り当てることが許可されているすべての方法をオーバーライドするのは合理的なスタートです。 あなたが実際にそれらをコピーしたくない場合は、例外をスローしてオブジェクトをコピーしないようにして、プログラマにパブリックにして警告することができます。


大3の法則は、上記のとおりです。

それが解決する問題の簡単な例は、簡単な英語で、

デフォルト以外のデストラクタ

コンストラクタでメモリを割り当てたので、削除するためにデストラクタを記述する必要があります。 そうしないと、メモリリークが発生します。

これは仕事が終わったと思うかもしれません。

問題は、オブジェクトのコピーが作成された場合、コピーは元のオブジェクトと同じメモリを指します。

いったんこれらのうちの1つがデストラクタ内のメモリを削除すると、もう1つは無効なメモリへのポインタを持ちます(これはダングリングポインタと呼ばれます)。

したがって、コピー・コンストラクターを作成して、新しいオブジェクトにそれら自身のメモリーを割り振って破棄するようにします。

代入演算子とコピーコンストラクタ

コンストラクタのメモリをクラスのメンバポインタに割り当てました。 このクラスのオブジェクトをコピーすると、デフォルトの代入演算子とコピーコンストラクタはこのメンバポインタの値を新しいオブジェクトにコピーします。

これは、新しいオブジェクトと古いオブジェクトが同じメモリを指していることを意味します。したがって、あるオブジェクトでそれを変更すると、もう一方のオブジェクトに対しても変更されます。 1つのオブジェクトがこのメモリを削除すると、もう1つのオブジェクトはそのメモリを使用しようとします。

これを解決するには、独自のバージョンのコピーコンストラクターと代入演算子を作成します。 あなたのバージョンは新しいオブジェクトに別のメモリを割り当て、最初のポインタがそのアドレスではなく指している値をコピーします。


既存の回答の多くは、すでにコピーコンストラクタ、代入演算子、およびデストラクタに接触しています。 しかし、ポストC ++ 11では、移動セマンティックの導入はこれを3以上に拡大する可能性があります。

最近マイケル・クライスは、このトピックに触れる話をしました: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class ://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class





rule-of-three