4 분 소요

복사 생성자

SoSimple sim2 = sim1
또는
SoSimple sim2(sim1)

을 통해서 객체 복사가 일어나는데, 일어나는 과정은 sim2 객체가 생성된 다음에 sim1 에 생성된 멤버 변수를 멤버 대 멤버로 복사가 일어난다.

#include <iostream>

class SoSimple
{
private:
    int num1;
    int num2;

public:
    SoSimple(int n1, int n2) : num1(n1), num2(n2)
    {
    }
    SoSimple(const SoSimple &copy) : num1(copy.num1), num2(copy.num2)
    {
        std::cout << "Called SoSimple(SoSimple &copy)" << std::endl;
    }
    void ShowSimpleData()
    {
        std::cout << num1 << std::endl;
        std::cout << num2 << std::endl;
    }
};

int main()
{
    SoSimple sim1(15, 30);
    std::cout << "생성 및 초기화 직전" << std::endl;
    SoSimple sim2 = sim1; // SoSimple sim2(sim1);
    std::cout << "생성 및 초기화 직후" << std::endl;
    sim2.ShowSimpleData();
    return 0;
}

>>>
생성  초기화 직전
Called SoSimple(SoSimple &copy)
생성  초기화 직후
15
30

두 번째 생성자와 같은 경우를 복사 생성자 라고 한다. 복사 생성자의 경우는 다른 일반 생성자와 차이가 있기 때문에 복사 생성자라는 이름이 붙은 것이다.

또한 복사하는 도중에 값이 바뀌면 안되므로 const 를 붙혀서 복사 생성자를 이용한다.

자동으로 삽입이 되는 디폴트 복사 생성자

복사 생성자를 정의하지 않으면, 멤버 대 멤버의 복사를 진행하는 디폴트 복사 생성자가 자동으로 삽입된다.

변환에 의한 초기화! 키워드 explicit으로 막을 수 있다.

디폴트 복사 생성자를 통해서 멤버 변수가 초기화 되는 것을 막기 위해서는 explicit 키워드를 사용할 수 있다.

깊은 복사와 얕은 복사

#include <iostream>
#include <cstring>

class Person
{
private:
    char *name;
    int age;

public:
    Person(char *myname, int myage)
    {
        int len = strlen(myname) + 1;
        name = new char[len];
        strcpy(name, myname);
        age = myage;
    }
    void ShowPersonInfo() const
    {
        std::cout << "이름: " << name << std::endl;
        std::cout << "나이: " << age << std::endl;
    }
    ~Person()
    {
        delete[] name;
        std::cout << "called destructor!" << std::endl;
    }
};

int main()
{
    Person woman1("Sohn soo kyoung", 22);
    Person woman2 = woman1;
    woman1.ShowPersonInfo();
    woman2.ShowPersonInfo();
    return 0;
}

>>>
이름: Sohn soo kyoung
나이: 22
이름: Sohn soo kyoung
나이: 22
called destructor!
called destructor!

나는 이렇게 출력결과가 나왔는데, 사실 called destructor 가 한번만 나오는 것이 일반적이라고 한다. 근데 왜 한 번만 나오는 것일까?

위 복사 생성자는 디폴트 복사 생성자를 사용했다. 디폴트 복사 생성자는 객체를 생성하고 그 객체의 멤버 변수를 멤버 대 멤버로 복사하는 구조를 띠게 된다. 그렇게 된다면 하나의 문자열 주소를 두개의 객체가 참조하는 꼴이 되기 때문에 한 번만 called destructor 가 나오는 것이 일반적이라고 한 것이다. 즉, 문자열 주소만 복사가 된 것이기 때문에 결국 하나의 문자열을 참조하는 꼴이 되버린다.

복사 생성자를 사용해서 다른 문자열을 참고하도록 복사하는 것을 깊은 복사 라고 한다.

깊은 복사를 위한 복사 생성자 정의

깊은 복사는 위에서 말했던 것 처럼 문자열까지 복사를 해서 복사된 객체는 다른 주소로 만들어진 문자열을 가리키도록 하는 것이다.

#include <iostream>
#include <cstring>

