C++, CS

[C++] 객체 복사를 막는 방법과 이유

dhlee-dev 2026. 4. 28. 20:39

C++에서는 객체를 복사할 때 복사 생성자와 복사 대입 연산자가 사용된다.
하지만 모든 객체가 안전하게 복사될 수 있는 것은 아니다.

예를 들어 특정 객체는 복사되면 비용이 크거나,
내부 자원 관리 문제로 인해 의도하지 않은 오류가 발생할 수 있다.

따라서 오늘은 객체의 복사를 막는 방법과 그 이유를 정리해보려고 한다.


객체 복사를 막는 방법

C++11 이후에는 복사 생성자와 복사 대입 연산자에 delete를 명시해서 객체 복사를 막을 수 있다.

class A {
public:
    A() = default;

    A(const A&) = delete;
    A& operator=(const A&) = delete;
};

위 코드에서

  • A(const A&) = delete
  • A& operator=(const A&) = delete

를 통해 복사 생성과 복사 대입을 모두 막았다.

이렇게 하면 아래와 같은 코드는 컴파일되지 않는다.

A a;
A b = a;  // 복사 생성 불가

A c;
c = a;    // 복사 대입 불가

C++11 이전 방식

C++11 이전에는 delete 문법이 없었기 때문에
복사 생성자와 복사 대입 연산자를 private 영역에 선언만 하고 정의하지 않는 방식으로 복사를 막았다.

class T {
private:
    T(const T&);
    T& operator=(const T&);
};

하지만 의도를 명확하게 알아보기 어렵고, friend 키워드를 사용하면 접근할 수 있기 때문에
C++11 이후엔 delete 키워드를 이용해서 복사를 막는 것이 권장된다.


복사 불가능한 멤버를 포함한 경우

클래스 내부에 복사 불가능한 멤버가 있으면 해당 클래스의 복사 연산이 자동으로 삭제될 수 있다.
대표적인 예시가 std::unique_ptr이다.

#include <memory>

class B {
private:
    std::unique_ptr<int> data;
};

int main() {
    B b;
    //B c = b;    // 컴파일러가 자동으로 복사를 삭제해서 컴파일 불가능
}

unique_ptr은 단독 소유권을 표현하는 스마트 포인터이므로 복사가 불가능하다.

즉, 복사를 막으려고 직접 delete를 작성하지 않아도
복사 불가능한 멤버가 있다면 클래스 전체의 복사가 제한될 수 있다.


객체 복사를 막는 이유

객체 복사를 막는 이유는 아래와 같이 정리할 수 있다.

복사 비용이 큰 경우

객체가 큰 데이터를 가지고 있다면 복사 비용이 커질 수 있다.

예를 들어 큰 배열, 이미지 데이터, 많은 문자열, 대용량 버퍼 등을 가진 객체라면
객체를 복사할 때마다 많은 메모리와 시간이 필요할 수 있다.
이런 경우 의도치 않은 복사가 발생하면 성능 저하로 이어질 수 있다.

따라서 복사가 필요하지 않은 객체라면 아예 복사를 막아 불필요한 비용을 방지할 수 있다.

자원 소유 문제가 있는 경우

객체가 raw pointer 형태로 동적 자원을 직접 소유하고 있다면 기본 복사 동작은 위험할 수 있다.
아래 예시 코드에서 복사로 인해 문제가 생기는 상황을 볼 수 있다.

#include <iostream>
#include <memory>

class Resource {
public:
    Resource() {
        std::cout << "생성자 진입\n";
        data = new int(10);
    }

    ~Resource() {
        std::cout << "소멸자 진입\n";
        delete data;
    }

private:
    int* data;
};

// 얕은 복사 문제 발생함, 정의되지 않은 동작!!
void Func(Resource resource) {
    // 리소스 처리
}


int main() {
    Resource a;
    Func(a);
}

Func(a)를 호출하면 Resource 객체가 값으로 복사된다.
이때 기본 복사는 data 포인터 값만 복사하므로, 원본과 복사본이 같은 메모리를 가리키게 된다.
이후에 함수가 끝날 때 data가 가리키는 메모리를 해제하고, main이 끝날 때 다시 해당 메모리를 해제한다.

결과적으로 두 객체의 소멸자에서 같은 메모리를 두 번 해제하게 되어 정의되지 않은 동작이 발생할 수 있다.
이러한 문제가 얕은 복사로 인해 발생할 수 있다.

