1. 복사 생성자

  • 복사 생성자(Copy Constructor)는 객체를 복사할 때 호출되는 생성자
  • class에 정의하지 않을 경우 컴파일러가 자동으로 default 복사 생성자를 생성
// 복사 생성자 예제 코드
#include <iostream>
using namespace std;

class CMyData
{
private:
    int m_Data = 0;

public:
    CMyData()
    {
        cout << "MyData()" << endl;
    }

    // 복사 생성자
    CMyData(const CMyData &rhs)
    {
        this->m_Data = rhs.m_Data;
        cout << "CMyData(const CMyData &)" << endl;
    }

    int GetData() const
    {
        return m_Data;
    }

    void SetData(int nParam)
    {
        m_Data = nParam;
    }

};

int main()
{
    CMyData a;
    a.SetData(10);

    CMyData b(a); // 복사 생성자 호출
    cout << b.GetData() << endl;

    return 0;
}


2. 함수 호출과 복사 생성자

  • 복사 생성자가 호출되는 경우
    • 객체를 복사하는 경우
    • 함수의 매개변수로 객체를 전달하는 경우
    • 함수의 리턴타입이 class인 경우(임시객체 생성)
  • 매개변수로 객체를 전달할 경우에는 참조자(reference)를 사용(성능 문제)
  • 상세한 내용은 다음 예제코드에 주석으로 설명하였음
// 함수의 매개변수로 객체가 전달되는 경우의 예제코드
#include <iostream>
using namespace std;

class CTestData
{
private:
    int m_nData = 0;
public:
    CTestData(int nParam) : m_nData(nParam)
    {
        cout << "CTestData(int)" << endl;
    }

    // 복사 생성자
    CTestData(const CTestData &rhs) : m_nData(rhs.m_nData)
    {
        cout << "CTestData(const CTestData &)" << endl;
    }

    // 읽기 전용 상수형 함수
    int GetData() const {
        return m_nData;
    }

    // 멤버 변수를 수정하는 함수
    void SetData(int nParam){
        m_nData = nParam;
    }
};

// 매개변수가 CTestData 클래스 타입이므로 복사 생성자 호출
// 해당 함수 호출 시, 객체의 복사본이 생성 (원본 객체에 대한 조작 X)
// * 해결방법: 참조자(Reference) 이용!
//             void TestFunc(CTestData &param);
//             객체를 참조형태로 매개변수로 전달할 경우 복사본 객체를 생성하는 것이 아니라 원본을 이용하게 됨!
void TestFunc(CTestData param)
{
    cout << "TestFunc()" << endl;
    // 피호출자 함수에서 매개변수 인스턴스의 값 변경
    param.SetData(20);
}

int main()
{
    cout << "***Begin***" << endl;
    // a 객체 생성 및 생성자를 이용하여 멤버 변수를 10으로 초기화
    CTestData a(10);
    // 생성된 객체를 TestFunc()함수의 매개변수로 전달 --> 복사 생성자 호출
    // TestFunc()에서 SetData()함수를 호출하여 객체의 값을 변경하려고 시도하지만 결과적으로 변경 되지 않음
    // 그 이유는 임시객체의 멤버 값을 변경 시킬 뿐 실제 객체 a의 멤버 값을 변경시키는 것은 아니기 때문
    // TestFunc()함수 종료와 함께 임시객체는 사라짐
    TestFunc(a);

    // 함수 호출 후 a의 값을 출력한다.
    cout << "a: " << a.GetData() <<endl;

    cout << "***End***" << endl;

    return 0;
}
// 실행결과
//
// ***Begin***
// CTestData(int)
// CTestData(const CTestData &)
// TestFunc()
// a: 10
// ***End***


3. 깊은 복사(Deep Copy) & 얕은 복사(Shallow Copy)

  • 깊은 복사: 복사에 의해 값이 실제로 각각 생성되는 것
  • 얕은 복사: 각각의 값이 생성되는 것이 아니라 포인터에 의해 접근 방법만 복사된 것(포인터에 의해 주소값이 복사된 경우)
  • 얕은 복사의 경우 다양한 메모리 문제가 발생할 수 있음(ex.이미 해제한 메모리에 대해 중복 해제 하는 경우)
  • 아래의 예제코드에서 pB = pA 부분을 통해 각각 동적할당된 메모리를 가리키는 것이 아니라 결국 pA와 pB가 동일한 메모리 주소를 가리킴(Shallow Copy)
  • 깊은 복사가 되게 하려면 *pB = *pA로 수정
