콘텐츠로 건너뛰기

C++에서 변수 및 객체를 복사하는 방법

1. 단일 변수 (Primitive Types & Pointers)

단일 변수는 가장 간단하게 복사할 수 있다.

  • 기본 타입 (Primitive Types): int, double, char 같은 기본 타입은 대입 연산자 (=)를 사용해 값을 직접 복사한다. 깊은 복사 또는 값 복사(Value copy)라 할 수 있다.
int original = 10;
int copy = original; // 값 10이 복사됨
  • 포인터 (Pointers): 포인터 변수를 복사할 때도 대입 연산자를 사용하지만, 복사되는 것은 포인터가 가리키는 대상이 아니라 메모리 주소 값 자체이다. 따라서 두 포인터는 같은 메모리 위치를 가리키게 된다. 얕은 복사이다.
int* ptr_original = new int(100);
int* ptr_copy = ptr_original; // 주소 값만 복사됨. 같은 메모리 (100)을 가리킴.

이 경우, ptr_copy를 통해 값을 변경하면 ptr_original이 가리키는 값도 변경된다.

2. 배열 (Arrays)

C++에서 기본 배열(C-style array)은 다른 변수처럼 대입 연산자로 직접 복사하는 것이 불가능하다. 배열은 메모리 블록을 나타내기 때문에, 배열의 요소를 하나씩 복사해야 한다. 즉, 모두 깊은 복사이다.

  • 반복문 사용: 배열의 크기만큼 반복문을 돌면서 각 요소를 수동으로 복사한다.
int source_arr[5] = {1, 2, 3, 4, 5};
int dest_arr[5];

for (int i = 0; i < 5; ++i) {
    dest_arr[i] = source_arr[i];
}
  • std::copy 또는 memcpy 사용: 대용량 배열 복사 시 성능을 위해 표준 라이브러리 함수를 사용할 수 있다.
#include <algorithm> // for std::copy
// int source_arr[5] = {1, 2, 3, 4, 5};
// int dest_arr[5];
std::copy(source_arr, source_arr + 5, dest_arr);

// 또는 C 스타일
#include <cstring> // for memcpy
memcpy(dest_arr, source_arr, sizeof(source_arr));
  • std::array 또는 std::vector 사용: C++ 표준 라이브러리의 컨테이너를 사용하면 배열처럼 직접 대입 연산자를 통해 복사할 수 있다. 이들은 깊은 복사(Deep Copy)를 수행한다.
#include <vector>
std::vector<int> original_vec = {10, 20, 30};
std::vector<int> copy_vec = original_vec; // 깊은 복사

3. 함수 (Functions/Functors)

C++에서 함수 자체를 복사한다는 것은 주로 함수 포인터, std::function 객체, 또는 람다 캡처 목록을 복사하는 것을 의미한다.

  • 함수 포인터: 함수 포인터는 함수가 위치한 메모리 주소를 저장하는 변수이므로, 단일 변수 복사와 마찬가지로 대입 연산자로 주소 값을 복사한다. 이건 실행코드의 머리를 잡고 있는 핸들을 하나 더 만들었다고 보면된다. 함수의 내용인 명령어 코드를 중간에 변경할 수 없으므로 깊은 복사, 얕은 복사 개념이 없다.
int add(int a, int b) { return a + b; }

int (*ptr_original)(int, int) = add;
int (*ptr_copy)(int, int) = ptr_original; // 주소 값 복사
  • std::function: 함수 객체(function object)를 담을 수 있는 표준 라이브러리 타입이다. 대입 연산자를 사용하면 함수 객체의 복사본이 생성된다. 깊은 복사를 의미한다.
#include <functional>
std::function<int(int, int)> func_original = add;
std::function<int(int, int)> func_copy = func_original; // 함수 객체 복사
  • 람다 (Lambda Expressions): 람다는 클로저 객체로 취급된다. 캡처된 변수가 있다면, 기본적으로 람다 객체를 복사할 때 캡처된 변수도 함께 복사된다. (값 캡처 [=], 참조 캡처 [&]에 따라 복사 방식이 달라진다.)
int x = 5;
auto lambda_original = [x]() { return x * 2; }; // x를 값으로 캡처
auto lambda_copy = lambda_original; // 람다 객체와 캡처된 x의 복사본이 생성됨
//람다의 얕은 복사
//참조 캡처를 사용
#include <iostream>