얕은 복사로 인해 발생할 수 있는 문제는 다음과 같다.

  • 같은 자원을 두 번 해제하는 중복 해제 문제
  • 한 객체가 자원을 해제한 뒤 다른 객체가 댕글링 포인터를 가지는 문제
  • 소유권이 불명확해지는 문제

이런 문제를 막기 위해서는 다음과 같은 방법을 사용할 수 있다.

  • 복사를 금지한다.
  • 깊은 복사를 직접 구현한다.
  • RAII와 스마트 포인터를 사용한다.

이동만 허용할 수도 있다

복사는 막되, 이동은 허용하고 싶은 경우도 있다.
예를 들어 자원의 소유권을 다른 객체로 넘기고 싶을 때는 복사보다 이동이 더 적절하다.

이 경우 복사 생성자와 복사 대입 연산자는 삭제하고, 이동 생성자와 이동 대입 연산자는 허용할 수 있다.

class Test {
public:
    Test() = default;

    // 복사 명시적으로 제거
    Test(const Test&) = delete;
    Test& operator=(const Test&) = delete;

    // 이동 연산 허용
    Test(Test&&) noexcept = default;
    Test& operator=(Test&&) noexcept = default;
};

위 코드는 복사는 막고 이동만 허용하는 형태이다.


이동 연산에 noexcept를 붙이는 이유

이동 생성자와 이동 대입 연산자에는 보통 noexcept를 붙이는 것이 좋다.

T(T&&) noexcept = default;
T& operator=(T&&) noexcept = default;

noexcept는 이 함수가 예외를 던지지 않는다는 의미이다.

이동 연산에 noexcept를 붙이는 이유는 다음과 같다.

  • 예외가 발생하지 않는다고 명시할 수 있다.
  • 표준 컨테이너가 더 안전하게 이동 연산을 사용할 수 있다.
  • 불필요한 복사를 줄이고 성능 최적화에 도움이 될 수 있다.

예를 들어 std::vector는 내부 버퍼를 재할당할 때 기존 원소들을 새 메모리로 옮겨야 한다.
이때 이동 생성자가 noexcept라면 vector는 복사 대신 이동을 선택할 수 있다.

만약 이동 생성자가 noexcept가 아니라면 vector는 예외 안전성을 위해
이동 대신 복사를 선택할 수 있고, 이로 인해 불필요한 성능 비용이 발생할 수 있다.

따라서 이동만 허용하는 객체라면 이동 생성자와 이동 대입 연산자에 noexcept를 붙이는 것이 좋다.


정리

객체 복사를 막는 방법은 다음과 같다.

  • 복사 생성자, 복사 대입 연산자에 = delete를 명시한다.
  • C++11 이전에는 private에 선언만 하고 정의하지 않는 방식을 사용했다.
  • unique_ptr처럼 복사 불가능한 멤버를 포함하면 클래스의 복사도 제한될 수 있다.

객체 복사를 막는 이유는 다음과 같다.

  • 객체가 클 경우 복사 비용이 크다.
  • raw pointer로 자원을 소유하는 객체는 얕은 복사 문제가 생길 수 있다.

또 복사는 막고 이동만 허용할 수도 있다.

  • 이동 생성자와 이동 대입 연산자를 정의한다.
  • 이동 연산에는 noexcept를 붙이는 것이 좋다.

마무리

C++ 클래스를 계속 다루다 보니 크기가 큰 객체나 자원을 소유하는 객체는 무작정 복사하는 것보다
이동하거나 참조로 다루는 편이 더 적절할 수 있다는 것을 느꼈다.

실제로 언리얼 엔진 코드에서도 복사를 막는 예시를 확인할 수 있었고,
복사를 허용할지 막을지는 객체의 소유권과 수명 관리에 관련된 중요한 설계라는 생각이 들었다.

앞으로 프로젝트에서 클래스를 설계할 때 객체가 복사되어도 안전한지,
이동만 허용해야 하는지, 아니면 아예 복사를 막아야 하는지를 잘 판단해서 적용해야겠다.

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

[C++] map과 unordered_map 정리  (0) 2026.04.30
[C++] vector 와 list의 차이 정리  (0) 2026.04.29
[C++] 스마트 포인터 정리  (0) 2026.04.27
[C++] RTTI와 RAII 정리  (0) 2026.04.24
[C++] vtable 정리  (1) 2026.04.23