본문 바로가기
  • 쓸쓸한 개발자의 공부방
C++

[C++] 복사 생성자 (copy constructor), 얕은 복사 (Shallow Copy), 깊은 복사 (deep copy)

by 심찬 2021. 8. 21.

 

 

눈에 잘 드러나지 않는 자동 생성되는 코드를 이해하는 부분이 항상 까다롭다.

그 중 하나가 복사 생성자다.

 

대부분 자동으로 생성되는 복사 생성자를 사용하게 되고 큰 문제는 없다.

(좀더 인텔리전트한 개발자가 되기 위해서)

몇몇 부분에서 성능 향상에 도움이 되는 복사 생성자 정의하는 포인트가 있다. 알아두면 정말 좋은 내용이다.

 

copy constructor ( 복사 생성자 )

 

- 자신과 동일한 타입 한 개를 인자로 가지는 생성자

- 사용자가 복사 생성자를 만들지 않으면 컴파일러가 자동으로 제공한다.

- 디폴트 복사 생성자다 (default copy constructor)

- 모든 멤버를 복사 한다.

 

#include <iostream>

class Point
{
public:
    int x;
    int y;

    Point() : x(0), y(0)
    {
        std::cout << "ctor 1" << std::endl;
    }
    Point(int a, int b) : x(a), y(b)
    {
        std::cout << "ctor 2" << std::endl;
    }
};

int main()
{
    Point p1;
    Point p2(1,2);   // ok
    Point p3(1);     // Point ( int ) error 1개 인자 생성자가 없다.
    Point p4(p2);    // Point ( Point ) - ok
    
    Point p5{p2};   
    Point p6{p2};    
    Point p7 = {p2};    
    Point p8 = p2;    
}

Point p3(1); 의 경우 아래와 같이 인자 한개짜리 생성자가 없다고 에러를 보여준다.

main.cpp:23:15: error: no matching function for call to ‘Point::Point(int)’

 

Point p4(p2); 가 에러가 나지 않는 이유는 컴파일러가 자동으로 복사 생성자를 만들기 때문이다.

아래 코드가 컴파일러가 자동으로 생성하고 있어 에러가 발생하지 않는다.

Point (const Point& p)
          : x(p.x), y(p.y)
{
}

 

 

사용자가 만드는 복사 생성자

컴파일러가 자동으로 생성하는 복사 생성자를 사용자가 직접 만들 수 있다.

#include <iostream>

class Point
{
public:
    int x;
    int y;
    
    Point(){}
    Point(int a, int b) : x(a), y(b) 
    {
        std::cout << "ctor" << std::endl;
    }
    
    //사용자가 만드는 복사 생성자
    Point( const Point& p) : x(p.x), y(p.y)
    {
        std::cout << "copy ctor" << std::endl;
    }
};

int main()
{
    Point p1;
    Point p2(1,2);   // ok
    //Point p3(1);     // Point ( int ) error 1개 인자 생성자가 없다.
    Point p4(p2);    // Point ( Point ) - ok
    
    Point p5{p2};   
    Point p6{p2};    
    Point p7 = {p2};    
    Point p8 = p2;    
}

 

 

 


 

복사 생성자가 호출되는 3가지 경우

 

1) 자신과 동일한 타입의 객체로 초기화 될 때

    Point p2{p1};    // 복사 생성자
    Point p4 = {p1};    // 복사 초기화
    Point p5 = p1;      // 복사 초기화  : explicit가 아닌 경우만

 

 

아래 복사 생성자 Point (const Point& p) : x(p.x), y(p.y) 의 경우 잘 수행 된다.

#include <iostream>

class Point
{
public:
    int x;
    int y;
    
    Point(int a, int b) : x(a), y(b) 
    {
        std::cout << "ctor" << std::endl;
    }    
    Point( const Point& p) : x(p.x), y(p.y)
    {
        std::cout << "copy ctor" << std::endl;
    }
};

int main()
{
    Point p1(1,2);   // 생성자
    Point p2{p1};    // 복사 생성자
    Point p3{p1};    // 직접 초기화
    Point p4 = {p1};    // 복사 초기화
    Point p5 = p1;      // 복사 초기화
}

 

위의 코드와 모두 동일하나, 복사 생성자에 explicit 를 넣어준 차이만 있다.

#include <iostream>

class Point
{
public:
    int x;
    int y;
    
    Point(int a, int b) : x(a), y(b) 
    {
        std::cout << "ctor" << std::endl;
    }    
    explicit Point( const Point& p) : x(p.x), y(p.y)      // explicit 복사 생성자
    {
        std::cout << "copy ctor" << std::endl;
    }
};

