언제 복사 생성자를 사용해야합니까?


87

C ++ 컴파일러가 클래스에 대한 복사 생성자를 생성한다는 것을 알고 있습니다. 어떤 경우에 사용자 정의 복사 생성자를 작성해야합니까? 몇 가지 예를 들어 줄 수 있습니까?



1
자신의 카 피터를 작성하는 경우 중 하나 : 딥 카피를해야 할 때. 또한 ctor를 생성하자마자 기본 ctor가 생성되지 않습니다 (기본 키워드를 사용하지 않는 한).
harshvchawla

답변:


75

컴파일러에 의해 생성 된 복사 생성자는 멤버 단위 복사를 수행합니다. 때로는 충분하지 않습니다. 예를 들면 :

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

10
비트 단위는 수행하지 않지만 특히 클래스 유형 멤버에 대한 복사기를 호출하는 멤버 단위 복사를 수행합니다.
Georg Fritzsche

7
assingment 연산자를 그렇게 쓰지 마십시오. 예외적으로 안전하지 않습니다. (새로 인해 예외가 발생하면 객체는 메모리의 할당 취소 된 부분을 가리키는 저장소와 함께 정의되지 않은 상태로 남습니다 (발생할 수있는 모든 작업이 성공적으로 완료된 후에 만 ​​메모리 할당 해제)). 간단한 해결책은 복사 스왑 idium을 사용하는 것입니다.
Martin York

당신이 바닥에서 3 선을 @sharptooth delete stored[];나는 그것이 있어야 생각delete [] stored;
피터 Ajtai

4
나는 그것이 단지 예일 뿐이라는 것을 알고 있지만 더 나은 해결책은를 사용하는 것임을 지적해야합니다 std::string. 일반적인 아이디어는 리소스를 관리하는 유틸리티 클래스 만 Big Three를 오버로드해야하며 다른 모든 클래스는 해당 유틸리티 클래스 만 사용해야하므로 Big Three를 정의 할 필요가 없습니다.
GManNickG

2
@Martin : 돌에 새겨 져 있는지 확인하고 싶었습니다. : P
GManNickG

46

나는의 규칙이 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;
};

나는 당신에 대해 모르지만 내 것이 더 쉽습니다.)


C ++ 11의 경우-이동 생성자 및 이동 할당 연산자의 3 개의 규칙에 추가되는 5 개의 규칙.
Robert Andrzejuk 2017 년

1
@Robb : 실제로 마지막 예제에서 설명한 것처럼 일반적으로 Rule Of Zero를 목표로해야합니다 . 특수 (일반) 기술 클래스 만 하나의 리소스 를 처리하는 데 관심을 가져야 하며 다른 모든 클래스는 스마트 포인터 / 컨테이너를 사용해야하며 걱정하지 않아야합니다.
Matthieu M.

@MatthieuM. 동의했습니다. :-) 저는 Rule of Five를 언급했습니다.이 답변은 C ++ 11 이전에 "Big Three"로 시작하기 때문입니다. 그러나 이제 "Big Five"가 적절하다는 점을 언급해야합니다. 나는이 답변이 질문 된 맥락에서 정확하기 때문에 반대 투표하고 싶지 않습니다.
Robert Andrzejuk

@Robb : 좋은 지적, Big Three 대신 Rule of Five를 언급하도록 답변을 업데이트했습니다. 바라건대 대부분의 사람들이 지금까지 C ++ 11 가능 컴파일러로 옮겨 갔으면합니다 (아직도 안타깝게 생각합니다).
Matthieu M.

32

나는 내 연습을 떠올려 복사 생성자를 명시 적으로 선언 / 정의해야 할 때 다음과 같은 경우를 생각할 수 있습니다. 사례를 두 가지 범주로 분류했습니다.

  • 정확성 / 의미 -사용자 정의 복사 생성자를 제공하지 않으면 해당 유형을 사용하는 프로그램이 컴파일되지 않거나 잘못 작동 할 수 있습니다.
  • 최적화 -컴파일러 생성 복사 생성자에 대한 좋은 대안을 제공하면 프로그램을 더 빠르게 만들 수 있습니다.


정확성 / 의미

