본문 바로가기
프로그래밍 언어/C++

C++ 4단원 복습! (1편)

by 조엘 2020. 3. 14.

안녕하세요. 파피몬입니다.

 

이 복습 내용은 최호성 선생님의 "이것이 C++이다"를 공부하고 작성하였습니다. 

아직 모르는 게 많은 학생이라 오개념이 있을 수 있습니다. 친절하게 댓글로 알려주시면 좋은 공부 될 것 같습니다!!

4단원에서는 복사 생성자와 임시 객체에 대해서 배워봅니다. 1편, 2편으로 나누어져 있습니다.

 

Chapter 4. 복사 생성자와 임시 객체

 

1. C++에서 다루는 4가지 생성자를 살펴봅니다.

 ** 디폴트 생성자 - Chapter 3에서 다루었습니다.

 ** 복사 생성자 - 처음 초기화한 객체에 기존 객체를 복사시킬 경우 호출됩니다. 

 ** 변환 생성자 - 매개 변수가 하나인 생성자입니다. 임시객체를 생성할 수 있어 묵시적 형변환을 차단해야 합니다. 

 ** 이동 생성자 - r-value가 임시 객체 일 때 부르며, 메모리를 덜 쓰는 구조로 만들어 줍니다.

천천히 하나씩 알아봅니다. 

 

 

2. class b = a; 라는 구문으로 Shallow CopyDeep Copy를 알아봅니다. a, b 각각에는 멤버 변수로 포인터 변수가 있다고 가정합니다. 

 

Shallow Copy는 따로 복사생성자를 정의하지 않아도 컴파일러가 기본적으로 만들어주는 디폴트 복사 생성자 입니다. a가 가르키고 있는 대상체를 b도 가르킬 수 있게 합니다. 이로써 두 가지 값 각각 같은 값을 가르키지만 a나 b중 한명이 동적 할당한 포인터를 해제하면 다른 하나는 갈 곳을 잃게 됩니다. 

 

Deep Copy는 a가 가르키는 대상체를 하나 더 만들어 b의 포인터가 가르킬 수 있게 합니다. 이로써 a와 b는 각각 다른 주소에 저장되어 있는, 하지만 같은 값을 가진 변수를 가르치는 독자적인 포인터를 갖습니다. 

 

이를 구현 하기 위해서는 복사 생성자를 별도로 정의해야 합니다. 구현하는 법을 알아봅시다. 

 

 

3. 기본 문법은 다음과 같습니다.

클래스이름 (const 클래스이름 &rhs);

Deep copy를 할 수 있는 예시 코드 입니다. 아무것도 써주지 않으면 2번의 Shallow copy가 실행됩니다. 

class CMyData
{
private:
    int *m_pnData = nullptr;
    
public: 
    CMyData(int nParam)
    {
        m_pnData = new int;
        *m_pnData = nParam;
    }
    
    CMyData(const CMyData &rhs)
    {
        m_pnData = new int;
        *m_pnData = *rhs.m_pnData;
    }
    
    ~CMyData()
    {
        delete m_pnData;
    }
};

복사 생성자를 따로 정의해 현 인스턴스에게 새롭게 동적으로 메모리를 할당해주고, 거기에 rhs의 m_pnData가 가르키는 값을 저장했습니다. 

 

 

4. 이번에는 class a,b를 별도로 생성한 후, a=b; 라는 코드의 동작을 살펴봅니다. 여기서 '=' 대입 연산자는 따로 정의를 해주지 않으면 복사 생성자처럼 Shallow copy를 진행합니다. 만약 클래스가 위에와 같이 정의되어 있다면, 소멸자가 불리면서 같은 값을 두 번 지우려는 불상사가 일어납니다. 이를 막아봅시다. 

class CMyData
{
private:
    int *m_pnData = nullptr;
    
public: 
    CMyData(int nParam)
    {
        m_pnData = new int;
        *m_pnData = nParam;
    }
    
    CMyData& operator=(const CMyData &rhs)
    {
        *m_pnData = *rhs.m_pnData;
        //객체 스스로에 대한 참조를 반환!
        return *this;
    }
    
    CMyData(const CMyData &rhs)
    {
        m_pnData = new int;
        *m_pnData = *rhs.m_pnData;
    }
    
    ~CMyData()
    {
        delete m_pnData;
    }
};

CMyData& operator=() 함수를 통해 '=' 연산자를 재정의 해줄 수 있습니다. 이는 연산자 오버로딩에서 더 깊게 다루겠습니다.

 

 

5. 변환 생성자 (매개변수가 하나인 생성자)에 대해 알아봅니다. 이 친구는 말도 안 해주고 클래스를 매개변수의 형식으로 바꿔줄때가 있습니다. 예시코드를 보면서 살펴 봅시다. 

#include<iostream>
using namespace std;

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

void TestFunc(CTestData param)
{
    //직접 멤버변수에 접근하는 것은 좋은 코드는 아닙니다.
    //클래스 내에서 접근하는 함수를 만들어 주는게 좋습니다. 
    cout<<"TestFunc: "<< param.m_nData <<endl;
}

int main()
{
    TestFunc(5);
    return 0;
}     

--------------------
CTestData(int)
TestFunc: 5
~CTestData()

여기서 TestFunc의 매개변수는 CTestData 이지만, CTestData(int)라는 변환 연산자를 지원하는 바람에 TestFunc(5)가 매개변수로 인정되어 넘어가 param(5)라는 객체를 만들어 버립니다. 

 

이와 같은 상황을 막기 위해 explicit 예약어를 사용합니다! 자기 맘대로 바꿔버리는 변환 생성자의 능력을 막아줌으로서 필요치 않은 상황을 만들지 않게 할 수 있습니다. 

#include<iostream>
using namespace std;

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

void TestFunc(CTestData param)
{
    //직접 멤버변수에 접근하는 것은 좋은 코드는 아닙니다.
    //클래스 내에서 접근하는 함수를 만들어 주는게 좋습니다. 
    cout<<"TestFunc: "<< param.m_nData <<endl;
}

int main()
{
    TestFunc(5);
    return 0;
}     

--------------------
오류!!

 

 

6. 하지만 필요에 따라서는 객체와 변수(위의 같은 경우 int)사이의 자유로운 변환이 필요할 때도 있습니다. 이 경우에는 형변환 연산자를 통해 구현합니다. 

#include<iostream>
using namespace std;

class CTestData
{
public:
    int m_nData = 0;
    
    explicit CTestData(int nParam)
        :m_nData(nParam)
    {
    	cout<< "CTestData(int)" <<endl;
    }
    
    //CTestData --> int 로 형변환!!
    operator int(void) { return m_nData; }
    
    ~CTestData()
    {
        cout<< "~CTestData()" <<endl;
    }
};

void TestFunc(CTestData param)
{
    //직접 멤버변수에 접근하는 것은 좋은 코드는 아닙니다.
    //클래스 내에서 접근하는 함수를 만들어 주는게 좋습니다. 
    cout<<"TestFunc: "<< param.m_nData <<endl;
}

int main()
{
    TestFunc(5);
    return 0;
}     

 

이 역시 explicit 예약어를 앞에 붙여 무조건적인 변환을 막고 static_cast 연산자를 통해 제한적인 형 변환을 거칠 수 있습니다! 

반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ 5단원 복습! (1편)  (0) 2020.03.22
C++ 4단원 복습! (2편)  (0) 2020.03.15
C++ 3단원 복습! (2편)  (0) 2020.03.12
C++ 3단원 복습! (1편)  (0) 2020.03.12
C++ 2단원 복습!  (0) 2020.03.11

댓글