etc./StackOverFlow

3의 법칙이란?

청렴결백한 만능 재주꾼 2021. 11. 24. 06:14
반응형

질문자 :fredoverflow


  • 객체를 복사 한다는 것은 무엇을 의미합니까?
  • 복사 생성자복사 할당 연산자는 무엇입니까?
  • 언제 직접 선언해야 합니까?
  • 내 개체가 복사되지 않도록 하려면 어떻게 해야 합니까?


소개

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 는 복사 할당 연산자에 의해 수행됩니다. 대상 개체가 이미 처리해야 하는 유효한 상태에 있기 때문에 작업은 일반적으로 조금 더 복잡합니다.

우리는 복사 생성자나 할당 연산자(또는 소멸자)를 선언하지 않았기 때문에 암묵적으로 정의되어 있습니다. 표준에서 인용:

[...] 복사 생성자와 복사 할당 연산자, [...] 및 소멸자는 특수 멤버 함수입니다. [ 참고 : 프로그램이 명시적으로 선언하지 않은 경우 구현은 일부 클래스 유형에 대해 이러한 멤버 함수를 암시적으로 선언합니다. 구현은 사용되는 경우 이를 암시적으로 정의합니다. [...] 끝 노트 ] [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 개체를 얻습니다. 암시적으로 정의된 소멸자는 항상 비어 있습니다. 생성자에서 리소스를 획득하지 않았기 때문에 이 경우에도 괜찮습니다. person 소멸자가 완료된 후 암시적으로 호출됩니다.

소멸자의 본문을 실행하고 본문 내에 할당된 모든 자동 개체를 제거한 후 클래스 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. a 를 통한 변경 사항 b 를 통해 관찰할 수 있습니다.
  2. b 가 파괴되면 a.name 은 매달린 포인터입니다.
  3. 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 를 작성할 때 this->namethat.name 모두 동일한 포인터를 포함하기 때문입니다.

예외 안전

new char[...] 가 메모리 고갈로 인해 예외를 발생시키면 이 솔루션은 실패합니다. 한 가지 가능한 해결책은 지역 변수를 도입하고 명령문을 재정렬하는 것입니다.

 // 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; }

이것은 또한 명시적인 확인 없이 자체 할당을 처리합니다. 이 문제에 대한 훨씬 더 강력한 솔루션은 copy-and-swap 관용구입니다 . 그러나 여기서 예외 안전에 대한 자세한 내용은 다루지 않겠습니다. 나는 다음과 같은 점을 강조하기 위해 예외에 대해서만 언급했습니다 . 리소스를 관리하는 클래스를 작성하는 것은 어렵습니다.

복사할 수 없는 리소스

파일 핸들이나 뮤텍스와 같은 일부 리소스는 복사할 수 없거나 복사해서는 안 됩니다. 이 경우 정의를 제공하지 않고 복사 생성자와 복사 할당 연산자를 private

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

boost::noncopyable 에서 상속하거나 삭제된 것으로 선언할 수 있습니다(C++11 이상에서):

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

3의 법칙

리소스를 관리하는 클래스를 구현해야 하는 경우가 있습니다. (단일 클래스에서 여러 리소스를 관리하지 마십시오. 이것은 고통으로 이어질 것입니다.) 이 경우 세 가지 규칙을 기억하십시오.

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

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

다섯의 법칙

C++11부터 개체에는 이동 생성자와 이동 할당이라는 2개의 추가 특수 멤버 함수가 있습니다. 이러한 기능을 구현하는 다섯 가지 상태의 규칙도 있습니다.

서명이 있는 예:

 class person { std::string name; int age; public: person(const std::string& name, int age); // Ctor person(const person &) = default; // 1/5: Copy Ctor person(person &&) noexcept = default; // 4/5: Move Ctor person& operator=(const person &) = default; // 2/5: Copy Assignment person& operator=(person &&) noexcept = default; // 5/5: Move Assignment ~person() noexcept = default; // 3/5: Dtor };

제로의 법칙

3/5의 법칙은 0/3/5의 법칙이라고도 합니다. 규칙의 0 부분은 클래스를 생성할 때 특수 멤버 함수를 작성할 수 없다고 명시되어 있습니다.

조언

std::string 과 같은 기존 클래스에서 이미 리소스를 관리하기 때문에 리소스를 직접 관리할 필요가 없습니다. std::string 멤버를 사용하는 간단한 코드 char* 사용하는 복잡하고 오류가 발생하기 쉬운 대안을 비교하기만 하면 확신할 수 있습니다. 원시 포인터 멤버를 멀리하는 한 3의 규칙은 자신의 코드와 관련이 없을 것입니다.


fredoverflow

3 의 법칙은 기본적으로 C++의 경험 법칙입니다.

수업에 필요한 것이 있다면

  • 복사 생성자 ,
  • 할당 연산자 ,
  • 또는 소멸자 ,

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

그 이유는 세 가지 모두 일반적으로 리소스를 관리하는 데 사용되며 클래스가 리소스를 관리하는 경우 일반적으로 복사 및 해제를 관리해야 하기 때문입니다.

클래스가 관리하는 리소스를 복사하는 데 적절한 의미 체계가 없으면 복사 생성자와 할당 연산자를 private 으로 선언(정의 하지 않음)하여 복사를 금지하는 것을 고려하십시오.

(C++ 표준의 향후 새 버전(C++11)은 C++에 이동 의미 체계를 추가하여 3법칙을 변경할 가능성이 있습니다. 그러나 C++11 섹션을 작성하기에는 이에 대해 아는 것이 너무 적습니다. 3의 법칙에 대해.)


sbi

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

간단한 영어로 해결하는 문제의 쉬운 예:

기본이 아닌 소멸자

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

이 작업이 완료되었다고 생각할 수 있습니다.

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

일단 이들 중 하나가 소멸자에서 메모리를 삭제하고 다른 하나는 사용하려고 할 때 잘못된 메모리에 대한 포인터를 갖게 됩니다(이를 댕글링 포인터라고 함).

따라서 복사 생성자를 작성하여 파괴할 메모리 조각을 새 객체에 할당합니다.

할당 연산자 및 복사 생성자

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

이것은 새 객체와 이전 객체가 동일한 메모리 조각을 가리키므로 한 객체에서 변경하면 다른 객체에 대해서도 변경된다는 것을 의미합니다. 한 개체가 이 메모리를 삭제하면 다른 개체가 계속 사용하려고 시도합니다.

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


Stefan

기본적으로 소멸자(기본 소멸자가 아님)가 있는 경우 정의한 클래스에 메모리 할당이 일부 있음을 의미합니다. 클래스가 일부 클라이언트 코드 또는 귀하에 의해 외부에서 사용된다고 가정합니다.

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

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


fatma.ekici

객체를 복사한다는 것은 무엇을 의미합니까? 개체를 복사할 수 있는 몇 가지 방법이 있습니다. 가장 많이 언급할 가능성이 있는 두 가지 유형에 대해 이야기하겠습니다. 깊은 복사와 얕은 복사입니다.

우리는 객체 지향 언어를 사용하고 있기 때문에(또는 적어도 그렇게 가정하고 있으므로) 메모리 조각이 할당되어 있다고 가정해 보겠습니다. 그것은 객체지향 언어이기 때문에 우리가 할당한 메모리 덩어리를 쉽게 참조할 수 있습니다. 왜냐하면 그것들은 일반적으로 우리 자신의 유형과 기본으로 만들어진 우리가 정의한 기본 변수(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; 과 같은 코드를 입력할 때 호출됩니다. 기본적으로 변수를 선언하고 한 줄에 할당하면 복사 생성자가 호출됩니다. 할당 연산자는 등호를 사용할 때 발생합니다 car2 = car1; . car2 는 동일한 문에서 선언되지 않았습니다. 이러한 작업을 위해 작성하는 두 개의 코드 덩어리는 매우 유사할 수 있습니다. 사실 일반적인 디자인 패턴에는 초기 복사/할당이 정당하다고 만족하면 모든 것을 설정하기 위해 호출하는 또 다른 기능이 있습니다. 제가 작성한 긴 코드를 보면 기능이 거의 동일합니다.

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

내 개체가 복사되지 않도록 하려면 어떻게 해야 합니까? 개인 함수를 사용하여 개체에 메모리를 할당할 수 있는 모든 방법을 재정의하는 것이 합리적인 시작입니다. 사람들이 복사하는 것을 정말로 원하지 않으면 공개로 설정하고 예외를 발생시키고 개체를 복사하지 않음으로써 프로그래머에게 경고할 수 있습니다.


user1701047

언제 직접 선언해야 합니까?

3법칙에 따르면 다음 중 하나를 선언하면

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

그런 다음 세 가지를 모두 선언해야 합니다. 복사 작업의 의미를 인수해야 할 필요성은 거의 항상 일종의 리소스 관리를 수행하는 클래스에서 비롯되었으며 거의 항상 다음을 암시한다는 관찰에서 비롯되었습니다.

  • 한 복사 작업에서 수행되고 있던 리소스 관리는 다른 복사 작업에서 수행되어야 하고

  • 클래스 소멸자는 리소스 관리에도 참여합니다(일반적으로 해제). 관리해야 하는 기존 리소스는 메모리였으며, 이것이 메모리를 관리하는 모든 표준 라이브러리 클래스(예: 동적 메모리 관리를 수행하는 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); }

Ajay yadav

기존 답변 중 많은 부분이 이미 복사 생성자, 할당 연산자 및 소멸자를 만지고 있습니다. 그러나 포스트 C++11에서 이동 의미론의 도입은 이것을 3 이상으로 확장할 수 있습니다.

최근 Michael Claisse는 이 주제를 다루는 연설을 했습니다. http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


xyz

C++에서 3의 법칙은 다음 멤버 함수 중 하나에 명확한 정의가 있으면 프로그래머가 다른 두 멤버 함수를 함께 정의해야 한다는 세 가지 요구 사항의 설계 및 개발의 기본 원칙입니다. 즉 소멸자, 복사 생성자, 복사 할당 연산자의 세 가지 멤버 함수가 필수적입니다.

C++의 복사 생성자는 특수 생성자입니다. 기존 개체의 복사본과 동일한 새 개체인 새 개체를 만드는 데 사용됩니다.

복사 할당 연산자는 일반적으로 동일한 유형의 개체에 대해 기존 개체를 지정하는 데 사용되는 특수 할당 연산자입니다.

빠른 예가 있습니다.

 // default constructor My_Class a; // copy constructor My_Class b(a); // copy constructor My_Class c = a; // copy assignment operator b = a;

Marcus Thornton

출처 : http:www.stackoverflow.com/questions/4172722/what-is-the-rule-of-three

반응형