operator - c++ rule of three




3의 규칙은 무엇입니까? (6)

  • 객체 복사 란 무엇을 의미합니까?
  • 복사 생성자복사 할당 연산자는 무엇입니까?
  • 내가 직접 신고해야합니까?
  • 내 개체가 복사되지 않도록하려면 어떻게해야합니까?

내가 직접 신고해야합니까?

세 가지 규칙은 당신이

  1. 복사 생성자
  2. 복사 할당 연산자
  3. 폐물 소각로

그러면 세 가지를 모두 선언해야합니다. 그것은 복사 작업의 의미를 인수해야 할 필요성이 거의 항상 어떤 종류의 자원 관리를 수행하는 클래스에서 유래했으며 거의 ​​항상 그 의미를 내포하고 있다는 관찰에서 자랐습니다.

  • 어떤 복사 작업에서 자원 관리가 수행되고 있었는지는 다른 복사 작업에서 수행해야 할 필요가있을 것입니다.

  • 클래스 소멸자도 자원 관리에 참여할 것이다. 관리해야 할 고전적인 리소스는 메모리 였기 때문에 메모리를 관리하는 모든 표준 라이브러리 클래스 (예 : 동적 메모리 관리를 수행하는 STL 컨테이너)는 모두 복사 작업과 소멸자 둘 다 "빅 3"을 선언합니다.

Rule of Three의 결과는 사용자 선언 소멸자의 존재는 단순한 회원 현명한 복사본이 클래스의 복사 작업에 적합하지 않을 것임을 나타냅니다. 즉, 클래스가 소멸자를 선언하면 복사 작업이 자동으로 생성되어서는 안된다는 것입니다. 왜냐하면 올바른 작업을 수행하지 않기 때문입니다. 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 기능은 두 가지 별개의 복사 시나리오를 보여줍니다. 초기화 person b(a); 복사 생성자 가 수행합니다. 그것의 임무는 기존 객체의 상태를 기반으로 새로운 객체를 생성하는 것입니다. 할당 b = a복사 할당 연산자에 의해 수행됩니다. 대상 개체는 이미 처리해야하는 유효한 상태이기 때문에 일반적으로 작업이 좀 더 복잡합니다.

우리는 복사 생성자 나 대입 연산자 (소멸자)를 선언하지 않았기 때문에 이것들은 우리를 위해 암시 적으로 정의됩니다. 표준 견적 :

[...] 복사 생성자와 복사 할당 연산자, [...] 및 소멸자는 특별한 멤버 함수입니다. [ 참고 : 프로그램에서 명시 적으로 선언하지 않으면 구현시 암시 적으로 일부 클래스 유형에 대해 이러한 멤버 함수가 선언됩니다. 구현은 사용되는 경우 암시 적으로 정의합니다. [...] end note ] [n3126.pdf section 12 §1]

기본적으로 개체를 복사한다는 것은 해당 개체를 복사한다는 의미입니다.

비 유니온 클래스 X에 대한 암시 적으로 정의 된 복사본 생성자는 해당 하위 개체의 구성원 단위 복사본을 수행합니다. [n3126.pdf section 12.8 §16]

비 유니온 클래스 X에 대한 암시 적으로 정의 된 복사본 할당 연산자는 하위 개체의 멤버 단위 복사본 할당을 수행합니다. [n3126.pdf section 12.8 §30]

암시 적 정의

암묵적으로 정의 된 특별한 멤버 함수는 다음과 같이 보입니다.

// 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()
{
}

Memberwise 복사는 정확히 우리가이 경우에 원하는 것입니다. nameage 가 복사되므로 자체 독립적이고 독립된 개체를 얻을 수 있습니다. 암시 적으로 정의 된 소멸자는 항상 비어 있습니다. 이 경우 생성자에서 리소스를 얻지 못했기 때문에이 또한 괜찮습니다. 멤버의 소멸자는 소멸자가 끝난 후에 암시 적으로 호출됩니다.

소멸자의 본문을 실행하고 본문 내에 할당 된 자동 객체를 모두 소멸시킨 후 X 클래스의 소멸자는 X의 직접 멤버 [...]에 대한 소멸자를 호출합니다 [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;
    }
};