int main() {
    int shared_x = 10; // 외부 공유 변수
    
    // 1. 람다 원본 (lambda_original) 생성
    // [&] : shared_x를 참조로 캡처. mutable은 참조 캡처에서는 불필요하지만,
    // 람다 내부에서 외부 변수(shared_x)를 직접 수정하고 있음에 주목.
    auto lambda_original = [&shared_x]() {
        // 원본과 복사본 모두 이 외부 변수를 직접 수정/참조함
        std::cout << "Original 람다 호출: shared_x 값: " << shared_x << std::endl;
        shared_x += 1; // 외부 변수의 값을 수정
        return shared_x; 
    };
    
    // 2. 람다 복사본 (lambda_copy) 생성
    // 람다 객체가 복사되지만, 캡처된 것은 shared_x의 주소(참조)이므로 주소만 복사됨.
    auto lambda_copy = lambda_original; 
    
    std::cout << "--- 함수 호출 전 ---" << std::endl;
    std::cout << "공유 외부 x 값 (shared_x): " << shared_x << std::endl; // 10
    
    // 3. lambda_original 호출
    std::cout << "\n--- Original 호출 ---" << std::endl;
    lambda_original(); // shared_x=10 출력 후 외부 shared_x가 11로 증가
    lambda_original(); // shared_x=11 출력 후 외부 shared_x가 12로 증가

    // 4. lambda_copy 호출
    // lambda_copy는 lambda_original이 수정한 현재 값(12)을 참조하여 시작함.
    std::cout << "\n--- Copy 호출 ---" << std::endl;
    lambda_copy(); // shared_x=12 출력 후 외부 shared_x가 13로 증가
    lambda_copy(); // shared_x=13 출력 후 외부 shared_x가 14로 증가
    
    // 5. Original 재호출 (변경된 값 확인)
    // lambda_original은 lambda_copy가 수정한 현재 상태(14)를 참조함.
    std::cout << "\n--- Original 재호출 ---" << std::endl;
    lambda_original(); // shared_x=14 출력 후 외부 shared_x가 15로 증가
    
    return 0;
}
//람다의 깊은 복사 예제
//람다의 값 캡

#include <iostream>
#include <functional> // std::function을 사용하지 않고 auto 람다의 동작 원리 이해를 위해

int main() {
    int initial_x = 10;
    
    // 1. 람다 원본 (lambda_original) 생성
    // [x] : x를 값으로 캡처. mutable 키워드를 사용하여 람다 내부에서 캡처된 x를 수정 가능하게 함.
    auto lambda_original = [x = initial_x]() mutable {
        std::cout << "Original 람다 내부 x 값: " << x << std::endl;
        x += 1; // 캡처된 x의 복사본을 수정 (원본 x와는 무관)
        return x; 
    };
    
    // 2. 람다 복사본 (lambda_copy) 생성
    // 람다 객체와 캡처된 변수 x의 복사본이 생성됨.
    auto lambda_copy = lambda_original; 
    
    std::cout << "--- 함수 호출 전 ---" << std::endl;
    std::cout << "초기 외부 x 값 (initial_x): " << initial_x << std::endl; // 10
    
    // 3. lambda_original 호출
    std::cout << "\n--- Original 호출 ---" << std::endl;
    lambda_original(); // x=10 출력 후 내부 x가 11로 증가
    lambda_original(); // x=11 출력 후 내부 x가 12로 증가

    // 4. lambda_copy 호출
    // lambda_copy는 lambda_original과 독립적인 캡처 변수 x의 복사본(값 10)을 가지고 시작함.
    std::cout << "\n--- Copy 호출 ---" << std::endl;
    lambda_copy(); // x=10 출력 후 내부 x가 11로 증가
    lambda_copy(); // x=11 출력 후 내부 x가 12로 증가
    
    // 5. Original 재호출
    // lambda_original은 캡처 변수 x의 현재 상태(12)를 유지함.
    std::cout << "\n--- Original 재호출 ---" << std::endl;
    lambda_original(); // x=12 출력 후 내부 x가 13로 증가        

    return 0;
}

4. 클래스 (Classes)

