← SEARCHABLE ARCHIVE
C / C++ · 2026-07-30

C++ 기초부터 마스터까지 — 14화

복사와 이동은 무엇이 다를까?

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

vector에 큰 객체를 넣거나 함수에서 객체를 반환할 때 복사가 많이 일어날 것처럼 보인다. C++에서는 값을 그대로 복제하는 복사와, 더 이상 필요 없는 객체의 자원을 넘겨받는 이동을 구분한다.

복사

복사는 원본과 같은 값을 가진 새로운 객체를 만든다.

class Texture
{
public:
    explicit Texture(std::vector<unsigned char> pixels)
        : pixels(std::move(pixels))
    {
    }

private:
    std::vector<unsigned char> pixels;
};

Texture original(LoadPixels());
Texture copied = original;

copied가 만들어질 때 vector 내부 데이터도 복사된다. 두 객체는 서로 독립적이므로 copied를 수정해도 original은 바뀌지 않는다.

작은 값은 복사 비용이 거의 없지만, 큰 버퍼나 많은 문자열을 가진 객체는 비용이 커질 수 있다.

이동

원본을 더 이상 사용하지 않는다면 내부 자원을 새 객체로 넘길 수 있다.

Texture loaded(LoadPixels());
Texture equipped = std::move(loaded);

std::move가 직접 데이터를 옮기는 것은 아니다. 객체를 이동 가능한 값으로 취급하도록 바꾸고, 실제 작업은 이동 생성자나 이동 대입 연산자가 수행한다.

이동된 loaded는 여전히 파괴할 수 있고 새 값을 대입할 수도 있다. 하지만 내부 상태가 이전과 같다고 기대하면 안 된다.

loaded = Texture(LoadPixels()); // 새 값을 대입하는 것은 가능하다.

Rule of Zero

직접 메모리를 new와 delete로 관리하면 복사 생성자, 복사 대입, 이동 생성자, 이동 대입, 소멸자를 함께 고민해야 한다. 하나라도 잘못 구현하면 이중 해제나 메모리 누수가 발생할 수 있다.

class CharacterData
{
public:
    std::string name;
    std::vector<int> learnedSkills;
    std::unique_ptr<Weapon> weapon;
};

표준 컨테이너와 스마트 포인터를 멤버로 사용하면 각 타입이 자신의 자원을 관리한다. 이처럼 특별 멤버 함수를 직접 작성하지 않는 방식을 Rule of Zero라고 부른다.

unique_ptr은 복사할 수 없고 이동만 할 수 있다. 이 제한 덕분에 단독 소유권을 실수로 복제하는 일을 컴파일 단계에서 막을 수 있다.

값을 전달하는 방법

읽기만 하는 큰 객체는 const T&로 받고, 함수가 소유권을 가져갈 때는 값으로 받은 뒤 이동하는 방식이 자주 사용된다.

class Party
{
public:
    void Add(CharacterData character)
    {
        members.push_back(std::move(character));
    }

private:
    std::vector<CharacterData> members;
};

Party party;
CharacterData warrior;
party.Add(std::move(warrior));

CharacterData는 unique_ptr을 멤버로 가지므로 복사할 수 없고 이동만 할 수 있다. 기존 객체를 추가할 때는 std::move로 소유권을 넘기거나 임시 객체를 전달해야 한다.

무조건 모든 곳에 std::move를 붙이는 것이 최적화는 아니다.

복사가 필요한 값인지, 소유권을 넘겨도 되는 값인지 먼저 결정해야 한다. 이동은 빠른 복사의 문법이 아니라 자원을 넘긴 뒤 원본 상태에 의존하지 않겠다는 표현이다.