C++ 복사/이동 생성자 | "Rule of Five" 가이드

C++ 복사/이동 생성자 | "Rule of Five" 가이드

이 글의 핵심

C++ 복사/이동 생성자에 대한 실전 가이드입니다. Rule of Five 등을 예제와 함께 설명합니다.

Rule of Five

리소스를 관리하는 클래스는 5개 특수 멤버 함수 정의

class Resource {
private:
    int* data;
    size_t size;
    
public:
    // 1. 생성자
    Resource(size_t s) : size(s), data(new int[s]) {}
    
    // 2. 소멸자
    ~Resource() {
        delete[] data;
    }
    
    // 3. 복사 생성자
    Resource(const Resource& other) 
        : size(other.size), data(new int[other.size]) {
        copy(other.data, other.data + size, data);
    }
    
    // 4. 복사 대입 연산자
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            copy(other.data, other.data + size, data);
        }
        return *this;
    }
    
    // 5. 이동 생성자
    Resource(Resource&& other) noexcept
        : size(other.size), data(other.data) {
        other.data = nullptr;
        other.size = 0;
    }
    
    // 6. 이동 대입 연산자
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

복사 생성자

class String {
private:
    char* data;
    size_t length;
    
public:
    String(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    
    // 복사 생성자
    String(const String& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        cout << "복사 생성자 호출" << endl;
    }
    
    ~String() {
        delete[] data;
    }
};

int main() {
    String s1("Hello");
    String s2 = s1;  // 복사 생성자 호출
    String s3(s1);   // 복사 생성자 호출
}

이동 생성자

class String {
private:
    char* data;
    size_t length;
    
public:
    String(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    
    // 이동 생성자
    String(String&& other) noexcept {
        data = other.data;
        length = other.length;
        
        other.data = nullptr;
        other.length = 0;
        
        cout << "이동 생성자 호출" << endl;
    }
    
    ~String() {
        delete[] data;
    }
};

int main() {
    String s1("Hello");
    String s2 = move(s1);  // 이동 생성자 호출
    // s1은 이제 비어있음
}

실전 예시

예시 1: 동적 배열

template<typename T>
class DynamicArray {
private:
    T* data;
    size_t size;
    size_t capacity;
    
public:
    DynamicArray(size_t cap = 10) 
        : size(0), capacity(cap), data(new T[cap]) {}
    
    ~DynamicArray() {
        delete[] data;
    }
    
    // 복사 생성자
    DynamicArray(const DynamicArray& other)
        : size(other.size), capacity(other.capacity), 
          data(new T[other.capacity]) {
        copy(other.data, other.data + size, data);
    }
    
    // 복사 대입
    DynamicArray& operator=(const DynamicArray& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            capacity = other.capacity;
            data = new T[capacity];
            copy(other.data, other.data + size, data);
        }
        return *this;
    }
    
    // 이동 생성자
    DynamicArray(DynamicArray&& other) noexcept
        : size(other.size), capacity(other.capacity), data(other.data) {
        other.data = nullptr;
        other.size = 0;
        other.capacity = 0;
    }
    
    // 이동 대입
    DynamicArray& operator=(DynamicArray&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            capacity = other.capacity;
            other.data = nullptr;
            other.size = 0;
            other.capacity = 0;
        }
        return *this;
    }
    
    void push_back(const T& value) {
        if (size == capacity) {
            resize();
        }
        data[size++] = value;
    }
    
    T& operator {
        return data[index];
    }
    
    size_t getSize() const {
        return size;
    }
    
private:
    void resize() {
        capacity *= 2;
        T* newData = new T[capacity];
        copy(data, data + size, newData);
        delete[] data;
        data = newData;
    }
};

int main() {
    DynamicArray<int> arr1;
    arr1.push_back(1);
    arr1.push_back(2);
    
    DynamicArray<int> arr2 = arr1;  // 복사
    DynamicArray<int> arr3 = move(arr1);  // 이동
}

예시 2: 파일 핸들러

class FileHandle {
private:
    FILE* file;
    string filename;
    
public:
    FileHandle(const string& name) : filename(name) {
        file = fopen(name.c_str(), "r");
        if (!file) {
            throw runtime_error("파일 열기 실패");
        }
    }
    
    ~FileHandle() {
        if (file) {
            fclose(file);
        }
    }
    
