[전문가를 위한 C++] 9장. 클래스와 객체 마스터하기

Chapter 9. 클래스와 객체 마스터하기

9.1 friend

  • friend로 지정된 대상은 속해 있는 클래스의 protected나 private 데이터 멤버와 메서드에 접근할 수 있다.

9.2 객체에 동적 메모리 할당하기

SpreadSheet 클래스

  • SpreadSheetCell을 가지는 SpreadSheet 클래스를 정의해 보자.

    class SpreadSheet {
        public:
          SpreadSheet(size_t width, size_t height);
        private:
          SpreadSheetCell** mCells = nullptr;
    };
    
    SpreadSheet::SpreadSheet(size_t width, size_t, height) 
      : mWidth(width), mHeight(height) {
          mCells = new SpreadSheetCell*[mWidth];
          for (size_t i = 0; i < mWidth; i++) {
              mCells[i] = new SpreadSheetCell[mHeight];
          }
      }
      
    int main() {
        SpreadSheet s1(4,3);
        return 0;
    }
    
    • 2차원 동적 배열을 할당할 때, 자바와 같이 new SpreadSheet[mWidth][mHeight]와 같이 작성할 수 없음에 주의.
    • 너비 4, 높이 3의 크기를 가진 s1이란 이름의 SpreadSheet 객체가 스택에 할당 되었을 때의 메모리 상태 Untitled Diagram drawio

소멸자로 메모리 해제하기

  • 객체 안에서 동적으로 할당한 메모리는 그 객체의 소멸자에서 해제하는 것이 바람직하다.
  • 소멸자는 익셉션이 발생하지 않기 때문에 기본적으로 noexcept가 적용된다.
  • SpreadSheet 소멸자 구현 예시

    SpreadSheet::~SpreadSheet() {
        for (size_t i = 0; i < mWidth; i++) {
            delete[] mCells[i];
        }
        delete[] mCells;
        mCells = nullptr;
    }
    

복제와 대입 처리하기

  • 컴파일러에서 생성되는 복제 생성자나 대입 연산자는 int, double, 포인터와 같은 기본 타입에 대해서는 비트 단위 복제, 얕은 복제 또는 대입이 적용된다. 이 때, 메모리를 동적으로 할당한 객체는 문제가 발생한다.
  • 따라서 클래스에 동적 할당 메모리가 있다면 이를 깊은 복제로 처리하도록 복제 생성자와 대입 연산자를 직접 정의해야 한다.

    // printSpreadSheet 함수가 리턴될 때, s 소멸자 호출.
    // s가 가리키던 메모리 해제해 버린다.
    void printSpreadSheet(SpreadSheet s) {...}
    
    int main() {
        SpreadSheet s1(4,3);
        printSpreadSheet(s1);  // shallow copy!
    
        SpreadSheet s2(2,2), s3(4,3);
        s2 = s3;  // memory leak!
        return 0;
    }
    
    • 얕은 복제와 댕글링 포인터
      • printSpreadSheet 메서드 호출 시 Untitled Diagram drawio (1)
      • printSpreadSheet 메서드 종료 (댕글링 포인터) Untitled Diagram drawio (2)
    • 대입 연산과 메모리 누수
  • SpreadSheet 복제 생성자

    class SpreadSheet {
        public:
          SpreadSheet(const SpreadSheet& src);
    };
    
    SpreadSheet::SpreadSheet(const SpreadSheet& src) 
      : SpreadSheet(src.mWidth, src.mHeight) {
          for (size_t i = 0; i < mWidth; i++ ) {
              for (size_t j = 0; j < mHeight; j++) {
                  mCells[i][j] = src.mCells[i][j];  // Deep Copy!
              }
          }
    }
    
  • SpreadSheet 대입 연산자
    • 선언

      class SpreadSheet {
          public:
            SpreadSheet& operator=(const SpreadSheet& rhs);
      };
      
    • 단순 구현

      SpreadSheet::SpreadSheet& operator=(const SpreadSheet& rhs) {
          // 자신과 같은지 확인
          if (this == &rhs) {
              return *this;
          }
      
          // 기존 메모리 해제
          for (size_t i = 0; i < mWidth; i++) {
              delete[] mCells[i];
          }
          delete[] mCells;
          mCells = nullptr;
      
          // 메모리 새로 할당
          mWidth = rhs.mWidth;
          mHeight = rhs.mHeight;
      
          mCells = new SpreadSheetCell*[mWidth];
          for (size_t i = 0; i < mWidth; i++) {
              mCells[i] = new SpreadSheetCell[mHeight];
          }
      
          // 데이터복제 
          for (size_t i = 0; i < mWidth; i++) {
              mCells[i][j] = rhs.mCells[i][j];
          }
      
          return *this;
      }
      
    • 복사 후 맞바꾸기 (익셉션 문제 해소)

      • 익셉션이 발생해도 원본 객체 (this)에 대한 복구는 보장 해야 한다.

        class SpreadSheet {
            public:
              SpreadSheet& operator=(const SpreadSheet& rhs);
              friend void swap(SpreadSheet& first, SpreadSheet& second) noexcept;
        };
        
        // swap함수에서는 except를 던지지 않는다.
        void swap(SpreadSheet& first, SpreadSheet& second) noexcept {
            using std::swap;
        
            swap(first.mWidth, second.mWidth);
            swap(first.mHeight, second.mHeight);
            swap(first.mCells, second.mCells);
        }
        
        SpreadSheet& SpreadSheet::operator=(const SpreadSheet& rhs) {
            if (this == &rhs) return *this;
        
            SpreadSheet temp(rhs);
            swap(*this, temp);
            return *this;
        }
        
  • 대입과 값 전달 방식 금지
    • 클래스에 메모리를 동적으로 할당할 때 아무도 이 객체에 대해 복제나 대입을 할 수 없게 만들 수도 있다. (명시적 삭제)

