개발/C, C++

[C++] 복사 생성자(Copy Constructor) 의 이해 및 활용

growing-dev 2023. 7. 27. 22:53

오늘은 C++에서 복사 생성자의 개념에 대해서 알아보고 이해하는 시간을 가져보도록 하겠습니다.

 

 복사 생성자(Copy Constructor)의 정의, 이해 및 활용

 

C++에서 복사 생성자는 중요한 개념 중 하나로, 객체를 복사하거나 전달할 때 사용되는 특별한 유형의 생성자입니다. 

 

 

 복사 생성자의 정의

 

복사 생성자는 객체의 내용을 다른 객체에 복사하는데 사용되는 특별한 생성자입니다. C++에서는 객체를 전달하거나 할당할 때마다 해당 객체의 복사 생성자가 호출됩니다. 기본적으로 C++ 컴파일러는 클래스에 대해 복사 생성자를 자동으로 생성해 주지만, 사용자가 직접 정의하여 커스터마이징 할 수도 있습니다.

기본 복사 생성자
기본 복사 생성자는 단순히 객체의 모든 멤버 변수들을 다른 객체에 복사하는 역할을 합니다. 얕은 복사를 수행하는데, 동적으로 할당된 메모리 등은 주소만 복사되기 때문에 문제가 발생할 수 있습니다.

사용자 정의 복사 생성자
사용자가 직접 복사 생성자를 정의할 수 있으며, 깊은 복사 등의 특별한 동작을 구현할 수 있습니다. 예를 들어, 동적으로 할당된 메모리를 새로운 객체에 적절히 복사하는 등의 작업을 할 수 있습니다.

 

복사 생성자의 호출 시점


복사 생성자는 다음 상황에서 호출됩니다.
- 함수의 매개변수로 객체를 전달할 때
- 함수가 객체를 반환할 때
- 객체를 다른 객체에 대입할 때
- 동적으로 할당된 객체를 생성하거나 해제할 때

 

 

 복사 생성자의 오버헤드, 그리고 얕은 복사와 깊은 복사

 

복사 생성자의 오버헤드는 객체를 복사하는 과정에서 발생하는 추가적인 비용을 의미합니다. 이러한 오버헤드는 복사 생성자가 객체를 복사할 때마다 발생하며, 처리해야 할 데이터의 양과 복잡성에 따라 성능에 영향을 미칠 수 있습니다.

복사 생성자의 오버헤드를 이해하려면 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 개념을 이해해야 합니다.

1. 얕은 복사(Shallow Copy):
얕은 복사는 단순히 주소만 복사하는 방식을 의미합니다. 복사 생성자가 멤버 변수들을 단순히 똑같은 주소로 가리키게 되는 것을 말합니다. 이 경우, 두 객체가 같은 데이터를 공유하게 되므로, 하나의 객체가 수정되면 다른 객체에도 영향을 미치게 됩니다. 이로 인해 예상치 못한 문제가 발생할 수 있으며, 객체가 해제될 때 두 객체가 같은 메모리를 가리키고 있기 때문에 두 번 해제하려는 시도가 발생하여 프로그램이 오류를 발생시키는 경우도 있습니다.

2. 깊은 복사(Deep Copy):
깊은 복사는 객체의 모든 데이터를 새로운 메모리 공간에 복사하는 방식을 의미합니다. 복사 생성자가 멤버 변수들을 각각 독립적으로 새로운 메모리 공간에 복사하는 것을 말합니다. 이렇게 하면 두 객체가 완전히 독립적인 복사본을 가지게 되므로 하나의 객체를 수정해도 다른 객체에는 영향을 미치지 않습니다.

복사 생성자의 오버헤드는 주로 깊은 복사를 수행할 때 발생합니다. 객체의 크기와 데이터의 양이 클수록 복사하는 데에 걸리는 시간이 증가하므로 성능에 영향을 미칠 수 있습니다. 또한, 동적으로 할당된 메모리 등 복사 작업의 복잡성에 따라서도 오버헤드가 발생할 수 있습니다.

복사 생성자의 오버헤드를 줄이기 위해 다음과 같은 방법들을 고려할 수 있습니다.
- C++11 이후에 도입된 이동 생성자와 이동 할당 연산자를 사용하여 객체를 이동시키는 방법을 고려합니다.
- 복사 생성자와 대입 연산자의 동작을 최적화하여 효율적인 복사를 수행하도록 구현합니다.
- 불필요한 복사를 피하기 위해 참조(Reference)를 사용하여 객체를 전달하거나 반환하는 방식을 고려합니다.