오늘날에도 사람들은 여전히이 스타일로 수업을 작성하고 문제를 겪습니다. " 나는 사람을 벡터로 밀어 넣었습니다. 이제 미친 메모리 오류가 발생합니다! "기본적으로 객체를 복사하는 것은 멤버를 복사하는 것을 의미하지만 name 멤버 단순히 가리키는 문자 배열이 아니라 포인터를 복사합니다! 이것은 몇 가지 불쾌한 효과가 있습니다 :

  1. b 를 통해 b 를 통한 변경 사항을 관찰 할 수 있습니다.
  2. b 가 파괴되면 a.name 은 매달린 포인터입니다.
  3. a 가 파괴되면 매달린 포인터를 삭제하면 정의되지 않은 동작이 발생 합니다.
  4. 과제는 과제 이전에 어떤 name 지적되었는지를 고려하지 않기 때문에 머지 않아 기억 누출이 발생합니다.

명시 적 정의

memberwise 복사는 원하는 효과가 없으므로 문자 배열의 전체 복사본을 만들기 위해 복사 생성자와 복사 할당 연산자를 명시 적으로 정의해야합니다.

// 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 를 쓸 때 this->namethat.name 에 같은 포인터가 포함되어 있기 때문입니다.

예외 안전성

불행히도, 메모리 부족으로 인해 new char[...] 가 예외를 throw하면이 솔루션은 실패합니다. 한 가지 가능한 해결책은 지역 변수를 도입하고 문장을 재정렬하는 것입니다.

// 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의 규칙

때로는 자원을 관리하는 클래스를 구현해야합니다. (단일 클래스에서 여러 리소스를 관리하지 마십시오. 이로 인해 고통이 발생합니다.)이 경우 세 가지 규칙을 기억하십시오.

소멸자, 복사 생성자 또는 복사 할당 연산자를 명시 적으로 선언해야하는 경우 명시 적으로 세 가지를 모두 선언해야합니다.

(불행히도,이 "규칙"은 C ++ 표준이나 내가 알고있는 컴파일러에 의해 시행되지 않습니다.)

조언

대부분의 경우 std::string 과 같은 기존 클래스가 이미 사용자를 위해 작업하므로 리소스를 직접 관리 할 필요가 없습니다. 단순히 std::string 멤버를 사용하여 간단한 코드를 char* 사용하여 복잡하고 오류가 발생하기 쉬운 대안에 비교하면 확신 할 수 있습니다. 원시 포인터 멤버로부터 멀리 떨어져있는 한, 세 가지 규칙은 자신의 코드와 관련이 없습니다.


객체 복사 란 무엇을 의미합니까? 객체를 복사 할 수있는 몇 가지 방법이 있습니다. 가장 많이 언급 할 수있는 2 가지 유형 인 딥 복사 및 얕은 복사에 대해 이야기 해 봅시다.