#include <iostream>
using namespace std;

int main()
{
    int *pA, *pB;

    pA = new int;
    *pA = 10;

    pB = new int;
    pB = pA; // shallow copy
//    *pB = *pA; <-- deep copy

    cout << *pA << endl;
    cout << *pB << endl;

    return 0;
}
  • 클래스에서 멤버 변수로 포인터 변수를 갖는 경우 얕은 복사에 의한 메모리 문제가 발생할 수 있음
  • 다음의 예제코드는 소멸자가 호출될 때 메모리 에러가 발생
#include <iostream>
using namespace std;

class CMyData
{
private:
    // 포인터 타입의 멤버변수
    int *m_pnData = nullptr;

public:
    CMyData(int param)
    {
        m_pnData = new int;
        *m_pnData = param;
    }


    int GetData() const
    {
        if(m_pnData != NULL)
        {
            return *m_pnData;
        }
    }

    // 소멸자
    ~CMyData()
    {
        delete m_pnData;
    }

};

int main()
{
    CMyData a(10);
    CMyData b(a); // 복사 생성자 호출

    cout << a.GetData() << endl;
    cout << b.GetData() << endl;

    return 0;
}
  • 클래스 내부에 복사 생성자가 정의 되어 있지 않기 때문에 CMyData b(a);에서 컴파일러에 의해 자동으로 default 복사 생성자 호출(default 복사 생성자는 얕은 복사 수행)
  • 객체 a, b는 동일한 메모리 m_pnData를 가리킴
  • 객체 a의 소멸자가 호출되면 할당 받은 메모리가 해제되고, 이어서 객체 b의 소멸자가 호출되면서 메모리 에러가 발생함
  • 이미 a객체의 소멸자에 의해 가리키던 주소의 메모리가 해제 되었기 때문
  • 메모리 중복 해제 문제를 해결하기 위해서는 깊은 복사를 수행하는 복사 생성자를 반드시 클래스 내부에 구현해야 함
// 클래스 내부에 다음과 같이 복사 생성자를 구현하여 깊은 복사를 수
// 깊은 복사 수행 복사 생성자 선언 및 정의
    CMyData(const CMyData &rhs)
    {
        cout << "CMyData(const CMyData &)" << endl;

        // 메모리 할당
        m_pnData = new int;

        // 포인터가 가리키는 위치에 값을 복사 (별도의 주소 위치에 값을 넣는 것으로 얕은 복사가 아니라 깊은 복사를 의미)
        *m_pnData = *rhs.m_pnData;

    }
  • 대입 연산자(=)에 의해 객체를 복사하는 경우에도 얕은 복사가 이루어지기 때문에 메모리 문제가 발생할 수 있음
  • 연산자 오버로딩(operator overloading)을 통해 문제 해결 가능(5장 연산자 오버로딩)
  • 클래스 내부에 복사 생성자와 대입 연산자 오버로딩 구현을 통해 얕은 복사로 인해 발생하는 메모리 문제 해결!
// main()
int main()
{
  Test a(10);
  Test b(10);
  a = b; // 대입 연산자에 의한 얕은 복사 수행
}
// 클래스 내부에 대입 연산자의 함수를 오버로딩(깊은 복사)
CMydata& operator=(const CMyData &rhs)
{
  *m_nData = *rhs.m_pnData;
}


본 포스팅 내용은 이것이 C++이다를 학습한 내용을 바탕으로 요약한 글입니다. 코드에 문제가 있을 수 있으며 다소 부정확한 내용이 있을 수 있으니 참고하시기 바랍니다. 잘못된 내용이 있다면 덧글로 남겨주시면 내용을 반영하여 수정하도록 하겠습니다. 감사합니다.



[Reference]

[1] 이것이 C++이다, p.181-201