class Person
{
private:
    char *name;
    int age;

public:
    Person(char *myname, int myage)
    {
        int len = strlen(myname) + 1;
        name = new char[len];
        strcpy(name, myname);
        age = myage;
    }
    Person(const Person &copy) : age(copy.age)
    {
        name = new char[strlen(copy.name) + 1];
        strcpy(name, copy.name);
    }
    void ShowPersonInfo() const
    {
        std::cout << "이름: " << name << std::endl;
        std::cout << "나이: " << age << std::endl;
    }
    ~Person()
    {
        delete[] name;
        std::cout << "called destructor!" << std::endl;
    }
};

int main()
{
    Person woman1("Sohn soo kyoung", 22);
    Person woman2 = woman1;
    woman1.ShowPersonInfo();
    woman2.ShowPersonInfo();
    return 0;
}

>>>
이름: Sohn soo kyoung
나이: 22
이름: Sohn soo kyoung
나이: 22
called destructor!
called destructor!

복사 생성자의 호출 시점

  1. 기존에 생성된 객체를 이용해서 새로운 객체를 초기화하는 경우
  2. Call-by-Value 방식의 함수 호출 과정에서 객체를 인자로 전달하는 경우
  3. 객체를 반환하되, 참조형으로 반환하지 않는 경우

메모리 공간의 할당과 초기화가 동시에 일어나는 상황

  1. int num1 = num2
  2. 함수가 호출되는 순간 매개변수의 값이 전달되는 경우
  3. return 을 통해서 함수의 리턴 값을 저장하는 경우

할당 이후, 복사 생성자를 통한 초기화

할당과 초기화가 동시에 이루어지는 경우는 멤버 대 멤버가 복사되도록 하면 된다.

#include <iostream>

class SoSimple
{
private:
    int num;

public:
    SoSimple(int n) : num(n)
    {
    }
    SoSimple(const SoSimple &copy) : num(copy.num)
    {
        std::cout << "Called SoSimple(const SoSimple &copy)" << std::endl;
    }
    void ShowData()
    {
        std::cout << "num: " << num << std::endl;
    }
};

void SimpleFuncObj(SoSimple ob)
{
    ob.ShowData();
}

int main()
{
    SoSimple obj(7);
    std::cout << "함수 호출 전" << std::endl;
    SimpleFuncObj(obj);
    std::cout << "함수 호출 후" << std::endl;
    return 0;
}

>>>
함수 호출 
Called SoSimple(const SoSimple &copy)
num: 7
함수 호출 

obj라는 매개변수가 ob 라는 인자로 들어갈 때 할당과 동시에 초기화가 이루어 지는 경우이므로 이는 디폴트 복사 생성자를 사용해서 복사 생성자를 만들 수 있다.



return 이 사용될 때 복사 생성자는 언제 사용이 될까?

#include <iostream>

class SoSimple
{
private:
    int num;

public:
    SoSimple(int n) : num(n)
    {
    }
    SoSimple(const SoSimple &copy) : num(copy.num)
    {
        std::cout << "Called SoSimple(const SoSimple &copy)" << std::endl;
    }
    SoSimple &AddNum(int n)
    {
        num += n;
        return *this;
    }
    void ShowData()
    {
        std::cout << "num: " << num << std::endl;
    }
};

SoSimple SimpleFuncObj(SoSimple ob)
{
    std::cout << "return 이전" << std::endl;
    return ob;
}

int main()
{
    SoSimple obj(7);
    SimpleFuncObj(obj).AddNum(30).ShowData();
    obj.ShowData();
    return 0;
}

>>>
Called SoSimple(const SoSimple &copy)  
return 이전
Called SoSimple(const SoSimple &copy)
num: 37
num: 7

obj -> ob(매개변수가 할당 및 초기화) -> Called SoSimple(const SoSimple &copy) -> return 이전 -> return을 통해서 임시 객체 만들어짐 -> Called SoSimple(const SoSimple &copy) -> AddNum을 통해서 임시 객체에 30이 더해짐 -> 임시 객채의 ShowData() -> num: 37 -> obj.ShowData() -> num: 7

반환할 때 만들어진 객체는 언제 사라져요?

  • 임시 객체는 다음 행으로 넘어가면 바로 소면되어 버린다.
  • 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.

댓글남기기