우리는 객체 지향 언어를 사용하고 있기 때문에 적어도 할당 된 메모리가 있다고 가정 해 봅시다. OO 언어이기 때문에 우리가 할당하는 메모리 덩어리는 대개 기본 변수 (ints, 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; 기본적으로 변수를 선언하고 한 줄에 지정하면 복사 생성자가 호출됩니다. 대입 연산자는 등호를 사용할 때 일어나는 일입니다 - car2 = car1; . 알림 car2 는 같은 문장에서 선언되지 않았습니다. 이 작업을 위해 작성한 두 코드 청크는 매우 유사합니다. 실제로 전형적인 디자인 패턴은 초기 복사 / 할당이 합법적이라고 만족하면 모든 것을 설정하는 또 다른 기능을 가지고 있습니다. 내가 작성한 긴 코드를 보면 기능은 거의 동일합니다.

내가 직접 신고해야합니까? 공유 할 코드를 작성하거나 프로덕션을 위해 어떤 방식 으로든 코드를 작성하지 않는다면 실제로 필요할 때만 선언하면됩니다. '우연히'사용하도록 선택하고 컴파일러를 만들지 않으면 프로그램 언어가하는 일을 알고 있어야합니다. 즉 컴파일러 기본값을 얻습니다. 예를 들어 복사 생성자는 거의 사용하지 않지만 대입 연산자 재정의는 매우 일반적입니다. 덧셈, 뺄셈 등의 의미를 무시할 수 있다는 것을 알고 계셨습니까?

내 개체가 복사되지 않도록하려면 어떻게해야합니까? private 함수를 사용하여 객체에 메모리를 할당 할 수있는 모든 방법을 오버라이드하는 것이 합리적입니다. 사람들이 복사하기를 정말로 원하지 않으면 예외를 던지고 객체를 복사하지 말고 프로그래머에게 알려주고 경고 할 수 있습니다.


기본적으로 소멸자 (기본 소멸자가 아님)가 있으면 정의한 클래스에 메모리가 일부 할당되어 있음을 의미합니다. 클래스가 일부 클라이언트 코드 나 외부에서 사용된다고 가정하십시오.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

MyClass에 기본 유형 지정 멤버가있는 경우 기본 할당 연산자가 작동하지만 할당 연산자가없는 포인터 멤버와 개체가 있으면 결과를 예측할 수 없습니다. 따라서 클래스의 소멸자에서 삭제해야 할 것이 있다면, 복사 생성자와 대입 연산자를 제공해야한다는 딥 복사 연산자가 필요할 수도 있습니다.


빅 3의 법칙은 위에 명시된 것과 같습니다.

간단한 영어로 쉬운 문제의 예를 들려줍니다.

비 기본 소멸자

생성자에서 메모리를 할당 했으므로이를 삭제하기 위해 소멸자를 작성해야합니다. 그렇지 않으면 메모리 누수가 발생합니다.

이 일이 끝났다고 생각할 수도 있습니다.

문제는 개체의 복사본이 만들어진 경우 복사본이 원래 개체와 동일한 메모리를 가리킬 것입니다.

일단 이들 중 하나가 소멸자의 메모리를 삭제하고, 다른 하나는 무의미한 메모리에 대한 포인터를 갖습니다 (이것은 매달린 포인터라고 함).

따라서 복사 생성자를 작성하여 새 객체에 자체 메모리 조각을 할당하여 파기하십시오.

대입 연산자 및 복사 생성자

생성자의 메모리를 클래스의 멤버 포인터에 할당했습니다. 이 클래스의 객체를 복사하면 기본 할당 연산자와 복사 생성자가이 멤버 포인터의 값을 새 객체에 복사합니다.

이것은 새로운 객체와 오래된 객체가 같은 메모리 조각을 가리키고 있다는 것을 의미합니다. 그래서 한 객체에서 그것을 변경할 때 다른 객체에서도 변경 될 것입니다. 한 개체가이 메모리를 삭제하면 다른 개체는이 메모리를 사용하려고 시도합니다.

이 문제를 해결하려면 복사 생성자와 할당 연산자에 대한 고유 한 버전을 작성하십시오. 귀하의 버전은 새로운 개체에 별도의 메모리를 할당하고 첫 번째 포인터가 가리키는 값 대신 해당 주소를 복사합니다.


Three of Rule은 C ++의 경험칙으로, 기본적으로 말하고 있습니다.

수업에

  • 복사 생성자 ,
  • 대입 연산자 ,
  • 또는 소멸자 ,

명시 적으로 정의 된 경우 세 가지 모두 가 필요할 수 있습니다 .

그 이유는 세 가지 모두가 일반적으로 리소스를 관리하는 데 사용되며, 클래스가 리소스를 관리하는 경우 대개 복사를 관리하고 해방해야합니다.

클래스가 관리하는 리소스를 복사하기위한 좋은 의미가 없다면 복사 생성자와 할당 연산자를 private 로 선언하여 ( defining 하지 않음) 복사를 금지하는 것을 고려하십시오.

C ++ 표준 (C ++ 11)의 새로운 버전이 C ++에 이동 의미를 추가하므로 규칙 3이 변경 될 가능성이 높습니다. 그러나 C ++ 11 섹션을 작성하는 데 대해서는 거의 알지 못합니다. 3의 규칙에 관하여.)





rule-of-three