클래스(객체) 복사는 가장 복잡하며, 얕은 복사깊은 복사 개념이 가장 중요하게 적용된다. 특히 기본 복사를 하게되면 클래스내부에 포인터변수가 있다면, 메모리 해제 과정에서 문제가 발생하게 된다.

4.1. 기본 복사 (Default Copy)

클래스에 사용자 정의 복사 생성자나, 사용자 정의 대입 연산자가 없을 때, 컴파일러는 기본적으로 멤버별 복사(Memberwise Copy)를 수행한다.

  • 멤버별 복사(Shallow Copy): 모든 멤버 변수를 순서대로 복사한다.
    • 일반 멤버 변수는 값이 복사된다.
    • 포인터 멤버 변수주소 값만 복사된다. (위의 1번 포인터 복사와 동일)
    • 클래스 내에, 포인터 멤버 변수가 있는 경우, 기본 복사를 사용하면 같은 메모리를 공유하게 되어 한쪽에서 delete를 호출하면 다른 쪽 객체에서 문제가 발생하는 이중 해제(Double Deletion) 위험이 있다. 이 경우 4.2와 같은 깊은 복사가 필요하다.
//문제가 발생하는 코드
#include <iostream>

class DataWrapper {
public:
    int* m_data; // 동적으로 할당된 메모리를 가리키는 포인터 멤버

    // 1. 생성자: 메모리 동적 할당
    DataWrapper(int val) {
        m_data = new int(val);
        std::cout << "객체 생성 및 메모리 할당: " << *m_data << std::endl;
    }

    // 2. 소멸자: 할당된 메모리 해제
    ~DataWrapper() {
        if (m_data != nullptr) {
            delete m_data;
            m_data = nullptr;
            std::cout << "객체 소멸 및 메모리 해제" << std::endl;
        }
    }
};

void test_shallow_copy() {
    // 1. 원본 객체 생성
    DataWrapper original(100);
    // original.m_data는 메모리 주소 X를 가리키고, 그 안에 값 100이 있음.

    std::cout << "\n--- 얕은 복사 수행 (기본 복사 생성자 호출) ---\n";
    // 2. 복사 객체 생성 (복사 생성자 자동 호출)
    // 컴파일러가 기본 멤버별 복사를 수행:
    // original의 m_data 포인터 주소 값(X)이 copy의 m_data로 그대로 복사됨.
    // 두 포인터가 같은 메모리 주소(X)를 가리키게 됨.
    DataWrapper copy = original;
    
    std::cout << "Original 포인터 주소: " << original.m_data << ", 값: " << *original.m_data << std::endl;
    std::cout << "Copy 포인터 주소: " << copy.m_data << ", 값: " << *copy.m_data << std::endl;
    
    std::cout << "\n--- 복사본을 통해 값 변경 ---\n";
    // 3. 복사본을 통해 값 변경
    *copy.m_data = 200;

    // 원본과 복사본 모두 같은 메모리(X)를 가리키므로 값이 함께 변경됨.
    std::cout << "Original 값 확인: " << *original.m_data << std::endl; // 200 출력
    std::cout << "Copy 값 확인: " << *copy.m_data << std::endl;     // 200 출력
    
    std::cout << "\n--- 함수 종료 (소멸자 호출 시 문제 발생) ---\n";
} // 여기서 copy와 original의 소멸자가 순차적으로 호출됨

int main() {
    test_shallow_copy();
    // 프로그램이 비정상 종료되거나 런타임 오류가 발생할 가능성이 높음.
    return 0;
}

4.2. 깊은 복사 (Deep Copy)

객체 내에 동적 할당된 메모리(포인터 멤버)가 있을 때, 주소 값만 복사하는 얕은 복사가 아닌, 새로운 메모리를 할당하고 그 내용을 복사하는 것을 깊은 복사라고 한다.

깊은 복사를 구현하려면 복사 생성자 또는 복사 대입 연산자를 사용자 정의해야 한다.

1. 복사 생성자 (Copy Constructor)

객체가 생성될 때(초기화 시) 복사를 처리한다. 생성자라는 의미는 객체를 만든다라는 의미이므로 객체를 하나 더 생성하면서 복사한다.

