[전문가를 위한 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 객체가 스택에 할당 되었을 때의 메모리 상태
소멸자로 메모리 해제하기
- 객체 안에서 동적으로 할당한 메모리는 그 객체의 소멸자에서 해제하는 것이 바람직하다.
- 소멸자는 익셉션이 발생하지 않기 때문에 기본적으로 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 메서드 호출 시
- printSpreadSheet 메서드 종료 (댕글링 포인터)
- 대입 연산과 메모리 누수
- 얕은 복제와 댕글링 포인터
-
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 안정적인 인터페이스 만들기
- 작성할 클래스마다 인터페이스 클래스와 구현 클래스를 따로 정의한다.
- 핌플 이디엄 (브릿지 패턴)