총 10분 중 11분
2001
시즌 2개, 그리고 영화
시즌 2: 5화 “아일랜드”
출연: 이나영, 김민준, 김민정, 현빈
장르: 애초에 역경을 딛고 이룩하는 숭고한 사랑이란 없다. 그 역경 자체가 사랑이다.
프로그램 특징: 그 곳에서 살아남는 사랑이 어떤 모습으로 걸어오는지 기다려 보고 싶다.
Ahranah를 시청할 프로필을 선택하세요.
아란정
게스트
프로필 추가
Programming 복사 생성자

복사생성자 copy constructor

생략 시 컴파일러가 자동 생성해주지만 클래스 내부에서 메모리 동적 할당 및 해제하고 이를 멤버 포인터로 관리하는 경우 직접 선언해야 한다.

이를 이해하기 위해선 Pass by value의 선행 이해가 필요하다.
다른 함수의 매개변수로 사용되는 경우 Call by value 로 전달된다. 호출 함수 스택에 따로 메모리를 할당해 객체의 복사된 값을 전달되는 형태인데 이를 복사 생성자라고 생각하면 된다.

Myclass(const Myclass &rhs)
// :m_data(rhs.m_data) 
{ 
     this -> m_data = rhs.m_data; 
     cout << "Myclass(const Myclass &)" << endl;
} 
    int main(){ 
        // 기본 생성자 
        Myclass a; 
        a.Setdata(10); 

        // 복사 생성자
        Myclass b(a); 
    }

a 를 복사의 원본으로 rhs가 a를 참조하는 형태 원본을 복사하는 형태기 때문에 멤버 m_data 두 개가 한꺼번에 나타난다.
이를 해결하기 위해 this -> m_data 를 표현해주거나 생성자 초기화 m_data(rhs.m_data)를 이용한다.

복사 생성자 호출

  1. 명시적 호출
    Myclass a;
  2. 함수 형태 호출
    Myclass b(a);
     1. 클래스가 매개변수로 사용
     2. 클래스가 반환형식으로 사용
           - 반환 형식으로 사용되는 경우 '이름 없는 임시 객체' 를 생성한다. 느껴지는 대로 골치 아픈 문제다.      
  3. 함수 형태 호출에서도 두 가지 사용 형식이 존재한다.

우리에게 더 익숙한 복사 생성자 호출 상황은 아래와 같다.

Myclass b;
b = a;

복사 생성자를 매개 변수로 사용

void TestFunc(Myclass param) {}

TestFunc param의 호출은 Myclass 인스턴스 원본 a를 두고 복사본을 생성한다. (함수 내부에서 쓸 용)
그렇게 되면 쓸데없이 클래스 객체가 두 개가 된다. 하나만 있어도 되는데 두 개로 처리한 것도 문제지만 함수 호출 자체가 성능 문제로 직결되기 때문에 수정이 필요하다. 복사 생성자를 삭제할까?

Myclass(const Myclass &rhs) = delete;

가능하지만.. 함수 호출 시 삭제된 함수를 참조하려 한다는 컴파일러의 오류 메세지를 만나게 된다.
*rhs: right hand side 의 약자로 그냥 많이 쓴단다. 뭔지 잘 모르겠다.

Call by reference

void TestFunc(Myclass &param){}

복사 생성자를 이용하지 않고도 원본을 argument로 전달해줄 수 있게 되었다.
사용자는 TestFunc(a); 로 더 높은 성능을 이용할 수 있다.

만약 void TestFunc(Myclass *param) 이었다면 사용자는 TestFunc(&a);를 파라미터로 주었어야 할 것이다.
동일한 성능을 나타내지만 포인터를 파라미터로 받을 시 댕글링 포인터(포인팅하는 쪽이 없어지는 경우)나 의존 관계 분석 불가로 인한 코드 최적화 실패 등의 단점이 있기 때문에 참조자&로 대체할 수 있는 포인터는 무조건 대체하는 것이 좋다.

사용자가 어떠한 값을 입력해야 하고, 그 값이 함수 내에서 변형될 수 있는 지를 알 수 있는 것은 프로그램 사용에 있어서 중요한 문제다.

void TestFunc(const Myclass &param){}

이었다고 치자.
사용자가 TestFunc 함수 내부에서 클래스 인스터스 param.setdata(10); 등의 명령어를 입력했을 때의 결과를 알 수 있을까?

사용자는 모른다.

const 참조라 원형값을 변경할 수 없음에도 뻘짓을 하고 있을지도 모른다.
근데 원형을 파라미터로 받는 경우에는 const를 꼭 사용하자. 잠깐 편하자고 ~~ 코드 뜯어볼 일을 만들지 말자.

그래서 이제 우리가 할 것은 무엇이냐면~
동적 생성한 인스턴스의 값을 관리하는 방법에 대해 알아볼 것이다.

깊은 복사와 얕은 복사

