[C++] 클래스(4) 복사생성자

김형태·2021년 5월 8일
0

C++

목록 보기
6/13

1. 들어가기 전에

1.1. this 포인터

class ThisClass
{
private:
    int num;
public:
    void thisFunc(int num)
    {
        this->num = 207; //멤버변수 num에 207이 저장됨.
        num = 105;
    }
}

thisFunc 함수 내에서 num은 매개변수 num을 의미하게 되므로, num이라는 이름만으로느느 멤버변수에 접근을 못하는데, 이때 this 포인터를 활용하면 멤버변수에 접근할 수 있음.


1.2. Self-reference

객체 자신을 참조할 수 있는 참조자. this 포인터를 이용해서 객체가 자신의 참조에 사용할 수 있는 참조자의 반환무늘 구성할 수 있음!

#include <iostream>
using namespace std;

class SelfRef
{
private:
    int num;
public:
    SelfRef(int n): num(n)
    {
        cout << "객체 생성" << endl;
    }
    SelfRef&	adder(int n)
    {
        num+=n;
        return *this;
    }
    SelfRef&	showTwoNumber(void)
    {
        cout << num << endl;
        return *this;
    }
}

int main(void)
{
    SelfRef obj(3);
    SelfRef &ref = obj.Adder(2);
    
    obj
    
    obj.showTwoNumber();
    ref.showTwoNumber();
    
    ref.adder(1).showTwoNumber().adder(2).showTwoNumber();
    return (0);
}

객체 생성
5
5
6
8


2. 복사생성자

2.1. C++ 스타일의 초기화

int num = 20;
int &ref = num;

는 다음과 같다.

int num(20);
int &ref(num);

위 두 방식은 결과적으로 동일하다. C++에서는 위 두 가지 초기화 방식을 동시에 지원하고 있다.

이제 객체 생성에 대해 생각해보자.

...
class Simple
{
private:
    int num1;
    int num2;
public:
    Simple(int n1, int n2): num1(n1), num2(n2)
    {
    }
    void	showSimple(void)
    {
        cout << num1 << endl;
        cout << num2 << endl;
    }
}

위에 정의된 클래스로 다음을 실행해보자.

int main(void)
{
    Simple	sim1(15, 20);
    Simple	sim2 = sim1;
    sim2.showSimple();
    return (0);
}

Simple sim2 = sim1;는 객체의 생성 및 초기화를 연상시킨다.
즉, sim2 객체를 새로 생성해서, 객체 sim1과 sim2간의 멤버 대 멤버 복사가 일어난다고 예상해 볼 수 있다.
그런데 실제로 그러한 일이 일어난다.

다음 두 문장이 동일한 의미로 해석되듯이,

int num1 = num2;
int num1(num2);

다음 두 문장도 동일한 의미로 해석이 된다.

Simple sim2 = sim1;
Simple sim2(sim1);

이때, 과연 sim2는 어떠한 과정을 거쳐서 생성되는 것일까?
생성자 호출 관점에서 이를 생각해보자.

Simple sim2(sim1);
  • Simple형 객체를 생성해라.
  • 객체의 이름은 sim2로 정한다.
  • sim1을 인자로 받을 수 있는 생성자의 호출을 통해서 객채생성을 완료한다.

위 객체 생성문에서 호출하고자 하는 생성자는 다음과 같이 Simple 객체를 인자로 받을 수 있는 생성자이다.

Simple(Simple &ref)
{
    ...
}

그리고 다음의 문장도

Simple sim2 = sim1;

실은 Simple sim2(sim1);로 묵시적으로 변환이 되어서 객체가 생성되는 것이다.

그런데 앞에 정의된 Simple 클래스에서는 이러한 유형의 생성자가 정의되어 있지 않았다.

이 때 호출되는 생성자의 이름은 디폴트 복사 생성자이다.

이 생성자도 똑같이 디폴트 생성자를 오버로딩한 형태인데 왜 특별한 이름이 붙었을까?

그건 복사 생성자호출되는 시점이 다른 일반 생성자와 차이가 있기 때문이다. 이것은 조금 뒤에 알아보고, 지금은 왜 디폴트 복사 생성자가 있는데 굳이 복사 생성자를 정의해주는지 알아보자.

cf) 멤버 대 멤버의 복사에 사용되는 원본을 변경시키는 것은 복사의 개념을 무너뜨리는 행위가 되니, 키워드 const를 삽입해서 이러한 실수를 막아주자.

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

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

Class Simple
{
private:
    int num1;
    int num2;
public:
    Simple(int n1, int n2): num1(n1), num2(n2)
    {
    }
    ...
};

위 클래스와 같이 복사생성자가 정의되어 있지 않으면, 다음 클래스와 완전히 동일하다.

Class Simple
{
private:
    int num1;
    int num2;
public:
    Simple(int n1, int n2): num1(n1), num2(n2)
    {
    }
    Simple(cosnt Simple &ref): num1(ref.n1), num(ref.n2)
    {
    }
    ...
};

또한

Simple sim2 = sim1;

는 자동으로

Simple sim2(sim2);

으로 묵시적으로 변환되어, 복사생성자가 호출된다. 이러한 묵시적 변환이 맘에 들지 않는다면 explicit 키워드를 쓰면 된다.

바로 위의 클래스에서 복사생성자 앞에 explicit을 붙이면

explicit Simple(const Simple &ref): num1(ref.n1), num2(ref.n2)
{
}

대입 연산자를 이용한 객체의 생성 및 초기화는 불가능하다.

이제 디폴트 복사 생성자를 사용할 때 생길 수 있는 문제에 대해 알아보자.

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
private:
	char *name;
    int age;
public:
    Person(const char *name, int age)
    {
        int len = strlen(name)+1;
        this->name = new char[len];
        strcpy(this->name, name);
        this->age = age;
    }
    void	showPersonInfo() const
    {
        cout << "이름 : " << this->name << endl;
        cout << "나이 : " << this->age << endl;
    }
    ~Person()
    {
        delete[] this->name;
        cout << "called destructor" << endl;
    }
};

int main(void)
{
    Person man1("hyeonkim", 30);
    Person man2 = man1;
    man1.showPersonInfo();
    man2.showPersonInfo();
    return (0);
}

위 코드에서 Person man2 = man1;문장을 통해 디폴트 복사생성자가 호출됐고, 이때 멤버 대 멤버의 단순 복사(얕은 복사)가 진행되었다. 즉, 하나의 문자열을 두 객체가 동시에 참조하는 꼴이 된다. 그러므로 객체의 소멸 과정에서 문제가 발생한다.

그러므로 깊은 복사를 원한다면 복사 생성자를 새로 정의하자.

Person(const Person &ref): age(ref.age)
{
    this->name = new char[strlen(ref.name) + 1];
    strcpy(this->name, ref.name);
}

2.3. 복사 생성자의 호출 시점

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

(추가 정리 예정)

profile
steady

0개의 댓글