1. 변환 생성자(Conversion Constructor)

  • 변환 생성자는 매개변수가 1개인 생성자
  • 매개변수가 1개인 생성자(변환 생성자)는 묵시적(implicit) 형변환이 가능하여 의도하지 않게 호출되어 문제가 발생할 수 있음
  • 아래의 예제코드를 보면 main()함수에서 TestFunc(5);함수를 호출할 때 클래스 타입이 아닌 int 타입으로 형변환 발생
  • 매개변수 형식이 맞지 않기 때문에 에러가 발생해야 정상이지만 아래의 코드에서는 문제 없이 실행 됨
#include <iostream>
using namespace std;

class CTestData
{
private:
    int m_nData = 0;

public:
    // 매개변수가 하나뿐인 생성자는 형변환이 가능하다. 변환 생성자라고도 함(Conversion Constructor)
    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;
    }

};

void TestFunc(CTestData param)
{
    cout << "TestFunc(): " << param.GetData() << endl;
}

int main()
{
    cout << "***Begin***" << endl;
    TestFunc(5);
    cout << "***End***" << endl;
    return 0;
}
  • 앞서 언급한 예제코드에서 소멸자를 추가하고, TestFunc() 함수의 매개변수를 CTestData param -> const CTestData &param으로 변경하여 실행결과 확인
class CTestData
{
    // 앞의 예제 중복코드 생략
    // 소멸자 추가
    ~CTestData()
    {
        cout << "~CTestData()" << endl;
    }
};

// 매개변수가 클래스 참조형으로 정의, 묵시적 형변환 발생 --> 변환 생성자 호출
void TestFunc(const CTestData &param)
{
    cout << "TestFunc(): " << param.GetData() << endl;
}
// 실행결과
// ***Begin***
// CTestData(int)
// TestFunc(): 5
// ~CTestData()
// ***End***
  • 실행결과를 보면 CTestData(int) 변환 생성자가 호출된 것을 알 수 있음
  • 하지만, main()함수에서는 객체를 생성하는 코드가 없음
  • 변환 생성자가 호출되었다는 것은 객체가 생성되었다는 것을 의미
  • 컴파일러에 의해 임시객체가 생성되고 이에 대한 참조가 TestFunc() 함수로 전달
  • 임시객체는 함수의 종료와 함께 소멸자를 호출하여 소멸
  • TestFunc(5); 코드는 실제로 TestFunc(CTestData(5));와 같이 동작하게 됨
  • 변환 생성자에 의해 묵시적 형변환이 발생하는 것을 막기 위해 explicit 키워드 선언
    • 임시객체의 생성을 방지
    • 사용자 모르게 변환 생성자 호출에 의한 형변환이 일어나는 것을 방지
class CTestData
{
    // 중복코드 생략
public:
    // explicit 키워드를 통해 묵시적 형변환이 일어나지 않도록 함
    explicit CTestData(int nParam) : m_nData(nParam)
    {
        cout << "CTestData(int)" << endl;
    }
    // 중복코드 생략
};

int main()
{
    TestFunc(5); // 컴파일 에러 발생!
    return 0;
}


2. 임시 객체

  • 함수의 리턴형이 클래스인 경우 생성
  • 순간적으로 생성되고, 소멸
  • 다음 예제코드의 b = TestFunc(10); 부분에서 임시객체가 생성되고, 대입연산 후 소멸
  • CTestData &ref = TestFunc(10);과 같이 참조자를 이용하여 임시객체를 참조할 수 있음(이 경우 임시객체는 main 함수 종료 시 소멸)
#include <iostream>
using namespace std;

class CTestData
{
private:
    int m_nData = 0;
    char *m_pszName = nullptr;
public:
    CTestData(int nParam, char *pszName): m_nData(nParam), m_pszName(pszName)
    {
        cout << "CTestData(int)" << m_pszName << endl;
    }

    ~CTestData(){
        cout << "~CTestData(int)" << m_pszName << endl;
    }