    // 복사 금지
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // 이동 허용
    FileHandle(FileHandle&& other) noexcept
        : file(other.file), filename(move(other.filename)) {
        other.file = nullptr;
    }
    
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file) {
                fclose(file);
            }
            file = other.file;
            filename = move(other.filename);
            other.file = nullptr;
        }
        return *this;
    }
    
    string read() {
        if (!file) return "";
        
        fseek(file, 0, SEEK_END);
        long size = ftell(file);
        fseek(file, 0, SEEK_SET);
        
        string content(size, '\0');
        fread(&content[0], 1, size, file);
        
        return content;
    }
};

int main() {
    FileHandle fh1("test.txt");
    // FileHandle fh2 = fh1;  // 에러: 복사 금지
    FileHandle fh2 = move(fh1);  // OK: 이동
}

예시 3: 스마트 포인터 (간단한 버전)

template<typename T>
class UniquePtr {
private:
    T* ptr;
    
public:
    explicit UniquePtr(T* p = nullptr) : ptr(p) {}
    
    ~UniquePtr() {
        delete ptr;
    }
    
    // 복사 금지
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;
    
    // 이동 허용
    UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;
    }
    
    UniquePtr& operator=(UniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }
    
    T& operator*() const {
        return *ptr;
    }
    
    T* operator->() const {
        return ptr;
    }
    
    T* get() const {
        return ptr;
    }
    
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }
};

int main() {
    UniquePtr<int> p1(new int(42));
    // UniquePtr<int> p2 = p1;  // 에러: 복사 금지
    UniquePtr<int> p2 = move(p1);  // OK: 이동
    
    cout << *p2 << endl;  // 42
}

복사 생략 (Copy Elision)

String createString() {
    return String("Hello");  // 복사/이동 생략 가능
}

int main() {
    String s = createString();  // 복사/이동 없음 (C++17)
}

자주 발생하는 문제

문제 1: 얕은 복사

// ❌ 얕은 복사 (기본 복사 생성자)
class BadString {
private:
    char* data;
    
public:
    BadString(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    
    ~BadString() {
        delete[] data;
    }
    
    // 기본 복사 생성자: 포인터만 복사
};

BadString s1("Hello");
BadString s2 = s1;  // 같은 메모리 가리킴
// 소멸 시 double free!

// ✅ 깊은 복사
class GoodString {
private:
    char* data;
    
public:
    GoodString(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    
    GoodString(const GoodString& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }
    
    ~GoodString() {
        delete[] data;
    }
};

문제 2: 자기 대입

// ❌ 자기 대입 미처리
Resource& operator=(const Resource& other) {
    delete[] data;  // 자기 대입 시 문제!
    size = other.size;
    data = new int[size];
    copy(other.data, other.data + size, data);
    return *this;
}

// ✅ 자기 대입 체크
Resource& operator=(const Resource& other) {
    if (this != &other) {
        delete[] data;
        size = other.size;
        data = new int[size];
        copy(other.data, other.data + size, data);
    }
    return *this;
}

문제 3: noexcept 누락

// ❌ noexcept 없음
Resource(Resource&& other) {
    // ...
}

// ✅ noexcept 추가
Resource(Resource&& other) noexcept {
    // ...
}

// 이유: vector 등에서 이동 생성자가 noexcept여야 사용

FAQ

Q1: Rule of Five는 언제 필요?

A: 리소스(메모리, 파일 등)를 직접 관리할 때.

Q2: 복사 vs 이동?

A:

  • 복사: 독립적인 복사본
  • 이동: 소유권 이전, 빠름

Q3: = delete는?

A: 복사/이동 금지. unique_ptr 같은 경우.

Q4: noexcept는 왜?

A: 예외 안전성, 성능 최적화 (vector 등).

Q5: Rule of Zero는?

A: 스마트 포인터 사용 시 특수 멤버 함수 불필요.

Q6: 복사/이동 생성자 학습 리소스는?

A:

  • “Effective Modern C++”
  • cppreference.com
  • “C++ Primer”

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Algorithm Copy | “복사 알고리즘” 가이드
  • C++ default와 delete | “특수 멤버 함수” 가이드
  • C++ Custom Deleters | “커스텀 삭제자” 가이드

관련 글

  • C++ 클래스와 객체 |
  • C++ new vs malloc |
  • C++ shared_ptr vs unique_ptr |
  • C++ malloc vs new vs make_unique | 메모리 할당 완벽 비교
  • C++ Custom Deleters |