[전문가를 위한 C++] 8장. 클래스와 객체 숙달하기

Chapter 8. 클래스와 객체 숙달하기

8.1 스프레드시트 예제

8.2 클래스 작성 방법

클래스 정의

  • 클래스 내부의 멤버 이니셜라이저
    • 클래스를 정의할 때는 멤버 변수를 선언과 동시에 초기화 할 수 있다.

      class SpreadSheetCell {
        public:
          int mValue = 0;
      };
      

메서드 정의 방법

  • std::string_view == const std::string&

객체 사용법

8.3 객체의 라이프 사이클

객체 생성

  • 객체가 생성되면 그 안에 담긴 객체들도 함께 생성된다.
  • 변수를 선언할 때 초깃값을 설정하듯 객체도 선언과 동시에 초깃값을 설정하는 것이 좋다.
  • 이 작업은 생성자라 부르는 특수한 메서드에서 처리할 수 있다.
  • 생성자 사용법

    class SpreadSheetCell {
      public:
        SpreadSheetCell(double initValue);
    };
    
    // 스택 
    SpreadSheetCell myCell(5), anotherCell(4);
    
    // 힙
    auto mySmartCell = make_unique<SpreadSheetCell>(5);  // samrt pointer
      
    SpreadSheetCell* myHeapCell = new SpreadSheetCell(4);  // pointer
      
    SpreadSheetCell* myHeapCell2 = nullptr;  // 곧바로 초기화 하지 않을 경우, 이렇게 nullptr로 초기화 하자.
    myHeapCell2 = new SpreadSheetCell(3);
    
    delete myHeapCell; myHeapCell = nullptr;
    delete myHeapCell2; myHeapCell2 = nullptr;
    
  • 디폴트 생성자
    • 아무런 인수도 받지 않는 생성자이다. 직접 값을 지정하지 않고도 데이터 멤버를 초기화 할 수 있다.
    • 디폴트 생성자가 필요한 경우
      • 객체 배열을 생성할 때는 클래스에 디폴트 생성자를 정의하는 것이 대체롤 편하다.
      • std::vector와 같은 표준 라이브러리 컨테이너에 저장하려면 반드시 디폴트 생성자를 정의한다.
      • 어떤 클래스의 객체를 다른 클래스에서 생성할 때도 디폴트 생성자가 있으면 편하다. (feat. 생성자 이니셜라이저)
    • 디폴트 생성자 작성 방법
      • 스택 객체의 다른 생성자와 달리 디폴트 생성자는 함수 호출 형식을 따르지 않는다. 따라서, 스택 객체를 생성할 때는 디폴트 생성자 이름 뒤에 소괄호를 생략하자.

        SpreadSheetCell::SpreadSheetCell() {
          initValue = 0;
        }
        
        SpreadSheetCell myCell;
        // SpreadSheetCell myCell(); 이 아니다!
        
      • 힙 객체의 디폴트 생성자를 호출하는 방법도 어렵지 않다.

        // smart pointer
        SpreadSheetCell* myHeapCell = make_unique<SpreadSheetCell>();
              
        
        // pointer
        SpreadSheetCell* myHeapCell2 = new SpreadSheetCell();
        // 다음과 같이 작성해도 된다.
        // SpreadSheetCell* myHeapCell2 = new SpreadSheetCell;
        delete myHeapCell2; 
        myHeapCell2 = nullptr;
        
    • 컴파일러에서 생성한 디폴트 생성자
      • 컴파일러에서 생성한 디폴트 생성자는 해당 클래스의 객체 멤버에 대해서도 디폴트 생성자를 호출하지만, int나 double 같은 기본 타입에 대해서는 초기화 하지 않는다.
      • 디폴트 생성자나 다른 생성자를 하나라도 정의하면 컴파일러는 생성자를 자동으로 만들어 주지 않는다.
    • 명시적 디폴트 생성자
    • 명시적으로 삭제된 생성자
      • 정적(static)메서드로만 구성된 클래스를 정의하면 생성자를 작성할 필요가 없을 뿐더러 컴파일러가 디폴트 생성자를 만들면 안 된다.
  • 생성자 이니셜라이저
    • 클래스 데이터 멤버에 대해 디폴트 생성자가 정의돼 있다면 생성자 이니셜라이저에서 이 객체를 명시적으로 초기화 하지 않아도 된다.
    • 반면 클래스에 있는 데이터 멤버에 대해 디폴트 생성자가 정의돼 있지 않다면 생성자 이니셜라이저를 사용해 그 객체를 적절히 초기화 해야 한다.

      class SpreadSheetCell {
        public:
          // double 값을 받는 명시적 생성자만 있을 뿐 디폴트 생성자가 없다.
          SpreadSheetCell(double d);
      };
      
      class SomeClass {
        public:
          SomeClass();
        private:
          SpreadSheetCell mCell;
      };
      
      // 컴파일 에러! mCell에 대해 디폴트 생성자가 없기 때문에 컴파일러는 mCell을 초기화할 방법이 없다.
      SomeClass::SomeClass() { } 
      
      // mCell을 초기화 하려면 다음과 같이 생성자 이니셜라이저를 작성해야 한다.
      SomeClass::SomeClass() : mCell(1.0) { }
      
    • 반드시 생성자 이니셜라이저나 클래스 내부 생성자 구문으로 초기화 해야 하는 경우
      • const 데이터 멤버
      • 레퍼런스 데이터 멤버
      • 디폴트 생성자가 정의되지 않은 객체 데이터 멤버
        • C++에서는 객체 멤버를 디폴트 생성자로 초기화 한다. 디폴트 생성자가 없으면 이 객체를 초기화할 수 없다.
      • 디폴트 생성자가 없는 베이스 클래스 (feat. 10장)
    • 생성자 이니셜라이저는 클래스 정의에 선언된 순서대로 데이터 멤버를 초기화 한다. 생성자 이니셜라이저에 나온 순서가 아님에 주의.
  • 복사 생성자
    • 함수에 인수를 전달할 때 기본적으로 값으로 전달된다. (값이나 객체의 복사본을 받는다.)
    • 함수에서 객체를 값으로 리턴할 때도 복사 생성자가 호출된다.
    • 다른 객체를 똑같이 복사하는 경우 복사 생성자를 명시적으로 호출할 수도 있다.
    • 성능을 높이려면 객체를 값이 아닌 const 레퍼런스로 전달하는 것이 가장 좋다.
  • 이니셜라이저 리스트 생성자
    • std::initializer_list<T>를 첫 번째 매개변수로 받고, 다른 매개변수는 없거나 디폴트값을 가진 매개변수를 추가로 받는 생성자를 말한다.
    • <initializer_list> 헤더를 인클루드.
    • 표준 라이브러리에 나온 클래스 모두 이니셜라이저 리스트 생성자를 지원한다.

      std::vector<std::string> myVec{"String 1", "String 2", "String 3"};
      
  • 위임 생성자
    • 반드시 생성자 이니셜라이저에서 호출해야 한다.

      // string_view 타입 생성자(위임 생성자)가 호출되면 이를 타깃 생성자(double타입 생성자)에 위임한다.
      // 타깃 생성자가 리턴하면 위임 생성자 코드가 실행된다.
      SpreadSheetCell::SpreadSheetCell(string_view initValue) 
        : SpreadSheetCell(stringToDouble(initValue)) {
      }
      
  • 컴파일러가 생성하는 생성자에 대한 정리
    • 복제 생성자를 명시적으로 정의하지 않는 한 컴파일러는 무조건 복사 생성자를 만든다.
    • 어떤 생성자라도 정의했다면 컴파일러는 디폴트 생성자를 만들지 않는다.

