C++ 복사 생성자(Copy Constructor)

sjongyuuu·2021년 8월 19일
1

C++

목록 보기
6/6
post-thumbnail

지난 생성자와 초기화 리스트편 포스팅에 이어 이번 포스팅에서는 복사 생성자(Copy Constructor)에 대해 다루어 보려고 한다.

🧐바로 들어가 보자.

복사 생성자의 의미는 다음과 같다.

  • 복사 생성자(Copy constructor)
    한 객체의 내용을 다른 객체로 복사하여 생성된 생성자

의미만 놓고 보면 간단하다.😁
그냥 일반적으로 생성된 생성자와 같은데 그 내용이 다른 객체를 복사하여 생성된 것으로 이해하면 된다.

다음으로 복사 생성자의 특징을 보자.

  • 자신과 같은 타입의 객체를 인자로 받는다.
  • 복사 생성자가 정의되어 있지 않다면, 디폴트 복사 생성자(Default Copy Constructor)가 생성된다.

여기서, 눈여겨봐야 할 것은 🔎 디폴트 복사 생성자가 존재한다는 것이다.

이는 일반 생성자에서 디폴트 생성자의 특징과 똑같다고 보면 되는데 그 특징은 다음과 같다.

👉 클래스 내에 복사 생성자를 정의하지 않더라도 객체 생성 시 컴파일러가 자동으로 복사 생성자를 생성한다.

다음으로 복사 생성자의 가장 기본적인 코드 형식을 보자.

class Test {

public:
    Test(const Test& obj) {  // copy constructor
        ...
    }
}

여기서 명심해야 할 것은
✋ 앞으로 복사 생성자를 정의해야 할 상황이 생긴다면,
매개 변수를 선언할 때 항상 위와 같은 형식으로 구현해야 하는 것이다.

매개변수를 보면,

const Test& obj

위와 같이,

✅ 반환형을 레퍼런스 타입으로 선언하였다.

그 이유는 다음과 같다.

👉 객체의 경우, 일반적으로 차지하는 메모리가 크기 때문에
매개 변수가 다음과 같이 선언되어 있다면,

Test(const Test obj)

복사할 객체를 매개 변수(obj)에 전부 복사하고 다시 매개 변수를 새로운 객체에 복사하는 작업을 수행하게 되기 때문에 이는 상당히 비효율적이다. 따라서, 반환형을 레퍼런스 타입으로 정의함으로써 기존 객체를 매개 변수(obj)에 복사하는 과정 없이 이를 직접 이용하여 새로운 객체에 복사하는 것이다.

✅ 다음으로, 매개변수를 const 타입으로 선언한 이유를 보자.

먼저, const 타입으로 선언된 변수는 이후에 객체의 값을 변경할 수 없고 읽기만 가능하다.
즉, 함수의 매개변수를 const 타입으로 선언한다는 것은 그 매개변수가 함수 내에서 값이 변경되지 않고 읽기만 가능하도록 한 것을 의미한다.

👉 이를 복사 생성자에 대입해보면,
복사 생성자의 인자로 들어오는 "복사할 객체"생성자 내에서 값을 변경할 일이 전혀 없다. 따라서, 이를 const 타입으로 선언함으로써 생성자 내에서 읽기의 기능만을 수행하도록 하는 것이다.

사실, const 타입으로 선언하는 작업은 하지 않아도 프로그램을 실행하는데 문제는 없다.🤣

✋ 하지만,

const로 선언함으로써 코드의 역할을 분명히 할 수 있기 때문에 비단 복사 생성자만이 아닌 다른 기능을 구현할 때에도 위와 같은 상황이 있을 때, 코드의 역할을 분명히 하도록 작성하는 것이 좋은 코드를 작성하는 방법이지 않을까 싶다.

다음으로 코드를 보면서 복사 생성자가 어떻게 호출되는지 보자.

class Test {
    int n;
public:
    Test(int x = 10): n(x) {  // Default Constructor
        ...
    }
    Test(const Test& obj) {  // Copy Constructor
        n = obj.n;
        ...
    }
};

int main() {
    Test t1(300);  // Default Constructor 호출
    Test t2(t1);  // Copy Constructor 호출
}

위 코드는 복사 생성자가 정의된 경우를 나타낸 것이다.

앞서, 설명한 바와 같이 복사 생성자가 정의되어 있지 않더라도
디폴트 복사 생성자에 의해 객체 t2는 객체 t1과 같은 내용을 포함하게 된다.

다음으로 다른 형태로 복사 생성자를 호출하는 코드를 보자.

int main() {
    Test t1(300);  // Default Constructor 호출
    Test t2 = t1;  // Copy Constructor 호출
}

첫 번째 코드에서 main 함수가 위와 같은 경우
객체 t2에 객체 t1을 대입하는 형태로 이 또한, 복사 생성자를 호출하는 방법으로 t2(t1)와 같다고 보면 된다.

마지막으로

💡 디폴트 복사 생성자를 사용할 때 주의할 점

을 알아보자.