int main()
{
    Point p1(1,2);   // 생성자
    Point p2{p1};    // 복사 생성자
    Point p3{p1};    // 직접 초기화
    Point p4 = {p1};    // 복사 초기화
    Point p5 = p1;      // 복사 초기화
}

main.cpp:24:19: error: no matching function for call to ‘Point::Point(Point&)’

main.cpp:25:16: error: no matching function for call to ‘Point::Point(Point&)’

 

2) 자신과 동일한 타입의 객체로 초기화 될 때

 

함수 인자를 call by value로 받을 때 복사 생성자가 호출된다.

함수 인자를 const reference로 사용하면 복사본을 만들지 않으므로 복사 생성자가 호출되지 않는다.

#include <iostream>

class Point
{
public:
    int x;
    int y;

    Point(int a = 0, int b = 0) : x(a), y(b) 
    {
        std::cout << "ctor" << std::endl;
    }
    Point( const Point& p)      : x(p.y), y(p.x)
    {
        std::cout << "copy ctor" << std::endl;
    }
};

// call by value함수 : 메모리 증가, 복사 생성자 불리고, 파괴될 때 소멸자 불린다. 함수 호출 2번됨.
//void foo( Point pt) // Point pt = p1

// 복사 생성자, 소멸자 안 불리므로 성능 향상에 좋다.!!!
void foo( const Point& pt) // const Point& pt = p1
{
}

int main()
{
    Point p1(1,2);
    foo(p1);
}

함수 인자를 const reference로 사용하면 복사 생성자가 호출되지 않는다.

 

3) 자신과 동일한 타입의 객체로 초기화 될 때

 

 - 리턴용 임시 객체가 생성될 때 복사 생성자가 호출된다.

 - 참조로 반환 (Point&) 하면 리턴 용 임시객체가 생성되지 않는다.

#include <iostream>

class Point
{
public:
    int x;
    int y;

    Point(int a = 0, int b = 0) : x(a), y(b) 
    {
        std::cout << "ctor" << std::endl;
    }
    Point( const Point& p)      : x(p.y), y(p.x)
    {
        std::cout << "copy ctor" << std::endl;
    }
};


Point p; // 생성자

//Point foo()  // 값 타입 반환
//{
//    return p;   // 리턴용 임시 객체를 만들어서 반환한다.
//}

Point& foo()    // 임시 객체를 만들지 않고 참조로 반환!  (지역 변수는 참조 반환하면 안된다!)
{
    return p;    
}

int main()
{
    foo();
    
}

복사 생성자가 호출되지 않았음을 알 수 있다.


 

default copy constructor

 

편리하지만 문제가 되는 경우가 있다.

 

기본 적인 디폴트 복사 생성자 호출 케이스다.

#include <iostream>

class Point
{
public:
    int x;
    int y;
    
    Point(int a, int b) : x(a), y(b) { }    
};

int main()
{
    Point p1(1,2); // ok
    Point p2(p1);  // 디폴트 복사 생성자 호출 , 모든 멤버가 복사
    
    std::cout << p2.x << std::endl; // 1
    std::cout << p2.y << std::endl; // 2
}

 

 


 

디폴트 복사 생성자의 문제점 (defulat copy constructor의 문제)

 

별 문제 없어보이는 아래 코드는 p1에 메모리 자원이 할당되고 p2가 생성되면서 문제가 발생한다.

왜 문제가 발생하는지 아래 순서를 하나씩 따라가면서 확인해 본다.

#include <iostream>
#include <cstring>

class Person
{
    char* name;
    int   age;
public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];   // 자원 할당
        strcpy(name, n);   // name에 n을 복사 해 넣어줍니다.
    }
    ~Person() { delete[] name;}      // 자원 해제
};

int main()
{
    Person p1("kim", 20);
    Person p2 = p1; // 실행시 오류 발생 (컴파일러에 따라 다른 결과)

}

 

1) Person p1 이 만들어 지면 *name과 age 공간이 생성된다.

2) 생성자에 의해 해당 2 줄을 실행하면 아래와 같은 구조를 갖는다.

   name = new char[strlen(n) +1]; 

   strcpy(name, n);

3) 해당 코드가 실행되면 복사 생성자가 호출된다. 컴파일러가 만들어주는 디폴트 복사 생성자가 수행된다.

  Person p2 = p1;

  * 디폴트 복사 생성자는 모든 멤버를 복사해 준다. 

 

4) 모든 코드 수행이 끝나면, 소멸자가 호출된다.

   p2의 소멸자가 호출되면서 name이 delete[] 되면서 메모리 공간 "kim" 공간도 역시 삭제된다.

 

