Menu
Coddy logo textTech

3/5/0의 법칙

Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 25번째.

클래스가 동적 메모리와 같은 리소스를 관리할 때, 사용자 정의 소멸자, 복사 생성자 및 이동 생성자가 필요하다는 것을 배웠습니다. 하지만 어떤 특별 멤버 함수를 구현해야 할지 결정하는 데 도움이 되는 지침 원칙이 있습니다. 바로 3의 규칙, 5의 규칙, 0의 규칙(Rule of Three, Five, and Zero)입니다.

3의 법칙(Rule of Three)은 다음과 같이 명시합니다: 만약 이 세 가지 중 하나라도 정의한다면, 세 가지 모두를 정의해야 합니다:

  • 소멸자 (Destructor)
  • 복사 생성자 (Copy constructor)
  • 복사 대입 연산자 (Copy assignment operator)

5가지 규칙(Rule of Five)은 현대 C++에서 이를 확장하여 이동 연산을 추가합니다:

  • 소멸자
  • 복사 생성자
  • 복사 대입 연산자
  • 이동 생성자
  • 이동 대입 연산자
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. 소멸자
    Buffer(const Buffer& other);                          // 2. 복사 생성자
    Buffer& operator=(const Buffer& other);               // 3. 복사 대입 연산자
    Buffer(Buffer&& other) noexcept;                      // 4. 이동 생성자
    Buffer& operator=(Buffer&& other) noexcept;           // 5. 이동 대입 연산자
};

0의 규칙(Rule of Zero)은 가장 간단한 접근 방식입니다. 클래스가 리소스를 직접 관리하지 않는다면, 이러한 함수들을 정의하지 마세요. 컴파일러가 이를 생성하도록 하거나, 리소스를 대신 처리해 주는 스마트 포인터와 표준 컨테이너를 사용하세요.

class Player {
    std::string name;              // std::string은 자체적으로 메모리를 관리합니다
    std::vector<int> scores;       // std::vector는 자체 리소스를 처리합니다
public:
    Player(std::string n) : name(n) {}
    // 소멸자, 복사 또는 이동 함수가 필요하지 않습니다!
};

이러한 규칙을 따르면 일부 연산은 정의되어 있지만 다른 연산이 누락되었을 때 발생하는 이중 삭제(double-deletion), 메모리 누수(memory leaks), 댕글링 포인터(dangling pointers)와 같은 버그를 방지할 수 있습니다.

challenge icon

챌린지

쉬움

동적으로 할당된 문자 데이터를 적절하게 관리하기 위해 5가지 특별 멤버 함수를 모두 구현하는 Rule of Five(5가지의 규칙)를 따르는 TextBuffer 클래스를 만들어 보겠습니다. 이를 통해 복사 및 이동 연산이 어떻게 함께 작동하여 견고한 리소스 관리 클래스를 생성하는지 보여줄 것입니다.

코드를 정리하기 위해 두 개의 파일을 생성합니다:

  • TextBuffer.h: 동적으로 할당된 문자 배열에 텍스트를 저장하는 TextBuffer 클래스를 정의합니다. 클래스에는 다음이 필요합니다:
    • Private 멤버: 텍스트 내용을 위한 char* 포인터 data, 그리고 문자열 길이(널 종료 문자 제외)를 위한 size_t length
    • C-문자열(const char*)을 인자로 받아 메모리를 할당하고 내용을 복사하며, "TextBuffer created: <text>"를 출력하는 매개변수 생성자
    • 메모리를 해제하고(null이 아닌 경우) "TextBuffer destroyed"를 출력하는 소멸자
    • 깊은 복사(deep copy)를 수행하고 "TextBuffer copied"를 출력하는 복사 생성자
    • 자기 자신에 대한 대입을 처리하고, 기존 데이터를 정리하며, 깊은 복사를 수행하고 "TextBuffer copy-assigned"를 출력하는 복사 대입 연산자. *this를 반환합니다.
    • 소유권을 이전하고 "TextBuffer moved"를 출력하는 이동 생성자 (noexcept로 표시). 소스 객체는 유효한 빈 상태로 둡니다.
    • 자기 자신에 대한 대입을 처리하고, 기존 데이터를 정리하며, 소유권을 이전하고 "TextBuffer move-assigned"를 출력하는 이동 대입 연산자 (noexcept로 표시). *this를 반환합니다.
    • 저장된 텍스트를 반환하는 getText() 메서드 (데이터가 null인 경우 빈 문자열 "" 반환)
    • 길이를 반환하는 getLength() 메서드
  • main.cpp: 5가지 특별 멤버 함수가 작동하는 모습을 보여줍니다. 입력으로부터 텍스트 문자열을 읽은 후 다음을 수행합니다:
    • 입력 텍스트로 original이라는 이름의 TextBuffer를 생성합니다.
    • original로부터 복사 생성자를 사용하여 copied를 생성합니다.
    • "Temporary"라는 텍스트로 another를 생성합니다.
    • 복사 대입을 사용합니다: another = original
    • std::move()를 사용하여 original로부터 이동 생성하여 moved를 생성합니다.
    • "Target"이라는 텍스트로 target을 생성합니다.
    • 이동 대입을 사용합니다: target = std::move(copied)
    • "--- Final State ---"를 출력합니다.
    • 각 버퍼(original, copied, moved, another, target)에 대해 "original: <text> (length: <len>)" 형식을 출력합니다.

