I'm FanJae.

[C++] Copy Constructor 본문

C++/Basic

[C++] Copy Constructor

FanJae 2024. 8. 19. 22:36

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

1. Copy Constructor

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

class Point
{
	int x;
	int y;
public:
	Point()             : x{0}, y{0} {} // 1
	Point(int a, int b) : x{a}, y{b} {} // 2
};

int main()
{
	Point p1;		// ok.
	Point p2(1,2);	// ok
//	Point p3(1);	// error. Point(int) 필요
	Point p4(p2); 	// ok.    Point(Point)
}

- Point p4(p2);와 같은 생성자는 만든적이 없음에도 이상없이 잘 작동한다.

 

복사 생성자를 만들어주지 않으면, 어떻게 생성해주는가?

- 컴파일러가 제공한다.

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

- 모든 멤버를 복사(bitwise copy)를 진행한다.

 

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

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

→ Point p2(p1);

→ Point p2{p1};

→ Point p2 = p1;

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; // 복사 초기화
}

② 함수 인자를 call by value로 받을 경우

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;
    }
};
// 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로 사용하면 복사본을 만들지 않는다. 따라서 복사 생성자가 호출되지 않는다.

 

③ 함수 인자를 call by value로 받을 경우

Point p; // 생성자

Point foo() // 값 타입 반환
{
    return p;
}

int main()
{
   foo(p1);
}

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

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

- 단, 지역변수는 절대 참조로 반환하면 안된다.

 


2. Default Copy Constructor의 문제점 (Shallow Copy)

#include <iostream>
#include <cstring

class Person
{
   char* name;
   int age;
public:
   Person(const char* n, int a) : age(a)
   {
       name = char[strlen(n) + 1];
       strcpy(name, n);
   }
   ~Person() { delete[] name; }
}
int main()
{
    Person p1("kim",20);
    Person p2 = p1; // 실행시 오류 발생
}

- 일반적으로 객체가 자신의 동일한 타입의 객체로 초기화 될 때 이와 같은 작업이 이루어진다.

- 복사 생성자가 사용되고, 사용자가 만들지 않은 경우 디폴트 복사 생성자가 사용된다.

- 디폴트 복사 생성자는 모든 멤버를 복사한다.

 

문제는 디폴트 복사 생성자가 모든 멤버를 복사해주기 때문에 발생하는 문제가 있다.

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

- 이와 같은 상황일때, p2 = p1; 과 같은 일이 일어났다면, 아래와 같이 복사된다.

p2에 name이 p1의 name과 동일한 것을 가리키고 있다.

- p1에 있는 "kim"이라는 문자에 대해서 새롭게 동적 할당 해주는 것이 아닌 기존 p1이 바라보던, kim을 그대로 복사한다.

- 이와 같이, 클래스 안에 포인터 멤버가 있을때 디폴트 복사 생성자가 메모리 자체를 복사하지 않고, 주소만 복사하는 현상을 얕은 복사라고 한다.

- 이 경우, 개발자가 직접 복사 생성자를 만들어야 한다.


3. Default Copy Constructor를 해결할 방법

 

3-1. 깊은 복사( Deep Copy)

기존 Default Copy Constructor를 해결 할 방법으로, 클래스 안에 포인트 멤버가 있을때, 메모리 주소를 복사 하지 말고 메모리 자체의 복사본을 만드는 기술을 이용해야한다.이를 깊은 복사(Deep Copy)라고 한다.

- 즉, 아래 그림과 같이 p1,p2가 별도의 포인터 멤버에 대해서 별도의 공간을 할당 하도록 만들어야 한다.

 

p2의 name이 p1의 name과 별개의 공간을 할당해서 포인트 하고 있음을 확인할 수 있다. 이처럼 해야한다.

Person(Const Point& p)
{
    age = p.age;
    // name = p.name; // 이렇게 하지말고.
    
    // 포인터는 복사 하지말고 새롭게 메모리를 할당
    name = new char[strlen(p.name) + 1];
    strcpy(name, p.name);
}

 

3-1-1.깊은 복사 ( Deep Copy ) 의 단점

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

- 자원이 작으면 상관이 없지만, 자원이 크면 상당히 낭비된다.

- 이러한 단점을 해결하는 것이 바로, 참조 계수(Reference Counting)이다.

 

3-2. 참조 계수 (Reference Counting)

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

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

 

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);
        //*ref = 1;
    }
    ~Person() 
    { 
       // ref의 참조계수가 0이면 더이상 생성된 객체가 없음을 의미
       if(--(*ref) == 0) 
       {
           delete[] name;
           delete ref;
       }
    
    Person(const Person &p) : name(p.name), age(p.age), ref(p.ref)
    {
        ++(*ref);
    }
}

- 이와 같이 작성을 해주더라도 문제가 존재한다.

- p1 객체가 만약 '자신의 이름을 변경'한 상황이라고 생각해보자.

- p2의 이름은 변경되면 안되므로 공유했던 자원은 분리된다.

- 이 문제의 경우, 멀티 스레드 환경에서는 동기화의 오버헤드가 추가된다.

 

3-3. 복사 금지

Person(const Person&) = delete;

- 객체를 복사하지 못하게 하자는 의도이다. 복사 생성자를 delete 한다.

- 이 경우, 컴파일러가 복사 생성자를 만들지 않는다

 

3-4. STL의 string 클래스 사용

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

- 이 경우, 동적 메모리 할당을 할 필요가 없다.

- string이 내부적으로 자원을 알아서 관리해주고, int 변수 처럼 사용이 가능하다.

 

※ move라는 기능이 있기는 하나, 중급 단계 정도에서 별도 포스트로 다뤄보고자 한다.

'C++ > Basic' 카테고리의 다른 글

[C++] Upcasting, Virtual Function  (0) 2024.08.21
[C++] Inheritance(상속)  (0) 2024.08.20
[C++] this, Reference return  (0) 2024.08.18
[c++] const member function, mutable  (0) 2024.08.17
[C++] Static Member  (0) 2024.08.16
Comments