이 섹션에서는 해당 유형을 사용하는 프로그램의 올바른 작동을 위해 복사 생성자를 선언 / 정의해야하는 경우를 설명합니다.

이 섹션을 읽은 후 컴파일러가 자체적으로 복사 생성자를 생성하도록 허용하는 몇 가지 함정에 대해 알아 봅니다. 따라서 seand 가 그의 대답 에서 언급했듯이 새 클래스에 대한 복사 가능성을 끄고 나중에 실제로 필요할 때 의도적으로 활성화하는 것이 항상 안전 합니다.

C ++ 03에서 클래스를 복사 할 수 없도록 만드는 방법

private copy-constructor를 선언하고 이에 대한 구현을 제공하지 마십시오 (그러면 해당 유형의 객체가 클래스의 자체 범위 또는 해당 친구에 의해 복사 되더라도 빌드가 링크 단계에서 실패 함).

C ++ 11 이상에서 클래스를 복사 할 수 없도록 만드는 방법

=delete끝에 복사 생성자를 선언하십시오 .


얕은 대 전체 복사

이것은 가장 잘 이해 된 사례이며 실제로 다른 답변에서 언급 된 유일한 사례입니다. shaprtooth 가 그것을 꽤 잘 덮었 습니다. 객체가 독점적으로 소유해야하는 심층 복사 리소스를 모든 유형의 리소스에 적용 할 수 있다는 점만 추가하고 싶습니다. 동적 할당 메모리는 한 종류에 불과합니다. 필요한 경우 객체를 깊게 복사하려면

  • 디스크에 임시 파일 복사
  • 별도의 네트워크 연결 열기
  • 별도의 작업자 스레드 생성
  • 별도의 OpenGL 프레임 버퍼 할당
  • 기타

자체 등록 개체

모든 객체가 생성 된 방식에 관계없이 어떻게 든 등록되어야하는 클래스를 고려하십시오. 몇 가지 예 :

  • 가장 간단한 예는 현재 존재하는 개체의 총 개수를 유지하는 것입니다. 개체 등록은 정적 카운터를 증가시키는 것입니다.

  • 더 복잡한 예는 해당 유형의 모든 기존 객체에 대한 참조가 저장되는 단일 레지스트리를 갖는 것입니다 (알림이 모든 객체에 전달 될 수 있음).

  • 참조 카운트 스마트 포인터는이 범주에서 특별한 경우로 간주 될 수 있습니다. 새 포인터는 전역 레지스트리가 아닌 공유 리소스에 자신을 "등록"합니다.

이러한 자체 등록 작업은 유형의 모든 생성자에 의해 수행되어야하며 복사 생성자도 예외는 아닙니다.


내부 상호 참조가있는 개체

일부 객체는 서로 다른 하위 객체 간의 직접적인 상호 참조가있는 사소하지 않은 내부 구조를 가질 수 있습니다 (사실 이러한 내부 상호 참조는이 경우를 트리거하기에 충분합니다). 컴파일러에서 제공하는 복사 생성자는 내부 개체 내 연결을 끊고 개체 연결로 변환합니다 .

예 :

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)에있는 동안 객체를 복사 해도 안전하고 그렇지 않으면 복사 해도 안전 하지 않은 클래스가있을 수 있습니다 . 안전한 복사 객체 복사를 허용하려면 방어 적으로 프로그래밍하는 경우 사용자 정의 복사 생성자에서 런타임 검사가 필요합니다.


복사 할 수없는 하위 개체

경우에 따라 복사 가능해야하는 클래스는 복사 불가능한 하위 개체를 집계합니다. 일반적으로 이것은 관찰 할 수없는 상태의 객체에서 발생합니다 (이 경우는 아래 "최적화"섹션에서 자세히 설명합니다). 컴파일러는이 경우를 인식하는 데 도움이됩니다.


유사 복사 가능한 하위 개체

복사 가능해야하는 클래스는 유사 복사 가능 유형의 하위 개체를 집계 할 수 있습니다. 유사 복사 가능 형식은 엄격한 의미에서 복사 생성자를 제공하지 않지만 개체의 개념적 복사본을 만들 수있는 또 다른 생성자를 가지고 있습니다. 유형을 유사 복사 가능하게 만드는 이유는 유형의 복사 의미론에 대한 완전한 합의가 없을 때입니다.