class MyClass {
private:
    int* data;
public:
    // 복사 생성자 (깊은 복사 구현)
    MyClass(const MyClass& other) { //&가 있다고 얕은 복사가 아니다. 
        data = new int(*other.data); // 새로운 메모리를 할당하고 내용을 복사
    }
    // ... (기타 멤버 및 소멸자)
};

MyClass original_obj;
MyClass copy_obj = original_obj; // 복사 생성자 호출

2. 복사 대입 연산자 (Copy Assignment Operator)

이미 존재하는 객체에 다른 객체의 값을 대입할 때 복사를 처리한다. 기존 객체의 자원은 반환한 후 복사해온다.

class MyClass {
private:
    int* data;
public:
    // 복사 대입 연산자 (깊은 복사 구현)
    MyClass& operator=(const MyClass& other) {
        if (this != &other) { // 자기 자신에게 대입하는 경우 방지
            delete data; // 기존 메모리 해제
            data = new int(*other.data); // 새로운 메모리 할당 및 복사
        }
        return *this;
    }
    // ... (기타 멤버 및 생성자/소멸자)
};

MyClass obj_A;
MyClass obj_B;
obj_B = obj_A; // 복사 대입 연산자 호출

5. 스마트 포인터

최근에는 로우 포인터 대신 스마트 포인터(unique_ptr, std::shared_ptr)를 클래스 멤버로 사용하여 복사 생성자와 복사 대입 연산자를 명시적으로 구현할 필요를 없앤다. 스마트 포인터가 자원 관리(RAII, Resource Acquisition Is Initialization)를 대신 해주기 때문이다. 하지만 스마트 포인터를 사용하지 않는 레거시 코드나 시스템에서는 Rule of Three/Five를 반드시 지켜야 한다.

#include <iostream>
#include <memory>

class SafeDataContainer {
private:
    // 힙 메모리를 관리하는 스마트 포인터
    // unique_ptr: 배타적인 소유권을 가짐 (오직 하나의 포인터만 자원을 소유)
    std::unique_ptr<int> data;

public:
    // 생성자: 힙 메모리 할당 및 초기화
    SafeDataContainer(int val) : data(std::make_unique<int>(val)) {
        std::cout << "DataContainer 생성: " << *data << std::endl;
    }

    // 소멸자만 구현 (Rule of Five 멤버 중 하나)
    // unique_ptr이 자동으로 data가 가리키는 메모리를 해제해주므로,
    // 명시적 delete는 필요 없지만, 객체 소멸 확인용으로만 구현
    ~SafeDataContainer() {
        if (data) {
            std::cout << "DataContainer 소멸: 값 " << *data << " 해제됨." << std::endl;
        } else {
            std::cout << "DataContainer 소멸: 이미 소유권이 이동됨." << std::endl;
        }
    }

    // data 값을 읽는 함수
    int getData() const {
        return *data;
    }
    
    // 복사 관련 함수들을 명시적으로 선언하거나 구현할 필요 없음!
    // 컴파일러가 unique_ptr의 규칙에 따라 복사를 '삭제(delete)'하고 이동을 '기본(default)'으로 정의함.
};

int main() {
    // 1. 원본 객체 생성
    SafeDataContainer original(100);
    
    // 2. 복사 시도 (컴파일 에러 발생): unique_ptr은 복사를 금지함.
    // SafeDataContainer copy = original; // ❌ 컴파일 에러 (복사 생성자가 deleted 됨)

    // 3. 이동 사용 (Rule of Five 중 이동 생성자 대신): 자원 소유권 이동
    SafeDataContainer moved_container = std::move(original); 
    // original의 data 자원을 moved_container로 안전하게 이동시킴.
    
    std::cout << "\n--- 이동 후 상태 확인 ---" << std::endl;
    // original.data는 nullptr 상태가 됨.
    // moved_container는 original이 가리키던 힙 메모리의 새로운 소유자가 됨.
    std::cout << "Moved Container 값: " << moved_container.getData() << std::endl; // 100
    // std::cout << "Original Container 값: " << original.getData() << std::endl; // 런타임 에러 발생 (data가 nullptr이 됨)
    
    // main 함수 종료 시:
    // moved_container만 소멸자 호출 시 메모리 해제 담당.
    // original은 이미 자원을 포기했으므로 아무 작업도 하지 않음.
    
    return 0;
}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다