C ++ 컴파일러가 클래스에 대한 복사 생성자를 생성한다는 것을 알고 있습니다. 어떤 경우에 사용자 정의 복사 생성자를 작성해야합니까? 몇 가지 예를 들어 줄 수 있습니까?
C ++ 컴파일러가 클래스에 대한 복사 생성자를 생성한다는 것을 알고 있습니다. 어떤 경우에 사용자 정의 복사 생성자를 작성해야합니까? 몇 가지 예를 들어 줄 수 있습니까?
답변:
컴파일러에 의해 생성 된 복사 생성자는 멤버 단위 복사를 수행합니다. 때로는 충분하지 않습니다. 예를 들면 :
class Class {
public:
Class( const char* str );
~Class();
private:
char* stored;
};
Class::Class( const char* str )
{
stored = new char[srtlen( str ) + 1 ];
strcpy( stored, str );
}
Class::~Class()
{
delete[] stored;
}
이 경우 멤버의 stored
멤버 별 복사 는 버퍼를 복제하지 않습니다 (포인터 만 복사됩니다). 따라서 버퍼를 공유하는 첫 번째 삭제 된 복사본은 delete[]
성공적으로 호출 되고 두 번째는 정의되지 않은 동작으로 실행됩니다. 깊은 복사 복사 생성자 (및 할당 연산자)가 필요합니다.
Class::Class( const Class& another )
{
stored = new char[strlen(another.stored) + 1];
strcpy( stored, another.stored );
}
void Class::operator = ( const Class& another )
{
char* temp = new char[strlen(another.stored) + 1];
strcpy( temp, another.stored);
delete[] stored;
stored = temp;
}
delete stored[];
나는 그것이 있어야 생각delete [] stored;
std::string
. 일반적인 아이디어는 리소스를 관리하는 유틸리티 클래스 만 Big Three를 오버로드해야하며 다른 모든 클래스는 해당 유틸리티 클래스 만 사용해야하므로 Big Three를 정의 할 필요가 없습니다.
나는의 규칙이 Rule of Five
인용되지 않았다는 것에 약간 오싹합니다 .
이 규칙은 매우 간단합니다.
다섯 가지 규칙 :
소멸자, 복사 생성자, 복사 할당 연산자, 이동 생성자 또는 이동 할당 연산자 중 하나를 작성할 때마다 다른 4 개를 작성해야합니다.
그러나 따라야 할보다 일반적인 지침이 있으며, 예외 안전 코드를 작성해야 할 필요성에서 비롯됩니다.
각 리소스는 전용 개체에서 관리해야합니다.
여기 @sharptooth
의 코드는 여전히 (대부분) 괜찮지 만, 그가 그의 클래스에 두 번째 속성을 추가한다면 그것은 그렇지 않을 것입니다. 다음 클래스를 고려하십시오.
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
하면 어떻게됩니까 new Bar
던져? 이 가리키는 개체를 어떻게 삭제 mFoo
합니까? 솔루션 (기능 수준 try / catch ...)이 있지만 확장되지 않습니다.
상황을 처리하는 적절한 방법은 원시 포인터 대신 적절한 클래스를 사용하는 것입니다.
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
동일한 생성자 구현 (또는 실제로 사용 make_unique
)으로 이제 예외 안전을 무료로 사용할 수 있습니다 !!! 흥미롭지 않나요? 그리고 무엇보다도 더 이상 적절한 소멸자에 대해 걱정할 필요가 없습니다! 나는 내 자신의 작성해야합니까 Copy Constructor
하고 Assignment Operator
있기 때문에,하지만 unique_ptr
이러한 작업을 정의하지 않습니다 ...하지만 여기에 문제가되지 않습니다)
따라서 sharptooth
의 수업이 재검토되었습니다.
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
나는 당신에 대해 모르지만 내 것이 더 쉽습니다.)
나는 내 연습을 떠올려 복사 생성자를 명시 적으로 선언 / 정의해야 할 때 다음과 같은 경우를 생각할 수 있습니다. 사례를 두 가지 범주로 분류했습니다.
이 섹션에서는 해당 유형을 사용하는 프로그램의 올바른 작동을 위해 복사 생성자를 선언 / 정의해야하는 경우를 설명합니다.
이 섹션을 읽은 후 컴파일러가 자체적으로 복사 생성자를 생성하도록 허용하는 몇 가지 함정에 대해 알아 봅니다. 따라서 seand 가 그의 대답 에서 언급했듯이 새 클래스에 대한 복사 가능성을 끄고 나중에 실제로 필요할 때 의도적으로 활성화하는 것이 항상 안전 합니다.
private copy-constructor를 선언하고 이에 대한 구현을 제공하지 마십시오 (그러면 해당 유형의 객체가 클래스의 자체 범위 또는 해당 친구에 의해 복사 되더라도 빌드가 링크 단계에서 실패 함).
=delete
끝에 복사 생성자를 선언하십시오 .
이것은 가장 잘 이해 된 사례이며 실제로 다른 답변에서 언급 된 유일한 사례입니다. shaprtooth 가 그것을 꽤 잘 덮었 습니다. 객체가 독점적으로 소유해야하는 심층 복사 리소스를 모든 유형의 리소스에 적용 할 수 있다는 점만 추가하고 싶습니다. 동적 할당 메모리는 한 종류에 불과합니다. 필요한 경우 객체를 깊게 복사하려면
모든 객체가 생성 된 방식에 관계없이 어떻게 든 등록되어야하는 클래스를 고려하십시오. 몇 가지 예 :
가장 간단한 예는 현재 존재하는 개체의 총 개수를 유지하는 것입니다. 개체 등록은 정적 카운터를 증가시키는 것입니다.
더 복잡한 예는 해당 유형의 모든 기존 객체에 대한 참조가 저장되는 단일 레지스트리를 갖는 것입니다 (알림이 모든 객체에 전달 될 수 있음).
참조 카운트 스마트 포인터는이 범주에서 특별한 경우로 간주 될 수 있습니다. 새 포인터는 전역 레지스트리가 아닌 공유 리소스에 자신을 "등록"합니다.
이러한 자체 등록 작업은 유형의 모든 생성자에 의해 수행되어야하며 복사 생성자도 예외는 아닙니다.
일부 객체는 서로 다른 하위 객체 간의 직접적인 상호 참조가있는 사소하지 않은 내부 구조를 가질 수 있습니다 (사실 이러한 내부 상호 참조는이 경우를 트리거하기에 충분합니다). 컴파일러에서 제공하는 복사 생성자는 내부 개체 내 연결을 끊고 개체 간 연결로 변환합니다 .
예 :
struct MarriedMan;
struct MarriedWoman;
struct MarriedMan {
// ...
MarriedWoman* wife; // association
};
struct MarriedWoman {
// ...
MarriedMan* husband; // association
};
struct MarriedCouple {
MarriedWoman wife; // aggregation
MarriedMan husband; // aggregation
MarriedCouple() {
wife.husband = &husband;
husband.wife = &wife;
}
};
MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?
어떤 상태 (예 : default-constructed-state)에있는 동안 객체를 복사 해도 안전하고 그렇지 않으면 복사 해도 안전 하지 않은 클래스가있을 수 있습니다 . 안전한 복사 객체 복사를 허용하려면 방어 적으로 프로그래밍하는 경우 사용자 정의 복사 생성자에서 런타임 검사가 필요합니다.
경우에 따라 복사 가능해야하는 클래스는 복사 불가능한 하위 개체를 집계합니다. 일반적으로 이것은 관찰 할 수없는 상태의 객체에서 발생합니다 (이 경우는 아래 "최적화"섹션에서 자세히 설명합니다). 컴파일러는이 경우를 인식하는 데 도움이됩니다.
복사 가능해야하는 클래스는 유사 복사 가능 유형의 하위 개체를 집계 할 수 있습니다. 유사 복사 가능 형식은 엄격한 의미에서 복사 생성자를 제공하지 않지만 개체의 개념적 복사본을 만들 수있는 또 다른 생성자를 가지고 있습니다. 유형을 유사 복사 가능하게 만드는 이유는 유형의 복사 의미론에 대한 완전한 합의가 없을 때입니다.
예를 들어, 객체 자체 등록 사례를 다시 살펴보면 객체가 완전한 독립형 객체 인 경우에만 전역 객체 관리자에 등록해야하는 상황이있을 수 있습니다. 다른 개체의 하위 개체 인 경우 관리 책임은 포함 개체에 있습니다.
또는 얕은 복사와 전체 복사가 모두 지원되어야합니다 (둘 중 어느 것도 기본값이 아님).
그런 다음 최종 결정은 해당 유형의 사용자에게 맡겨집니다. 객체를 복사 할 때 의도 한 복사 방법을 명시 적으로 지정해야합니다 (추가 인수를 통해).
프로그래밍에 대한 비 방어 적 접근 방식의 경우 일반 복사 생성자와 준 복사 생성자가 모두 존재할 수도 있습니다. 이는 대부분의 경우 단일 복사 방법을 적용해야하는 반면 드물지만 잘 알려진 상황에서는 대체 복사 방법을 사용해야 할 때 정당화 될 수 있습니다. 그러면 컴파일러는 복사 생성자를 암시 적으로 정의 할 수 없다고 불평하지 않습니다. 준 복사 생성자를 통해 해당 유형의 하위 개체를 복사해야하는지 여부를 기억하고 확인하는 것은 전적으로 사용자의 책임입니다.
드물게 객체의 관찰 가능한 상태 의 하위 집합이 객체의 정체성에서 분리 할 수없는 부분을 구성 (또는 고려) 할 수 있으며 다른 객체로 이전 할 수 없어야합니다 (이는 다소 논란의 여지가있을 수 있음).
예 :
객체의 UID (하지만 자체 등록 행위에서 ID를 획득해야하므로 위의 "자체 등록"케이스에도 속함).
새 객체가 소스 객체의 기록을 상속하지 않아야하지만 대신 단일 기록 항목 " <OTHER_OBJECT_ID>에서 <TIME>에 복사 됨 "으로 시작하는 경우 객체 기록 (예 : 실행 취소 / 다시 실행 스택) .
이러한 경우 복사 생성자는 해당 하위 객체 복사를 건너 뛰어야합니다.
컴파일러 제공 복사 생성자의 서명은 하위 개체에 사용할 수있는 복사 생성자에 따라 다릅니다. 적어도 하나의 하위 객체에 실제 복사 생성자 가 없지만 (상수 참조로 소스 객체 가져 오기 ) 대신 변경 복사 생성자가있는 경우 (비상 수 참조로 소스 객체 가져 오기 ) 컴파일러는 선택의 여지가 없습니다. 그러나 암시 적으로 선언 한 다음 변경 복사 생성자를 정의합니다.
이제 하위 객체 유형의 "변이하는"복사 생성자가 실제로 소스 객체를 변형하지 않고 const
키워드 에 대해 모르는 프로그래머가 작성한 것이라면 어떻게 될까요? missing을 추가하여 해당 코드를 수정할 수없는 경우 const
다른 옵션은 올바른 서명으로 사용자 정의 복사 생성자를 선언하고 const_cast
.
내부 데이터에 대한 직접 참조를 제공 한 COW 컨테이너는 구성시 딥 복사해야합니다. 그렇지 않으면 참조 계수 핸들로 동작 할 수 있습니다.
COW는 최적화 기술이지만 복사 생성자의이 논리는 올바른 구현에 중요합니다. 이것이 바로 다음으로 넘어갈 "최적화"섹션이 아닌 여기에이 케이스를 배치 한 이유입니다.
다음과 같은 경우 최적화 문제에서 자체 복사 생성자를 정의해야 할 수 있습니다.
요소 제거 작업을 지원하지만 단순히 제거 된 요소를 삭제 된 것으로 표시하고 나중에 해당 슬롯을 재활용 할 수있는 컨테이너를 고려하십시오. 이러한 컨테이너의 복사본이 만들어지면 "삭제 된"슬롯을 그대로 유지하는 대신 남아있는 데이터를 압축하는 것이 좋습니다.
객체는 관찰 가능한 상태의 일부가 아닌 데이터를 포함 할 수 있습니다. 일반적으로 이것은 객체가 수행하는 특정 느린 쿼리 작업의 속도를 높이기 위해 객체의 수명 동안 축적 된 캐시 / 메모리 데이터입니다. 관련 작업이 수행 될 때 다시 계산되기 때문에 해당 데이터 복사를 건너 뛰는 것이 안전합니다. 이 데이터를 복사하는 것은 정당화되지 않을 수 있습니다. 객체의 관찰 가능한 상태 (캐시 된 데이터가 파생 된 상태)가 변경 작업에 의해 수정되는 경우 (그리고 객체를 수정하지 않을 경우 왜 딥을 생성 하는가) 빠르게 무효화 될 수 있습니다. 복사?)
이 최적화는 관찰 가능한 상태를 나타내는 데이터에 비해 보조 데이터가 큰 경우에만 정당화됩니다.
C ++에서는 복사 생성자를 선언하여 암시 적 복사를 비활성화 할 수 있습니다 explicit
. 그러면 해당 클래스의 객체는 함수로 전달되거나 값으로 함수에서 반환 될 수 없습니다. 이 트릭은 가벼워 보이지만 실제로 복사하는 데 비용이 많이 드는 유형에 사용할 수 있습니다 (하지만 유사 복사 가능하게 만드는 것이 더 나은 선택 일 수 있음).
C ++ 03에서는 복사 생성자를 선언 할 때도 정의해야했습니다 (물론 사용하려는 경우). 따라서 이러한 복사 생성자를 사용하는 것은 논의중인 문제에서 컴파일러가 자동으로 생성하는 것과 동일한 코드를 작성해야한다는 것을 의미했습니다.
C ++ 11 및 최신 표준에서는 기본 구현을 사용하기위한 명시 적 요청으로 특수 멤버 함수 (기본 및 복사 생성자, 복사 할당 연산자 및 소멸자)를 선언 할 수 있습니다 (선언을으로 종료
=default
).
이 답변은 다음과 같이 개선 될 수 있습니다.
- 더 많은 예제 코드 추가
- "내부 상호 참조가있는 개체"사례 설명
- 링크 추가
아래 코드 스 니펫을 고려해 보겠습니다.
class base{
int a, *p;
public:
base(){
p = new int;
}
void SetData(int, int);
void ShowData();
base(const base& old_ref){
//No coding present.
}
};
void base :: ShowData(){
cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
this->a = a;
*(this->p) = b;
}
int main(void)
{
base b1;
b1.SetData(2, 3);
b1.ShowData();
base b2 = b1; //!! Copy constructor called.
b2.ShowData();
return 0;
}
Output:
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();
b2.ShowData();
데이터를 명시 적으로 복사하기 위해 작성된 코드없이 생성 된 사용자 정의 복사 생성자가 있기 때문에 정크 출력을 제공합니다. 따라서 컴파일러는 동일하게 생성하지 않습니다.
여러분 대부분이 이미 알고 있지만이 지식을 여러분 모두와 공유하는 것을 생각해보십시오.
건배 ... 즐거운 코딩 !!!