얕은 복사: 메모리 주소 가져오기
깊은 복사: 메모리 주소의 값 가져오기

int *ptr = new int;
*ptr = 10;

int *ptr2 = new int;
ptr2 = ptr;
// 지금 ptr2 는 ptr이 값으로 가진 new int (10)의 주소를 받았다. 

delete ptr;
delete ptr2;

무슨 문제가 일어날까? ptr2는 이미 delete ptr에서 자신이 가리키고 있는 메모리 주소의 집주인이 사라졌으므로 할당 해제된 상태다.
여기서 한 번 더 할당 해제를 시도하면 오류가 난다.
ptr -> 10
ptr2 ---^ [new int] (접근 불가)

*ptr2 = *ptr;

로 수정하면 ptr2 는 자신이 동적 할당한 int 메모리에 10을 넣어서 그걸 가리키고 있을 것이다.

ptr -> 10
ptr2 -> 10

포인터가 존재할 때 얕은 복사가 문제되는 상황

class Myclass
{
public:
    Myclass(int nParam)
    {
    m_dataPtr = new int;
    *m_dataPtr = nParam;
       }

    int getdata(){
        if (m_dataptr != nullptr){
            return *m_dataptr;
            }
        return 0;
    }
private:
    int *m_dataptr = nullptr;
 };

 int main(){
     Myclass a;
    Myclass b(a);

    cout << a.getdata() << endl;
    cout << b.getdata() << endl;

    return 0;
 }

문제는 어디서 나타난걸까

Myclass b(a); 를 수행하면 깊은 복사 수행이 아니기 때문에 컴파일러가 자동으로 Myclass b(a);에서

 Myclass(const Myclass &rhs){
     m_dataptr = rhs.m_dataptr;
 }

를 실행한다.

여기서 메모리 해제를 시도했다면

 // 객체 소멸 시점에 동적 할당 메모리 삭제
 ~Myclass() { delete m_dataptr; }

( 소멸자는 블록 {} 이 끝나면 실행된다 )

복사 생성자인 b는 원본 데이터의 m_dataptr;이 삭제된 상태에서 또 소멸자를 부르기 때문에 앞서서 봤던 해제된 메모리를 다시 해제하려고 시도한 오류가 나타난다.

그럼 또 어떡하라공

복사 생성자 정의하고 깊은 복사

Myclass(const Mydata &rhs)
{
    // 디버그용
    cout << "복사생성자 호출" << endl;

    // 메모리 할당
    m_dataptr = new int;
    *m_dataptr = *rhs.m_dataptr;
 }

인스턴스 복사 후 소멸자가 나와도 복사 생성자는 자신이 할당한 메모리를 가리키고 있기 때문에 더 이상 해제된 메모리를 해제하는 오류는 나타나지 않는다.

저자 왈, 이 부분 객체지향 배우면 시험 범위라니까 모두들 열심히 이해해봅시다.

추가로 ...
앞서서, 우리에게 익숙한 방식 b = a; 로 단순 대입(pass by value)한 결과에는 대입 연산자 동작 방식을 새로 정의해주어야 한다.

 Myclass& operator = (const Myclass &rhs)
 {
     *m_dataptr = *rhs.m_dataptr;

    // 객체 자신에 대한 참조 반환
    return *this;
 }

 // b = a; 대신 아래와 같이 이용할 수도 있다. 
 b.operator=(b);

연산자 = 의 작동 방식을 정의해준 코드로 .. 어렵다
a의 모든 값을 새로 정의한 복사 생성자 new로 넣기 위해 깊은 복사를 정의해준건가?
b = a; 면 a의 m_dataptr 값이 = 에서 *m_dataptr = *rhs.m_dataptr;로 복사되어 b의 m_dataptr이 된건가
그럼 *rhs.m_dataptr; 를 반환하면 안되나?

... 어물쩡 넘기기 ㅎ

전체 코드

class Myclass
{
public:
    Myclass(int nParam)
    {
        m_dataptr = new int;
        *m_dataptr = nParam;
    }

    Myclass(const Myclass &rhs)
    {
        cout << "Myclass const &rhs" << endl;
        m_dataptr = new int;
        *m_dataptr = *rhs.m_dataptr;
    }

   ~Myclass()
   {
       delete m_dataptr;
   }

   Myclass& operator=(const Myclass &rhs)
   {
       *m_dataptr = *rhs.m_dataptr;
    return *this;
   }

   int getdata()
   {
       if (m_dataptr != nullptr) 
     return *m_dataptr;

    return 0;
  }

  private:
      int *m_dataptr = nullptr;

  };

  int main()
  {
      Myclass a(10);
    Myclass b(20);

    a = b;
    cout << a.getdata() << endl;

    return 0;
 }

답은?

20

  • 이 글은 최호성 저자의 이것이 C++이다 를 참고하여 작성되었습니다.
Programming 복사 생성자