복사 생성자의 오버헤드를 최소화하면 성능 향상에 도움이 됩니다. 그러나 복사 생성자의 적절한 동작을 보장하는 것이 중요하며, 얕은 복사로 인한 예상치 못한 문제가 발생하지 않도록 주의해야 합니다.

 

 

 예제 코드

 

아래 Person 클래스 예제를 통해 조금 더 공부해 보겠습니다.

여기서 Person(const char* name, int age) 이 바로 기본 생성자 이며 Person(const Person& other) 이 복사 생성자입니다. 인자로 Person& 을 받는 것을 알 수 있습니다. 내부에서는 new char를 통해 새로운 이름을 동적할당하고 age도 복사해 줍니다. 즉 깊은 복사가 이루어져서 독립된 객체가 생성되는 것을 알 수 있습니다.

 

#include <iostream>
#include <cstring>

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

public:
    // 생성자
    Person(const char* name, int age) {
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
        this->age = age;
    }

    // 복사 생성자 (깊은 복사)
    Person(const Person& other) {
        this->name = new char[strlen(other.name) + 1];
        strcpy(this->name, other.name);
        this->age = other.age;
    }

    // 소멸자
    ~Person() {
        delete[] name;
    }

    // 정보 출력 메서드
    void printInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

int main() {
    // 객체 생성
    Person person1("Alice", 30);

    // 복사 생성자 호출하여 새로운 객체 생성
    Person person2 = person1;

    // 두 객체의 정보 출력
    std::cout << "Person 1: ";
    person1.printInfo();

    std::cout << "Person 2: ";
    person2.printInfo();

    return 0;
}

 

이대로 빌드해서 실행해 보면 아래와 같이 두 개의 정보가 동일하게 출력됨을 알 수 있습니다.

person1과 person2 정보

 

테스트를 위해 복사 생성자를 제거합니다. 

    // 복사 생성자 (깊은 복사)
    //Person(const Person& other) {
        //this->name = new char[strlen(other.name) + 1];
        //strcpy(this->name, other.name);
        //this->age = other.age;
    //}

 

이렇게되면 복사 생성자를 깊은 복사로 정의하지 않았기 때문에 person2는 person1을 가리키기만 할 뿐입니다. 이때 에러가 발생하는데, new char를 새롭게 해주지 않아서 person1, person2 가 각각 소멸자가 호출되었을 때 delete [] name을 2번 해주어서 에러가 발생합니다. 즉 메모리 공간은 하나만 할당되어 있다는 것을 알 수 있습니다.

에러 발생

 

우리가 일반적으로 Person person2 = person1; 을 하겠다는 의도는 깊을 복사를 하겠다는 의도가 맞을 겁니다. 따라서 깊은 복사를 할 수 있는 복사 생성자를 정의하고 클래스 내부 멤버변수들을 생성하거나 초기화해줄 필요가 있습니다.

 

아래 코드에, setAge 라는 메서드를 추가하였습니다.

복사 이후 person2 age를 40으로 변경했을 때 person1과 person2의 상태가 각각이 됨을 확인할 수 있습니다.

#include <iostream>
#include <cstring>

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

public:
    // 생성자
    Person(const char* name, int age) {
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
        this->age = age;
    }

    // 복사 생성자 (깊은 복사)
    Person(const Person& other) {
        this->name = new char[strlen(other.name) + 1];
        strcpy(this->name, other.name);
        this->age = other.age;
    }

    // 소멸자
    ~Person() {
        delete[] name;
    }

    // 정보 출력 메서드
    void printInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }

    // 정보 수정 메서드
    void setAge(int age) {
        this->age = age;
    }
};

int main() {
    // 객체 생성
    Person person1("Alice", 30);
    
    // 복사 생성자 호출하여 새로운 객체 생성
    Person person2 = person1;
    person2.setAge(40);
    // 두 객체의 정보 출력
    std::cout << "Person 1: ";
    person1.printInfo();

    std::cout << "Person 2: ";
    person2.printInfo();

    
    return 0;
}

 

깊은 복사 후 예제

 

 

 

 결론

 

오늘은 복사 생성자에 대해서 공부해 보았습니다. 클래스 객체 하나 생성하는데 이렇게 생각해야 할게 많다니 알면 알수록 어려운 언어인 것 같습니다. 하지만 반복적으로 공부하고 체득하면 당연한 개념이 되므로 알았던 내용도 꼼꼼히 다시 정리할 필요가 있을 것 같습니다.

반응형