[C++] 복사 생성자와 임시 객체

강민석·2022년 10월 18일
0

이것이 C++이다

목록 보기
3/8
post-thumbnail

4.1 복사 생성자

  • '복사 생성자(Copy Constructor)'는 객체의 복사본을 생성할 때 호출되는 생성자다.
  • 클래스 내부에서 메모리를 동적 할당 및 해제하고 이를 멤버 포인터 변수로 관리하고 있는 경우, 복사 생성자를 적용하지 않으면 문제가 발생할 수 있다.
#include <iostream>
using namespace std;

class Example {
    public:
        Example() {
            cout << "Example()" << endl;
        }

        Example(const Example &rhs) { // right hand side의 약자.
            this -> data = rhs.data;
            cout << "Example(const Example &)" << endl;
        }

        int GetData(void) const { return data; }
        void SetData(int param) { data = param; }

    private:
        int data = 0;
}

int main(int argc, char* argv[]) {
    // 디폴트 생성자가 호출되는 경우
    Example a;
    a.SetData(10);

    // 복사 생성자가 호출되는 경우
    Example b(a);
    cout << b.GetData() << endl;

    return 0;
}

4.1.1 함수 호출과 복사 생성자

  • 다음과 같은 함수는 복사 생성자의 호출을 유발한다.
void TestFunc(Example param) {
    cout << "TestFunc()" << endl;
    param.SetData(20);
}
  • 위와같은 함수는 프로그램의 성능에 안좋은 영향을 미친다.
  • 다음과 같이 참조자 매개변수를 사용하여 문제점을 보완할 수 있다.
void TestFunc(Example &param) {
    cout << "TestFunc()" << endl;
    param.SetData(20);
}
  • 참조자 매개변수는 호출 시점에 해당 함수가 call by reference인지 판단할 방법이 없다.
    • 이럴 경우, 함수를 호출했을때 원하지 않는 값의 변경이 있을 수 있다.
    • 매개변수의 형식이 클래스 형식이라면 상수형 참조로 선언하는것이 좋다.

4.1.2 깊은 복사와 얕은 복사

  • 깊은 복사(Deep Copy): 복사에 의해 실제로 두 개의 값이 생성되는 것.

  • 얕은 복사(Shallow Copy): 원본 값은 하나인데, 접근 포인터만 둘로 늘어나는 것.

  • 얕은 복사에 의해 원본 데이터를 두 개 이상의 포인터가 참조하고 있는 경우, 어느 한 곳에서 delete 연산자를 통해 메모리를 해제하면 문제가 발생할 수 있다.

4.1.3 대입 연산자

  • 단순 대입 연산자는 구조체나 클래스에도 기본적으로 적용된다.
  • 클래스의 단순 대입 연산은 기본적으로 얕은 복사를 수행한다.
    • '연산자 다중 정의'를 이용해 단순 대입 연산시 깊은 복사가 수행되게끔 할 수 있다.
    Example& operator=(const Example &rhs) {
        *data = *rhs.data;
        return *this;
    }

4.2 묵시적 변환

4.2.1 변환 생성자

  • 매개변수가 한 개인 생성자를 '변환 생성자(Conversion Constructor)'라고도 한다.
  • '변환 생성자'가 존재하는 클래스의 경우 묵시적인 형변환이 일어날 수 있다.
class Example {
public:
    Example(int param): data(param) {}
    int GetData(void) const {
        return data;
    }

    void SetData(int param) {
        data = param;
    }

private:
    int data = 0;
}

void TestFunc(Example param) {
    cout << param.GetData() << endl;
}

int main(int argc, char* argv[]) {
    // TestFunc의 매개변수 타입은 Example이지만, Example 클래스의 변환 생성자에 의해 묵시적으로 형변환이 일어난다.
    // 따라서 위 코드는 TestFunc(Example(5))와 같은 의미다.
    // 변환 생성자에 explicit 예약어를 추가함으로써 묵시적인 형변환을 방지할 수 있다.
    TestFunc(5);

    return 0;
}
  • 위 예제의 경우, TestFunc(5)를 호출하면 Example 타입의 임시 객체가 생성된 후 함수의 종료와 동시에 소멸된다.
  • 묵시적 형변환을 남발할 경우 임시 객체로 인해 메모리가 낭비될 수 있다.

4.2.2 허용되는 변환

  • 위의 예제에서 int 자료형이 Example 형식으로 변환될 수는 있으나, 반대의 경우는 불가능하다.
  • 다음과 같이 형변환 연산자를 추가함으로써 반대의 경우도 구현할 수 있다.
class Example {
    ~~~
    public:
        operator int(void) { return data;}
    ~~~
}

int main(int argc, char* argv[]) {
    Example a(10);
    int data1 = (int)a; // C 스타일 변환으로, 되도록 사용하지 않는것이 권장됨. 형변환이 지원되지 않는 경우에도 강제로 형변환을 해버리기 때문.
    int data2 = a;
    int data3 = static_cast<int>(a); // C++에서 권장되는 형변환 연산. 형변환이 지원되지 않는 경우에 제약이 따른다.
    return 0;
}
  • C++의 형변환 연산자로는 다음과 같은 것들이 있다.

    • const_cast
    • static_cast
    • dynamic_cast
    • reinterpret_cast
  • 형변환 연산자에도 explicit 예약어를 적용할 수 있다.

4.3 임시 객체와 이동 시맨틱

  • 묵시적 형변환시 발생하는 임시 객체보다도 더 은밀한 임시 객체가 있다.
  • 이는 함수의 반환 형식이 클래스인 경우에 발생한다.

4.3.1 이름 없는 임시 객체

4.3.2 이동 시맨틱

  • 이동 시맨틱이란, 복사 생성자와 대입 연산자에 r-value 참조를 조합해서 새로운 생성 및 대입의 경우를 만들어 낸 것이다.
#include <iostream>

class Example {
public:
    Example() { cout << "디폴트 생성자" << endl; }
    ~Example() { cout << "소멸자" << endl; }

    Example(const Example &rhs): data(rhs.data) {
        cout << "복사 생성자" << endl;
    }

    Example(Example &&rhs): data(rhs.data) {
        cout << "이동 생성자" << endl;
    }

    Example& operator=(const Example &) = default;

    int GetData() const { return data; }
    void SetData(int param) { data = param; }

private:
    int data = 0;
}

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

int main(int argc, char* argv[]) {
    Example b;
    cout << "*****Before*****" << endl;
    b = TestFunc(20);
    cout << "*****After******" << endl;
    Example c(b);

    return 0;
}
/*
출력 결과:
    디폴트 생성자
    *****Before*****
    ***TestFunc(): Begin***
    디폴트 생성자
    ***TestFunc(): End***
    이동 생성자
    소멸자 -> TestFunc에서의 지역변수 a의 소멸
    소멸자 -> TestFunc의 반환값을 b로 넘길 때 생기는 임시 객체의 소멸
    *****After******
    복사 생성자
    소멸자 -> b의 소멸
    소멸자 -> c의 소멸
*/
  • 어차피 사라질 임시 객체에는 깊은 복사를 수행하는 것이 아니라, 얕은 복사를 수행함으로써 성능을 높일 수 있다.

0개의 댓글