[전문가를 위한 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) { }