이동 의미론으로 이동 처리하기

  • 우측값 레퍼런스 (rvalue 참조)
    • 좌측값(lvalue) : 이름과 주소를 가진 대상
    • 우측값(rvalue) : 좌측값이 아닌 모든 대상
    • 우측값 레퍼런스 매개변수는 타입이 우측값 레퍼런스여도 매개변수 자체는 이름이 있기 때문에 좌측값이라는 점에 주의한다.
      void handleMessage(std::string& msg) {...}
      void handleMessage(std::string&& msg) {
          helper(std::move(msg));
          // msg도 이름이 있으므로 좌즉값(lvalue)임에 주의!
      }
      std::string a = "Hello";
      std::string b = "World";
      
      handleMessage(a);  // handleMessage(std::string& msg); 호출
      handleMessage(a + b);  // haneleMessage(std::string&& msg); 호출 
      handleMessage("Hello World");  // handleMessage(std::string&& msg); 호출
      handleMessage(std::move(b));  // handleMessage(std::string&& msg); 호출
      
  • 이동 의미론 구현
    • 클래스에 이동 의미론을 추가하려면 이동 생성자와 이동 대입 연산자를 구현해야 한다.
    • 이 때 이동 생성자와 이동 대입 연산자를 noexcept로 지정해서 컴파일러에게 익셉션이 절대로 발생하지 않는다고 알려주자. (표준 라이브러리와의 호환성)
  • 5의 규칙 클래스에 동적 할당 메모리를 사용하는 코드를 작성했다면 소멸자, 복제 생성자, 이동 생성자, 복제 대입 연산자, 이동 대입 연산자를 반드시 구현한다.
  • 0의 규칙 다섯 가지 특수 멤버 함수를 구현할 필요가 없도록 클래스를 디자인 한다.
    • 즉, 표준 라이브러리와 컨테이너를 사용하자.

9.3 메서드 종류

static 메서드

  • static 메서드는 특정 객체에 의해 호출되지 않기 때문에 this 포인터를 가질 수 없으며 어떤 객체의 non-static 멤버에 접근하는 용도로 호출할 수 없다.
  • 기본적으로 일반 함수와 비슷하지만 private static, private protected 멤버만 접근할 수 있다.
  • 같은 타입의 객체를 포인터나 레퍼런스로 전달하는 방법 등을 사용해 그 객체를 static에서 볼 수 있도록 했다면 non-static private or protected 멤버에 접근할 수 있다.

const 메서드

  • static 메서드는 인스턴스에 속하지 않아 객체 내부의 값을 변경할 수 없으므로 const가 의미가 없다.
  • const 객체는 const 메서드만 호출할 수 있다. (소멸자 제외)

메서드 오버로딩

  • const_cast() 패턴
    • const 버전과 non-const 버전의 구현 코드가 똑같을 때 코드 중복을 위한 패턴. (호출 방식을 통일할 수 있다.)
    • const 버전은 예전 대로 구현하고, non-const 버전은 const 버전을 적절히 캐스팅 하여 호출한다.

      const SpreadSheetCell& SpreadSheet::getCellAt(size_t x, size_t y) const {
          verifyCoordinate(x, y);
          return mCells[x][y];
      }
      
      SpreadSheetCell& SpreadSheet::getCellAt(size_t x, size_t y) {
          return const_cast<SpreadSheetCell&>(std::as_const(*this).getCellAt(x, y));
          // return const_cast<SpreadSheetCell&>(static_cast<SpreadSheetCell&>(*this).getCellAt(x,y));
      }
      

인라인 메서드

디폴트 인수

9.4 데이터 멤버의 종류

static 데이터 멤버

  • C++17 부터 static 데이터 멤버를 inline으로 선언할 수 있다. 그러면 소스 파일에 공간을 따로 할당하지 않아도 된다.

    class SpreadSheet {
        private:
          static inline size_t sCounter = 0;
    };
    
  • 클래스 메서드 안에서는 static 데이터 멤버를 마치 일반 데이터 멤버인 것처럼 사용한다.
  • static 데이터 멤버에 대해 public으로 선언하면 클래스 메서드 밖에서 접근할 수 있다. (바람직하진 않다.)

const static 데이터 멤버

레퍼런스 데이터 멤버

const 레퍼런스 데이터 멤버

9.5 중첩 클래스

9.6 클래스에 열거 타입 정의하기

9.7 연산자 오버로딩

SpreadSheetCell의 덧셈 구현

  • SpreadSheetCell 객체에 다른 SpreadSheetCell 객체를 더해 제 3의 셀을 결과로 얻고, 기존 셀은 변하지 않도록 한다.
  • add 메서드
  • operator+ 오버로딩으로 구현하기
  • operator+ 전역함수로 구현하기
    • 교환법칙 성립하게 만들 수 있다.

      SpreadSheetCell operator+(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs) {
          return SpreadSheetCell(lhs.getValue() + rhs.getValue());
      }
      
  • 묵시적 변환은 explicit 키워드로 막을 수 있다.

산술 연산자 오버로딩

비교 연산자 오버로딩

9.8 안정적인 인터페이스 만들기

  • 작성할 클래스마다 인터페이스 클래스구현 클래스를 따로 정의한다.
    • 핌플 이디엄 (브릿지 패턴)