    CTestData(const CTestData &rhs) : m_nData(rhs.m_nData), m_pszName(rhs.m_pszName)
    {
        cout << "CTestData(const CTestData &): " << m_pszName << endl;
    }

    CTestData& operator=(const CTestData &rhs)
    {
        cout << "operator=" << endl;
        // 값은 변경, 이름 값은 그대로 둠
        m_nData = rhs.m_nData;

        return *this;
    }

    int GetData() const
    {
        return m_nData;
    }

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

};

// CTestData 객체를 반환하는 함수 --> 임시객체가 생성되는 환경
CTestData TestFunc(int nParam)
{
    // 객체 a는 지역 변수
    // 따라서 함수가 반환되면 소멸 됨
    CTestData a(nParam, "a");

    return a;
}

int main()
{
    CTestData b(5, "b");
    cout << "***Before***" << endl;

    // 함수 실행 후 결과값으로 임시객체가 생성되고, 대입 연산 후 즉시 소멸한다.
    // 대입연산 후에 임시객체가 사라지는 시점이 중요!
    b = TestFunc(10);

    cout << "***After***" << endl;
    cout << b.GetData() << endl;

    return 0;
}

// 실행결과
//  CTestData(int)b
//  ***Before***
//  CTestData(int)a
//  CTestData(const CTestData &): a <-- 임시객체 생성
//  ~CTestData(): a <-- 임시객체 소멸
//  operator=
//  ~CTestData(int)a
//  ***After***
//  10
//  ~CTestData(int)b


3. r-value 참조

  • 대입 연산자의 오른쪽 항을 rvalue라고 함
  • r-value 참조자는 &가 두 번 붙음(&&rRef)
int main()
{
    // r-value 참조자 선언
    int &&data = 3 + 4;
    cout << data << endl;
    return 0;
}
  • 연산에 의해 생성되는 r-value도 임시객체
  • r-value 참조자는 다음과 같은 경우에 사용
  • 아래 예시코드에서 int &&rParamint &로 사용할 경우 'int'에서 'int &'(으)로 변환할 수 없습니다. 컴파일 에러 발생
// r-value 참조자형으로 매개변수를 전달 받음
void TestFunc(int &&rParam)
{
    cout << "TestFunc(int &&)" << endl;
}

int main()
{
    // r-value로 인자를 전달
    TestFunc(3 + 4);
    return 0;
}


4. 이동 시맨틱(Move Semantics)

  • C++11에서 추가된 개념으로 이동 생성자, 이동 대입 연산자로 구현
  • 임시객체를 다루기 위한 기능
  • 복사 생성자와 대입 연산자에 r-value 참조를 조합하여 새로운 생성자를 정의
  • 임시객체의 경우 곧 소멸되기 때문에 이동 생성자를 활용하여 깊은 복사가 아닌 얕은 복사를 통해 임시객체에 접근
#include <iostream>
using namespace std;

class CTestData
{
private:
    int m_nData = 0;

public:
    CTestData()	 { cout << "CTestData()" << endl; }
    ~CTestData() { cout << "~CTestData()" << endl; }

    CTestData(const CTestData &rhs)
            : m_nData(rhs.m_nData)
    {
        cout << "CTestData(const CTestData &)" << endl;
    }

    //이동 생성자, r-value 참조자 활용
    CTestData(const CTestData &&rhs) : m_nData(rhs.m_nData)
    {
        cout << "CTestData(const CTestData &&)" << endl;
    }

    CTestData& operator=(const CTestData &ref) = default;

    int GetData() const
    {
        return m_nData;
    }

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

};

CTestData TestFunc(int nParam)
{
    cout << "**TestFunc(): Begin***" << endl;
    CTestData a;
    a.SetData(nParam);
    cout << "**TestFunc(): End*****" << endl;
    return a;
}

int main()
{
    CTestData b;
    cout << "*Before***************" << endl;
    b = TestFunc(20);
    cout << "*After****************" << endl;
    CTestData c(b);

    return 0;
}


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



[Reference]

[1] 이것이 C++이다, p.202-226