[생성자를 통한 객체 생성]

 

생성자를 통해 객체를 생성하는 방식은 다양합니다. 아래의 코드에서는 일반 생성자, 복사 생성자, 이동 생성자의 활용 예시를 적어두었습니다.

#include <iostream>
#include <string>

class MyString 
{
public:
    std::string Name;
    std::string Tag;

    MyString(const char* _Str) : Name(_Str)
    {
        std::cout << "일반 생성자 호출 : " << _Str << "\n";
    }

    MyString(const MyString& _Other) : Name(_Other.Name)
    {
        std::cout << "복사 생성자 호출 : " << _Other.Name << "\n";;
    }

    MyString(MyString&& _Other) noexcept : Name(std::move(_Other.Name))
    {
        std::cout << "이동 생성자 호출 : " << _Other.Name << "\n";;
        this->Name;
    }

    MyString operator+(const MyString& _Other) const
    {
        std::cout << "operator+ 호출\n";
        return MyString((Name + _Other.Name).c_str());
    }
};

int main() 
{
    MyString str1 = "asdf";
    MyString str2 = "qwer";
    MyString temp = str1 + str2;
    MyString str3 = temp;
    MyString str4 = std::move(temp);
}

 

이제 다음의 코드를 확인해보겠습니다.

MyString temp = str1 + str2;
MyString str3 = temp;

 

첫 번째 줄은 str1 + str2를 통해 객체 하나를 생성한 뒤 temp에 전달하는 방식으로, 복사 생성자가 호출될 것이라 예상됩니다. 하지만 이전에 포스팅했던 대로 복사 생략이 발생합니다.

https://umtimos.tistory.com/144

 

[C++] 복사 생략(Copy Elision)

[일반 생성자와 복사 생성자] C++에서 클래스의 객체를 생성할 때, 임의로 생성자를 따로 선언하지 않을 경우 디폴트 생성자로 객체를 생성합니다. 하지만 생성자를 따로 정의하여 인자를 전달하

umtimos.tistory.com

 

컴파일러는 이미 생성된 str1 + str2가 리턴한 객체를 temp인셈 치고 사용하는 것입니다. 하지만 두 번째 줄의 경우네는 복사 생성자가 호출되는데, 아래 그림과 같은 작업이 이뤄집니다.

temp로부터 값을 전달할 임시 객체를 생성하고, str3는 복사 생성자를 호출하여 빈 공간을 할당한 뒤, 임시 객체의 값을 빈 공간에 복사하고 소거되면서 str3가 완성되는 구조입니다. 이 과정에서는 복사 생략이 발생하지 않습니다.

 

 

[이동 생성자]

 

C++ 11부터는 이런 문제를 해결하고자 이동 생성자를 도입했습니다. 이동 생성자는 기존의 복사 생성자보다 적은 비용을 활용하여 객체를 이전(이동)시기는 방식으로 동작합니다. 임시 객체나 더 이상 사용하지 않을 객체에 대해 자원을 복사하는 대신, 그 자원의 소유권을 넘겨주는 방식으로 동작하는 것입니다.

MyString str4 = std::move(temp);

 

위의 코드에서 사용된 std::move()는 말 그대로 temp를 rvalue로 취급하도록 하여, 이동 생성자의 인자로 전달되도록 해줍니다. 즉, temp는 더 이상 쓸 일이 없음을 명시적으로 알려주어 해당 시점에 MyString 클래스가 이동 생성자를 호출하게 됩니다.

 

+) std::move()는 C++ 11부터 utility 라이브러리를 통해 제공되고 있습니다. 

+) 여기서 한 가지 유의할 점은, 데이터 이동 과정은 이동 생성자나 이동 대입 연산자가 호출되는 시점에 진행되는 것이지, move를 사용한 시점에 수행되는 것이 아니라는 점입니다. 실제로 이동 생성자나 이동 대입 연산자를 따로 선언하지 않고 move() 함수만 활용할 경우 기존보다 더욱 느린 복사 생성자가 호출됩니다.

 

이동 생성자가 호출되면 temp가 가진 내부 자원(여기서는 Name)의 내용이 str4로 이전(이동)외고, temp는 비워진 상태가 됩니다. 이러한 과정에서 불필요한 메모리 복사나 할당이 발생하지 않기 때문에 복사 생성자보다 훨씬 효율적으로 동작할 수 있게 됩니다. 이를 확인하기 위한 간단한 코드로, 아래를 확인해봅시다.

MyString(MyString&& _Other) noexcept : Name(std::move(_Other.Name))
{
    std::cout << "이동 생성자 호출" << "\n";;
    std::cout << "temp의 Name : " << _Other.Name << "\n";;
    std::cout << "str4의 Name : " << this->Name << "\n";;
}

 

이동 연산자가 호출되는 과정에서 std::move()의 인자로 전달된 temp의 Name 내부 버퍼를 str4(this)의 Name이 가져갑니다. 이렇게 되면 원래 객체인 temp는 더 이상 유효한 상태가 아니기 때문에 다시 활용하지 않는 것이 좋습니다.

 

 

[이동 생성자가 유용한 경우]

 

다음의 경우에 이동 생성자가 유용하게 활용될 수 있습니다.

 

- 함수 리턴 값이 객체인 경우

- 컨테이너 등에 값을 emplace_back, push_back 할 때

- 자원을 소유하고 있는 객체를 더이상 사용하지 않을 때

 

추가로, 아래의 경우에도 사용하면 좋습니다.

template <typename T>
void my_swap(T &a, T &b) 
{
  T tmp(std::move(a));
  a = std::move(b);
  b = std::move(tmp);
}

 

C++ 기본 자료형(int, float 등)이 아니라, 위에서 사용한 MyString과 같은 사용자 정의 자료형의 경우에는 위와 같이 사용하면 좋습니다.

'C++' 카테고리의 다른 글

[C++] 빌드 과정  (0) 2025.09.03
[C++] 완벽한 전달(Perfact Forwarding)과 std::forward  (0) 2025.05.16
[C++] 좌측값과 우측값(lvalue and rvalue)  (0) 2025.05.15
[C++] 복사 생략(Copy Elision)  (0) 2025.05.15
[C++] union  (0) 2025.05.14

+ Recent posts