5) 그 다음 p1의 소멸자가 불릴 때,

  이미 char* name 의 공간이 p2 소멸자에 의해 이미 지워졌기 때문에 잘못된 메모리 공간을 지울 수 있다.

 

>> 이는 메모리를 통채로 복사하지 못하고 주소만 복사하고 있는데 이를 "얕은 복사"라고 부른다.

 

 

얕은 복사 (Shallow Copy)

클래스 안에 포인터 멤버가 있을 때 디폴트 복사 생성자가 메모리 자체를 복사하지 않고 주소만 복사하는 현상.

c++ 개발자라면 반드시 알아둬야 하는 내용이다.

 

해결책

개발자가 직접 복사 생성자를 만들어야 한다. 

어떻게 복사해야 하는가?

 


 

깊은 복사 (deep copy)

클래스 안에 포인터 멤버가 있을 때 메모리 주소를 복사하지 말고 메모리 자체의 복사본을 만드는 코딩 방법.

 

#include <iostream>
#include <cstring>

class Person
{
    char* name;
    int   age;
public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
    }
    ~Person() { delete[] name;}

    // ******** 사용자 정의 : 복사 생성자 *********
    Person(const Person& p) : age(p.age)   // 초기화 리스트를 통해 초기화!!!
    {
    //    age = p.age;   // -> 초기화 리스트로 대체
    //    name = p.name;  // -> 아래 코드로 대체
    
        // ******* 포인터는 복사 하지말고. 새롭게 메모리 할당 ********
        name = new char[strlen(p.name) + 1];
        strcpy(name, p.name);        
    }
};


int main()
{
    Person p1("kim", 20);
    Person p2 = p1; 

}

 

복사 생성자 인자로 받아온 const Person& 타입의 p에서 p.name으로 메모리 생성한다.

        name = new char[strlen(p.name) + 1];
        strcpy(name, p.name);        

 

 

깊은 복사(deep copy)의 단점

객체를 여러 번 복사하면 동일한 자원이 메모리에 여러 번 놓이게 된다.

자원의 크기가 큰 경우 메모리 낭비가 발생 한다.

깊은 복사의 단점을 해결하는 또 다른 방법

 

자원을 같이 쓰자.

2명이 쓰고 있다고 자원을 관라한다.

p2를 파괴할 때 자원 delete하지 않고 자신의 것만 해제한다.

 

참조 계수 (reference counting)

- 여러 객체가 하나의 자원을 공유한다.

- 몇 몇의 객체가 자원을 사용하는지 개수를 관리한다.

- c++뿐 아니라 c에서도 자주 사용하는 방법

 

참조 계수 (reference counting) 복사

참조 계수를 활용해 객체를 복사/관리하는 방법

 

#include <iostream>
#include <cstring>

class Person
{
    char* name;
    int   age;
    int*  ref;   // 참조 계수 관리 위한 포인터
public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy(name, n);
        
        ref = new int(1);   // 1로 초기화
        //*ref = 1;
    }
    
    ~Person()     {
        
        // ******* 참조 계수 기반인 경우의 소멸자. ******
        if ( --(*ref) == 0 )
        {
            delete[] name;
            delete ref;
        }
    }

    Person(const Person& p) : name(p.name), age(p.age), ref(p.ref) {
        ++(*ref);
    }    
};


int main()
{
    Person p1("kim", 20);
    Person p2 = p1; 

}

 

여기서 또 다른 문제?

 

 - p1 객체가 자신의 이름을 변경하면 어떻게 ?

 - p2 이름 변경이 되면 안되기 때문에 공유 했던 자원은 분리 되어야 한다.

 

 

복사 금지

사람은 언제나 고유한 한 사람만이 존재한다. 그러니 복사 안되도록 하자!

객체 복사 못하도록 만든다. 복사 생성자를 delete한다.

디폴트 복사 생성자를 삭제 했으니 복사 생성자 호출시 컴파일 에러 발생한다. (심플!)

Person(const Person&) = delete;

 

String을 사용

 

문자열이 필요하면 STL 의 string 클래스를 사용하자

동적 메모리 할당할 필요가 없다.

stirng 이 내부적으로 자원 관리 해준다.

int 변수처럼 사용하면 된다.

#include <iostream>
#include <cstring>
#include <string>

class Person
{
    std::string name;
    int   age;
public:
    Person(std::string n, int a) : name(n), age(a)
    {
    }
};

int main()
{
    Person p1("kim", 20);
    Person p2 = p1;

}

 

또 다른 방법? move 를 활용하는 방법! (다른 포스팅에서 다루기로...)

 

 

 

 

댓글