예를 들어, 객체 자체 등록 사례를 다시 살펴보면 객체가 완전한 독립형 객체 인 경우에만 전역 객체 관리자에 등록해야하는 상황이있을 수 있습니다. 다른 개체의 하위 개체 인 경우 관리 책임은 포함 개체에 있습니다.

또는 얕은 복사와 전체 복사가 모두 지원되어야합니다 (둘 중 어느 것도 기본값이 아님).

그런 다음 최종 결정은 해당 유형의 사용자에게 맡겨집니다. 객체를 복사 할 때 의도 한 복사 방법을 명시 적으로 지정해야합니다 (추가 인수를 통해).

프로그래밍에 대한 비 방어 적 접근 방식의 경우 일반 복사 생성자와 준 복사 생성자가 모두 존재할 수도 있습니다. 이는 대부분의 경우 단일 복사 방법을 적용해야하는 반면 드물지만 잘 알려진 상황에서는 대체 복사 방법을 사용해야 할 때 정당화 될 수 있습니다. 그러면 컴파일러는 복사 생성자를 암시 적으로 정의 할 수 없다고 불평하지 않습니다. 준 복사 생성자를 통해 해당 유형의 하위 개체를 복사해야하는지 여부를 기억하고 확인하는 것은 전적으로 사용자의 책임입니다.


개체의 ID와 밀접하게 관련된 상태를 복사하지 마십시오.

드물게 객체의 관찰 가능한 상태 의 하위 집합이 객체의 정체성에서 분리 할 수없는 부분을 구성 (또는 고려) 할 수 있으며 다른 객체로 이전 할 수 없어야합니다 (이는 다소 논란의 여지가있을 수 있음).

예 :

  • 객체의 UID (하지만 자체 등록 행위에서 ID를 획득해야하므로 위의 "자체 등록"케이스에도 속함).

  • 새 객체가 소스 객체의 기록을 상속하지 않아야하지만 대신 단일 기록 항목 " <OTHER_OBJECT_ID>에서 <TIME>에 복사 됨 "으로 시작하는 경우 객체 기록 (예 : 실행 취소 / 다시 실행 스택) .

이러한 경우 복사 생성자는 해당 하위 객체 복사를 건너 뛰어야합니다.


복사 생성자의 올바른 서명 적용

컴파일러 제공 복사 생성자의 서명은 하위 개체에 사용할 수있는 복사 생성자에 따라 다릅니다. 적어도 하나의 하위 객체에 실제 복사 생성자 가 없지만 (상수 참조로 소스 객체 가져 오기 ) 대신 변경 복사 생성자가있는 경우 (비상 수 참조로 소스 객체 가져 오기 ) 컴파일러는 선택의 여지가 없습니다. 그러나 암시 적으로 선언 한 다음 변경 복사 생성자를 정의합니다.

이제 하위 객체 유형의 "변이하는"복사 생성자가 실제로 소스 객체를 변형하지 않고 const키워드 에 대해 모르는 프로그래머가 작성한 것이라면 어떻게 될까요? missing을 추가하여 해당 코드를 수정할 수없는 경우 const다른 옵션은 올바른 서명으로 사용자 정의 복사 생성자를 선언하고 const_cast.


COW (Copy-On-Write)

내부 데이터에 대한 직접 참조를 제공 한 COW 컨테이너는 구성시 딥 복사해야합니다. 그렇지 않으면 참조 계수 핸들로 동작 할 수 있습니다.

COW는 최적화 기술이지만 복사 생성자의이 논리는 올바른 구현에 중요합니다. 이것이 바로 다음으로 넘어갈 "최적화"섹션이 아닌 여기에이 케이스를 배치 한 이유입니다.



최적화

다음과 같은 경우 최적화 문제에서 자체 복사 생성자를 정의해야 할 수 있습니다.


복사 중 구조 최적화