객체 소멸

객체에 대입하기 (복제 대입 연산자)

  • 이미 할당된 객체를 덮어 쓸 때는 대입.
  • 대입 연산자 선언 방법

    SpreadSheetCell& operator=(const SpreadSheetCell& rhs);
    
  • 대입 연산자 정의 방법

     SpreadSheetCell& SpreadSheetCell::operator=(const SpreadSheetCell& rhs) {
       if (this == rhs) { }
       mValue = rhs.mValue;
       return *this;
     }
    

컴파일러가 만들어주는 복제 생성자와 복제 대입 연산자

복제와 대입 구분하기

  • 때로는 객체를 복제 생성자로 초기화할지 아니면 대입 연산자로 대입할지 구분이 힘들 때가 있다.
  • 선언처럼 생겼다면 복제 생성자를 사용하고, 대입문처럼 생겼다면 대입 연산자로 처리한다.
    • 복제 생성자를 이용해 객체를 생성

      SpreadSheetCell myCell(5);
      SpreadSheetCell anotherCell(myCell); 
      SpreadSheetCell aThirdCell = myCell;
      
    • 대입 연산자가 호출

      SpreadSheetCell myCell(5);
      SpreadSheetCell anotherCell(myCell);
      anotherCell = myCell;  // 여기서 operator= 호출한다.
      
  • 리턴값이 객체인 경우

    string SpreadSheetCell::getString() const {
      return doubleToString(mValue);  // 1) 이름 없는 임시 string 객체 생성
    }
    
    SpreadSheetCell myCell(5);
    string s1;
    s1 = myCell.getString();  // 2) s1의 대입 연산자 호출. 연산자 매개변수는 1)의 임시객체.
    // 3) 임시 객체 삭제
    
  • 생성자에서 대입 연산자를 호출할 때와 복사 생성자를 호출할 때 차이점
    • 어떤 객체가 다른 객체를 담고 있다면 컴파일러에서 만들어준 복제 생성자는 객체에 담긴 복제 생성자를 재귀적으로 호출한다.
    • 복제 생성자를 직접 정의했다면 생성자 이니셜라이저를 이용할 수 있다. 이 때 생성자 이니셜라이저에서 데이터 멤버를 생략하면 생성자 본문에 작성된 코드를 실행하기 전에 컴파일러가 그 멤버에 대한 초기화 작업을 처리해 준다.
    • 따라서 생성자 본문을 실행할 시점에는 데이터 멤버가 모두 초기화 된 상태다.

      // mValue는 복제 생성자가 아닌 대입 연산자가 적용된다.
      SpreadSheetCell::SpreadSheetCell(const SpreadSheetCell& src) {
        mValue = src.mValue;  // std::string mValue;
      }
      
      // mValue는 복제 생성자에 의해 초기화 된다.
      SpreadSheetCell::SpreadSheetCell(const SpreadSheetCell& src) : mValue(src.mValue) { }