디폴트 복사 생성자는 "얕은 복사(Shallow copy)"를 수행한다.
즉, 디폴트 복사 생성자에 의해 초기화된 객체는 복사한 객체가 가진 reference와 value 모두 똑같이 가지게 되는 것이다.

여기서, value를 복사할 때에는 문제가 되지 않지만 reference를 복사할 때, 특히 동적 할당된 변수를 복사할 때 문제가 발생한다.

✍ 결론부터 말하자면!!
필드에 동적 할당을 받아 초기화되는 변수가 있다면
디폴트 복사 생성자를 이용하지 않고 사용자가 복사 생성자를 직접 정의하여 깊은 복사(Deep copy)가 일어나도록 해야 한다!

코드를 보면서 알아보자.

#include <iostream>
using namespace std;

class Test {
    int n;
    int *m;
public:
    Test(int x, int *y) {  // Default Constructor
        n = x;
        m = new int[5];
        copy(y, y+5, m);
    }
    int getN() { return n; }
    void printM() { 
        for (int i = 0; i < 5; i++) cout << m[i] << " ";
        cout << endl;
    }
    void setM(int idx, int t) { 
        m[idx] = t;
    }
};

int main() {
    int a = 150;
    int arr[5] = {0, 1, 2, 3, 4};
    Test t1(300, arr);  // Default Constructor 호출
    Test t2(t1);  // Default Copy Constructor 호출
    
    cout << "<Compare value of n>" << endl;
    cout << t1.getN() << endl;
    cout << t2.getN() << endl;

    cout << "<Compare value of m>" << endl;
    t1.printM();
    t2.printM();

    t2.setM(2, 80);  // t2에서 배열 m의 2번 index의 value 변경

    cout << "<After the change of m's value, Compare value of m>" << endl;
    t1.printM();
    t2.printM();
}

위 코드는 디폴트 복사 생성자를 통해 새로운 객체를 정의하는 것으로 코드가 길지만, 객체 t1과 t2가 출력하는 결과값을 중심으로 보면 된다.

먼저, 필드에 선언된 변수 n은 int형으로 선언된 변수로 복사가 일어날 때, n에 저장된 value(t1에서 n의 value)를 복사하게 된다.

다음으로, 변수 m은 int형 포인터로 선언되어 생성자에서 동적 할당을 받아 초기화된다.
이를 디폴트 복사 생성자를 통해 복사를 하게 되면 변수 m 즉, 1차원 배열 m이 가진 값들을 복사하는 것이 아니라 "배열 m이 저장된 주소"를 복사(얕은 복사)하게 된다.

위 코드의 출력을 보면 바로 알 수 있다.

main() 함수를 보면 t2에서 배열 m의 2번 index의 값만 80으로 변경했음에도 t1의 값 또한 같이 변경된 것을 볼 수 있다.

따라서, 복사 생성자를 직접 정의하여 깊은 복사가 일어나도록 하여 문제를 해결할 수 있다.

코드는 다음과 같다.

#include <iostream>
using namespace std;

class Test {
    int n;
    int *m;
public:
    Test(int x, int *y) {  // Default Constructor
        n = x;
        m = new int[5];
        copy(y, y+5, m);
    } 
    Test(const Test& obj) {  // Copy Constructor
        n = obj.n;
        m = new int[5];  // 깊은 복사 
        copy(obj.m, obj.m + 5, m);
    }
    int getN() { return n; }
    void printM() { 
        for (int i = 0; i < 5; i++) cout << m[i] << " ";
        cout << '\n';
    }
    void setM(int idx, int t) { 
        m[idx] = t;
    }
};

int main() {
    int a = 150;
    int arr[5] = {0, 1, 2, 3, 4};
    Test t1(300, arr);  // Default Constructor 호출
    Test t2(t1);  // Copy Constructor 호출
    
    cout << "<Compare value of n>" << endl;
    cout << t1.getN() << endl;
    cout << t2.getN() << endl;

    cout << "<Compare value of m>" << endl;
    t1.printM();
    t2.printM();

    t2.setM(2, 80);

    cout << "<After the change of m's value, Compare value of m>" << endl;
    t1.printM();
    t2.printM();
}

위 코드에서 정의된 복사 생성자를 보면, m에 대해 다시 한번 동적 할당을 하여 메모리의 새로운 공간을 할당받아 그 공간에 복사한 value들을 저장하도록 하였다.

위 코드의 출력은 다음과 같다.

.

.

.

C++ 복사 생성자 [끝]

  • 읽어 주셔서 정말 감사드립니다.
  • 오타나 잘못된 정보를 댓글로 남겨주시면 정말 감사하겠습니다.
profile
내 생각 저장소

2개의 댓글

comment-user-thumbnail
2023년 1월 3일

참고 잘 하고 갑니다!

답글 달기
comment-user-thumbnail
2023년 12월 23일

복사 생성자의 매개변수를 보면, 부분에서 반환형이아니고 인자 타입을 레퍼런스로 해주는거 아닌가용??

답글 달기