요소 제거 작업을 지원하지만 단순히 제거 된 요소를 삭제 된 것으로 표시하고 나중에 해당 슬롯을 재활용 할 수있는 컨테이너를 고려하십시오. 이러한 컨테이너의 복사본이 만들어지면 "삭제 된"슬롯을 그대로 유지하는 대신 남아있는 데이터를 압축하는 것이 좋습니다.


관찰 불가능한 상태 복사 건너 뛰기

객체는 관찰 가능한 상태의 일부가 아닌 데이터를 포함 할 수 있습니다. 일반적으로 이것은 객체가 수행하는 특정 느린 쿼리 작업의 속도를 높이기 위해 객체의 수명 동안 축적 된 캐시 / 메모리 데이터입니다. 관련 작업이 수행 될 때 다시 계산되기 때문에 해당 데이터 복사를 건너 뛰는 것이 안전합니다. 이 데이터를 복사하는 것은 정당화되지 않을 수 있습니다. 객체의 관찰 가능한 상태 (캐시 된 데이터가 파생 된 상태)가 변경 작업에 의해 수정되는 경우 (그리고 객체를 수정하지 않을 경우 왜 딥을 생성 하는가) 빠르게 무효화 될 수 있습니다. 복사?)

이 최적화는 관찰 가능한 상태를 나타내는 데이터에 비해 보조 데이터가 큰 경우에만 정당화됩니다.


암시 적 복사 비활성화

C ++에서는 복사 생성자를 선언하여 암시 적 복사를 비활성화 할 수 있습니다 explicit. 그러면 해당 클래스의 객체는 함수로 전달되거나 값으로 함수에서 반환 될 수 없습니다. 이 트릭은 가벼워 보이지만 실제로 복사하는 데 비용이 많이 드는 유형에 사용할 수 있습니다 (하지만 유사 복사 가능하게 만드는 것이 더 나은 선택 일 수 있음).

C ++ 03에서는 복사 생성자를 선언 할 때도 정의해야했습니다 (물론 사용하려는 경우). 따라서 이러한 복사 생성자를 사용하는 것은 논의중인 문제에서 컴파일러가 자동으로 생성하는 것과 동일한 코드를 작성해야한다는 것을 의미했습니다.

C ++ 11 및 최신 표준에서는 기본 구현을 사용하기위한 명시 적 요청으로 특수 멤버 함수 (기본 및 복사 생성자, 복사 할당 연산자 및 소멸자)를 선언 할 수 있습니다 (선언을으로 종료 =default).



할 일

이 답변은 다음과 같이 개선 될 수 있습니다.

  • 더 많은 예제 코드 추가
  • "내부 상호 참조가있는 개체"사례 설명
  • 링크 추가

6

콘텐츠가 동적으로 할당 된 클래스가있는 경우. 예를 들어 책 제목을 char *로 저장하고 제목을 new로 설정하면 복사가 작동하지 않습니다.

title = new char[length+1]그런 다음 복사 생성자를 작성해야합니다 strcpy(title, titleIn). 복사 생성자는 "얕은"복사를 수행합니다.


2

Copy Constructor는 객체가 값으로 전달되거나 값으로 반환되거나 명시 적으로 복사 될 때 호출됩니다. 복사 생성자가 없으면 C ++는 얕은 복사를 만드는 기본 복사 생성자를 만듭니다. 개체에 동적으로 할당 된 메모리에 대한 포인터가 없으면 얕은 복사가 수행됩니다.


0

클래스가 특별히 필요로하지 않는 한 copy ctor 및 operator =를 비활성화하는 것이 좋습니다. 이것은 참조가 의도 될 때 값으로 인수를 전달하는 것과 같은 비 효율성을 방지 할 수 있습니다. 또한 컴파일러 생성 메서드가 유효하지 않을 수 있습니다.


-1

아래 코드 스 니펫을 고려해 보겠습니다.

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();데이터를 명시 적으로 복사하기 위해 작성된 코드없이 생성 된 사용자 정의 복사 생성자가 있기 때문에 정크 출력을 제공합니다. 따라서 컴파일러는 동일하게 생성하지 않습니다.

여러분 대부분이 이미 알고 있지만이 지식을 여러분 모두와 공유하는 것을 생각해보십시오.

건배 ... 즐거운 코딩 !!!

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.