- 객체 복사 란 무엇입니까 ?
- 무엇 복사 생성자 와 복사 대입 연산자는 ?
- 언제 스스로 신고해야합니까?
- 객체가 복사되지 않도록하려면 어떻게해야합니까?
답변:
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 section 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()
{
}
: Memberwise 복사는 우리가이 경우에 원하는 것을 정확하게
name
그리고 age
우리가 자체 포함, 독립적 얻을 수 있도록, 복사 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
단지 회원 가리키는 문자 배열이 아닌 포인터를 복사합니다 ! 이것은 몇 가지 불쾌한 효과가 있습니다.
a
확인할 수 있습니다 b
.b
파괴 되면 a.name
매달려있는 포인터입니다.a
삭제되면 매달려 포인터를 삭제하면 정의되지 않은 동작이 생성 됩니다.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->name
와 that.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 idiom 이지만 예외 안전에 대한 자세한 내용은 다루지 않겠습니다. 나는 다음과 같은 점을 지적하기 위해 예외를 언급 했다. 자원을 관리하는 클래스를 작성하는 것은 어렵다.
파일 핸들 또는 뮤텍스와 같은 일부 리소스는 복사 할 수 없거나 복사해서는 안됩니다. 이 경우 간단히 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;
때때로 자원을 관리하는 클래스를 구현해야합니다. (단일 클래스에서 여러 리소스를 관리하지 않으면 고통을 초래할뿐입니다.)이 경우 세 가지 규칙을 기억하십시오 .
소멸자, 복사 생성자 또는 복사 할당 연산자를 명시 적으로 선언해야하는 경우 세 가지를 모두 명시 적으로 선언해야합니다.
(안타깝게도이 "규칙"은 C ++ 표준 또는 내가 알고있는 컴파일러에 의해 시행되지 않습니다.)
C ++ 11부터 객체에는 이동 생성자와 이동 할당이라는 두 가지 추가 특수 멤버 함수가 있습니다. 이러한 기능을 구현하는 5 가지 상태의 규칙
서명이있는 예 :
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
3/5의 규칙은 0/3/5의 규칙이라고도합니다. 규칙의 0 부분은 클래스를 만들 때 특수 멤버 함수를 작성할 수 없음을 나타냅니다.
대부분의 경우 std::string
이미 리소스를 이미 관리하고 있기 때문에 리소스를 직접 관리 할 필요가 없습니다 . std::string
멤버를 사용하는 간단한 코드를 a 를 사용하여 복잡하고 오류가 발생하기 쉬운 대안 char*
과 비교하면 확신 할 수 있습니다. 원시 포인터 멤버에서 멀리 떨어져있는 한 세 규칙은 자신의 코드와 관련이 없습니다.
세 가지의 규칙은 C ++에 대한 엄지 손가락의 규칙입니다, 기본적으로 말
수업이 필요한 경우
- 복사 생성자 ,
- 할당 연산자 ,
- 또는 소멸자 ,
명확하게 정의 하면 세 가지 가 모두 필요할 것 입니다.
그 이유는 세 가지가 모두 리소스를 관리하는 데 일반적으로 사용되기 때문에 클래스가 리소스를 관리하는 경우 일반적으로 복사 및 해제를 관리해야합니다.
클래스가 관리하는 리소스를 복사하는 데 의미가 없으면 복사 생성자와 할당 연산자를로 선언하여 ( 정의 하지 않음 ) 복사를 금지하는 것이 private
좋습니다.
(다가오는 C ++ 표준의 새로운 버전 (C ++ 11)은 C ++에 이동 의미를 추가하여 3의 규칙을 변경 할 수 있습니다. 그러나 C ++ 11 섹션을 작성하기에는 이것에 대해 거의 알지 못합니다. 3의 법칙에 대해.)
boost::noncopyable
) 복사 할 수없는 클래스에서 (개인적으로) 상속하는 것 입니다. 또한 훨씬 더 명확 할 수 있습니다. C ++ 0x와 "삭제"기능이 여기에 도움이 될 수 있지만 다음과 같은 구문을 잊어 버렸습니다. /
noncopyable
표준 라이브러리의 일부가 아닌 한 , 나는 그것을 많이 개선하지는 않습니다. (아, 그리고 당신이 삭제 구문을 잊어 버린 경우, 당신은 내가 모르는 mor ethan을 잊었다. :)
)
빅 3의 법칙은 위에서 지정한대로입니다.
일반 영어로 해결되는 문제의 쉬운 예 :
기본이 아닌 소멸자
생성자에 메모리를 할당 했으므로 소멸자를 작성하여 삭제해야합니다. 그렇지 않으면 메모리 누수가 발생합니다.
이 작업이 완료된 것으로 생각할 수 있습니다.
문제는 사본이 객체로 만들어진 경우 사본이 원래 객체와 동일한 메모리를 가리킬 것입니다.
일단 이것들 중 하나가 소멸자에서 메모리를 삭제하면 다른 하나는 유효하지 않은 메모리에 대한 포인터를 갖게됩니다 (이것은 매달려있는 포인터라고 불립니다).
따라서 복사 할 생성자를 작성하여 새 객체에 자체 메모리 조각을 할당하여 파기합니다.
할당 연산자 및 복사 생성자
생성자의 메모리를 클래스의 멤버 포인터에 할당했습니다. 이 클래스의 객체를 복사하면 기본 할당 연산자와 복사 생성자가이 멤버 포인터의 값을 새 객체에 복사합니다.
즉, 새 개체와 이전 개체는 같은 메모리 조각을 가리 키므로 한 개체에서 변경하면 다른 개체도 변경됩니다. 한 개체가이 메모리를 삭제하면 다른 개체는이 메모리를 계속 사용하려고합니다.
이 문제를 해결하려면 고유 한 버전의 복사 생성자와 할당 연산자를 작성하십시오. 버전은 새 객체에 별도의 메모리를 할당하고 첫 번째 포인터가 주소가 아닌 가리키는 값을 복사합니다.
기본적으로 소멸자 (기본 소멸자가 아님)가 있으면 정의한 클래스에 메모리 할당이 있음을 의미합니다. 클래스가 일부 클라이언트 코드 또는 사용자가 외부에서 사용한다고 가정하십시오.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
MyClass에 기본 형식의 멤버가 일부만 있으면 기본 할당 연산자가 작동하지만 할당 연산자가없는 포인터 멤버 및 개체가 있으면 결과를 예측할 수 없습니다. 따라서 클래스의 소멸자에서 삭제할 항목이 있으면 깊은 복사 연산자가 필요할 수 있습니다. 즉 복사 생성자와 할당 연산자를 제공해야합니다.
객체 복사 란 무엇입니까? 딥 카피와 얕은 카피라는 객체를 복사 할 수있는 몇 가지 방법이 있습니다.
우리는 객체 지향 언어 (또는 적어도 그렇게 가정하고 있음)이므로 메모리 조각이 할당되었다고 가정 해 봅시다. OO 언어이므로 할당 된 메모리 청크를 쉽게 참조 할 수 있습니다. 일반적으로 기본 변수 (int, char, 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
는 동일한 진술로 선언되지 않습니다. 이러한 작업을 위해 작성하는 두 코드 조각은 매우 유사합니다. 실제로 일반적인 디자인 패턴에는 초기 복사 / 할당이 만족스러운 경우 모든 것을 설정하기 위해 호출하는 또 다른 함수가 있습니다. 필자가 작성한 긴 코드를 보면 함수는 거의 동일합니다.
언제 스스로 신고해야합니까? 공유하거나 프로덕션 용 코드를 작성하지 않는 경우 필요할 때만 선언하면됩니다. '우연히'언어를 사용하기로 선택하고 언어를 만들지 않은 경우 프로그램 언어가 무엇인지 알고 있어야합니다. 즉, 컴파일러 기본값을 사용합니다. 예를 들어 복사 생성자를 거의 사용하지 않지만 할당 연산자 재정의는 매우 일반적입니다. 더하기, 빼기 등의 의미를 무시할 수 있다는 것을 알고 있습니까?
객체가 복사되지 않도록하려면 어떻게해야합니까? 개인 기능으로 객체에 메모리를 할당 할 수있는 모든 방법을 무시하는 것이 합리적 시작입니다. 사람들이 실제로 복사하는 것을 원하지 않으면 공개를 만들고 예외를 발생시키고 객체를 복사하지 않음으로써 프로그래머에게 경고 할 수 있습니다.
언제 스스로 신고해야합니까?
세 가지 규칙에 따르면
그런 다음 세 가지를 모두 선언해야합니다. 복사 작업의 의미를 인계해야 할 필요성은 거의 항상 일종의 자원 관리를 수행하는 클래스에서 비롯되었으며 거의 항상 그 의미를 암시했습니다.
한 복사 작업에서 수행 된 모든 자원 관리는 다른 복사 작업에서 수행되어야 할 것입니다.
클래스 소멸자는 리소스 관리에 참여할 것입니다 (보통 리소스를 공개). 관리해야 할 기본 리소스는 메모리 였기 때문에 메모리를 관리하는 모든 표준 라이브러리 클래스 (예 : 동적 메모리 관리를 수행하는 STL 컨테이너)가 모두 복사 작업과 소멸자 모두 "큰 세 가지"를 선언하는 것입니다.
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 ++ 11 이후에서 이동 의미론의 도입은 이것을 3 이상으로 확장 할 수 있습니다.
최근 마이클 클라이 스 (Michael Claisse)는이 주제에 관한 강연을했다 : http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
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;
c++-faq
.