이동 후, 소스 객체(originalcopied)는 길이가 0인 빈 텍스트를 보여야 하며, 대상 객체는 이전된 데이터를 보유해야 합니다. 이는 안전한 리소스 관리를 보장하기 위해 5가지 함수가 모두 함께 작동하는 Rule of Five를 보여줍니다.

strlenstrcpy와 같은 문자열 함수를 위해 <cstring>을 포함하고, std::move()를 위해 <utility>를 포함하세요.

치트 시트

3의 규칙(Rule of Three)은 다음 세 가지 특수 멤버 함수 중 하나라도 정의한다면, 세 가지 모두를 정의해야 한다는 규칙입니다:

  • 소멸자(Destructor)
  • 복사 생성자(Copy constructor)
  • 복사 대입 연산자(Copy assignment operator)

5의 규칙(Rule of Five)은 현대적인 C++을 위해 이동 연산을 추가하여 이를 확장합니다:

  • 소멸자(Destructor)
  • 복사 생성자(Copy constructor)
  • 복사 대입 연산자(Copy assignment operator)
  • 이동 생성자(Move constructor)
  • 이동 대입 연산자(Move assignment operator)
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. 소멸자
    Buffer(const Buffer& other);                          // 2. 복사 생성자
    Buffer& operator=(const Buffer& other);               // 3. 복사 대입
    Buffer(Buffer&& other) noexcept;                      // 4. 이동 생성자
    Buffer& operator=(Buffer&& other) noexcept;           // 5. 이동 대입
};

0의 규칙(Rule of Zero)은 클래스가 리소스를 직접 관리하지 않는다면, 어떠한 특수 멤버 함수도 정의하지 말아야 한다는 규칙입니다. 리소스를 자동으로 처리하는 스마트 포인터와 표준 컨테이너를 사용하세요:

class Player {
    std::string name;              // std::string은 자체적으로 메모리를 관리합니다
    std::vector<int> scores;       // std::vector는 리소스를 처리합니다
public:
    Player(std::string n) : name(n) {}
    // 소멸자, 복사 또는 이동 함수가 필요하지 않습니다!
};

이러한 규칙을 따르면 이중 해제(double-deletion), 메모리 누수(memory leaks), 댕글링 포인터(dangling pointers)와 같은 버그를 방지할 수 있습니다.

직접 해보기

#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"

using namespace std;

int main() {
    string input;
    getline(cin, input);

    // TODO: 입력 텍스트를 사용하여 'original'이라는 이름의 TextBuffer 생성

    // TODO: 'original'로부터 복사 생성자를 사용하여 'copied' 생성

    // TODO: "Temporary"라는 텍스트를 가진 'another' 생성

    // TODO: 복사 할당 연산 사용: another = original

    // TODO: std::move()를 사용하여 'original'로부터 이동 생성으로 'moved' 생성

    // TODO: "Target"이라는 텍스트를 가진 'target' 생성

    // TODO: 이동 할당 연산 사용: target = std::move(copied)

    // TODO: "--- Final State ---" 출력

    // TODO: 다음 형식으로 각 버퍼의 상태 출력:
    // "original: <text> (length: <len>)"
    // 다음 항목들에 대해 출력: original, copied, moved, another, target

    return 0;
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