[C++] 스터디 CPPALOM 12주차: Effective C++ Chap 1~2
파워 포인트 파일(.pptx)을 Markdown으로 변환하여 업로드하였음)
# <br>CPPALOM # <br>12주차 – Effective C++ Chap 1~2 한수빈 # <br>항목1: C++를 언어들의 연합체로 바라보는 안목은 필수 * C++은 다중패러다임 프로그래밍 언어라고 불린다. * 다음 네 가지 하위 언어를 제공한다: * C * 객체 지향 개념의 C++: 클래스를 쓰는 C * 템플릿 C++: 일반화 프로그래밍 * STL: 템플릿 라이브러리 # <br>항목2: #define을 쓰려거든 const, enum, inline을 떠올리자 * 상수를 사용하기 위해서 #define을 사용하지 마라. 대신: * 주의점: 다음은 컴파일 오류를 일으킨다. * A::C는 선언만 되어있으며, 정의가 되어있지 않다. * 그러나 min()은 참조를 요구하기 때문에 오류가 난다. ```C++ const char* const authorName = “Scott Meyers”; const std::string authorName(“Scott Meyers”); ``` ```C++ #include <algorithm> class A { public: static const int C = 10; // DECLARATION }; // const int A::C; DEFINITION int main() { std::min(0, A::C); } ``` # <br>enum hack * const 객체에 대해 주소를 취하는 것을 방지할 수 있다. * 많은 코드에서 이 기법이 쓰이고 있으므로 이러한 방식에 대해 익혀 놓아야 한다. * 수빈의 첨언: * 현대 C++에서 단순히 상수를 이용하고 싶을 때에는 constexpr를 권장한다! ```C++ class GamePlayer { private: enum { NumTurns = 5}; int scores[NumTurns]; }; ``` # <br>#define의 위험성 * f가 호출되기 전에 a가 증가하는 횟수가 달라진다. * 위의 경우 2번 증가, 아래의 경우 1번 증가한다. * ++a가 더 크다면, ++a가 f의 인자로 들어가기 때문이다. ```C++ #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) int main(){ int a = 5, b = 0; CALL_WITH_MAX(++a, b); CALL_WITH_MAX(++a, b+10); } ``` # <br>항목3: 낌새만 보이면 const를 들이대 보자! * 소스 코드 수준에서 불변을 결정할 수 있다. * 다음과 같은 참사를 막을 수 있다. * 만약 어떤 변수가 변하지 않는다면, 그냥 const를 붙여라. * const correctness! ```C++ class Rational { ... }; const Rational operator*(const Rational& lhs, const Rational& rhs); …Rational a, b, c; …(a * b) = c; …if (a * b = c) ``` # <br>상수 멤버 함수 * 멤버 함수에도 역시 const를 붙일 수 있다. * const 객체여도 해당 함수들은 호출할 수 있다! * 비트수준 상수성 * 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 ‘const’임을 인정하는 개념이다. * 하지만 멤버 변수가 포인터일 경우, 해당 포인터의 값만 변경하지 않는다면 const가 여전히 인정되므로 이에 주의해야 한다! * 논리수준 상수성 * 클래스 바깥에서 볼 때 해당 객체의 행위가 여전히 동일하다면, 그 멤버 함수가 ‘cosnt’임을 인정하는 개념이다. # <br>mutable * 논리 수준 상수성을 구현 가능하게 만든다. * length()는 분명 멤버 변수를 변경시키지만 이는 내부 구현을 위한 변경일 뿐, 논리적인 객체의 상태를 변경시키는 것은 아니다. * mutable 키워드를 지닌 변수들은 const 멤버 함수에서도 변경될 수 있다. ```C++ class CTextBlock { public: ... std::size_t length() const; private: char *pText; mutable std::size_t textLength; mutable bool lengthIsValid; }; std::size_t CTextBlock::length() const{ if(!lengthIsValid) { textLength = std::strlen(pText); lengthIsValid = true; } return textLength; } ``` # <br>const 멤버 함수 코드 중복의 해결책 * const_cast를 통해서 멋지게 해결할 수 있다. * 반드시 비상수 멤버 함수가 상수 멤버 함수를 호출하는 형태로 구현되어야 한다! * 비상수 멤버 함수는 상수 멤버 함수를 호출할 수 있지만, * 상수 멤버 함수는 비상수 멤버 함수를 호출하면 안 되기 때문이다. ```C++ char& operator[](std::size_t position) { return const_cast<char&>( static_cast<const TextBlock&>(*this)[position] ); } ``` # <br>항목4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자 * 초기화가 될 지 안 될 지 모르니 명시적으로 반드시 초기화를 수행하라! * 대입과 초기화를 구분해서 하자: * 기본 클래스는 파생 클래스보다 먼저 초기화된다. * 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다. * 멤버 초기화 리스트에 순서가 바뀌어 기재되었더라도, 반드시 선언된 순서대로 초기화된다. ```C++ ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) { theName = name; theAddress = address; thePhones = phones; numTimesConsulted = 0; } ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) : theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) { } ``` # <br>정적 객체와 번역 단위 * 정적 객체 * 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체이다. * 전역 객체, 네임스페이스 유효범위에서 정의된 객체, 클래스 안에서 static으로 선언된 객체, 함수 안에서 static으로 선언된 객체, 파일 유효범위에서 static으로 정의된 객체 * 이들 중 함수 안에 있는 정적 객체는 지역 정적 객체, 나머지는 비지역 정적 객체라고 한다. * 번역 단위 * 컴파일을 통해 하나의 목적 파일을 만드는 바탕이 되는 소스 코드이다. 기본적으로 소스 파일 하나이다. * 문제: * 별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 ‘정해져 있지 않다‘. # <br>초기화 순서를 결정하는 법 * Singleton! * FileSystem이 필요할 때마다 tfs()를 호출하여 활용한다. * 단 하나의 객체만을 보장하게 되며, 초기화 역시 사용 이전에 반드시 수행되게 된다! ```C++ FileSystem& tfs() { static FileSystem fs; return fs; } ``` # <br>항목5: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 * 자동으로 선언해주는 함수들이 있다: * (생성자) * 복사 생성자 * 복사 대입 연산자 * 소멸자 * 이들에 대해서 항상 주의하자! * 혹 기본 함수들을 활용할 수 있더라도, default 키워드를 이용해서 직접 선언하자. ```C++ CTextBlock() = default; CTextBlock(const CTextBlock& rhs) = default; ~CTextBlock() = default; CTextBlock& operator=(const CTextBlock& rhs) = default; ``` # <br>항목6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 private으로 해당 함수들을 비공개하자 복사 생성자와 대입 연산자를 비공개한 Uncopyable 클래스를 기본 클래스로 활용하는 것도 한 방법이다: ```C++ class HomeForSale { public: private: HomeForSale(const HomeForSale&); HomeForSale& operator=(const HomeForSale&); }; ``` ```C++ class Uncopyable { protected: Uncopyable() = default; ~Uncopyable() = default; private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); }; ``` ```C++ class HomeForSale : private Uncopyable ``` # <br>항목7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 * getTimeKeeper()는 TimeKeeper의 파생 클래스를 반환한다. * 이 경우, ptk의 소멸은 TimeKeeper의 부분만을 소멸시킨다. * 이를 방지하기 위해서 가상 소멸자를 선언할 수 있다. * 가상 소멸자는 남용하면 안 된다! * virtual table을 유지하기 위해서 추가적인 메모리가 소모된다. * 가상 함수가 하나라도 들어 있다면 가상 소멸자를 선언하자. ```C++ TimeKeeper *ptk = getTimeKeeper(); delete ptk; ``` ```C++ class TimeKeeper { public: TimeKeeper(); virtual ~TimeKeeper(); } ``` # <br>순수 가상 소멸자를 이용한 추상 클래스 * 어떤 클래스는 추상 클래스였으면 좋겠는데 마땅히 추가할 순수 가상 함수가 없을 때도 있다. * 순수 가상 소멸자를 선언하자! * 단, 반드시 정의 부분을 추가해주어야 한다. ```C++ class AWOV { public: virtual ~AWOV() = 0; }; AWOV::~AWOV() {} ``` # <br>항목8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 * 소멸자에서 예외가 발생하는 것은 좋지 않다. * vector, list 등의 컨테이너에서 이들을 감당하는 것은 결코 쉽지 않다. * 가령, 자원 관리 클래스들은 소멸자에서 자신이 관리하는 자원들을 소멸시킨다: * 하지만 close() 호출이 예외를 발생시킨다면, 이는 문제가 발생할 수 있다. ```C++ class DBConn { public: ~DBConn() { db.close(); } private: DBConnection db; } ``` # <br>예외를 처리하는 방법 프로그램을 바로 끝낸다. 예외를 삼킨다. 둘 다 좋은 전략은 아니다. ```C++ ~DBConn() { try { db.close() } catch (...) { // log std::abort(); } } ``` ```C++ ~DBConn() { try { db.close() } catch (...) { // log } } ``` * close 호출의 책임을 DBConn의 사용자 역시 질 수 있는 기회를 줘야 한다. * 물론, 사용자는 무시해도 된다. * 중요한 것은, 예외에 대해서 그것을 능동적으로 사용자가 처리할 수 있는 창구를 만들어냈다는 것이다! ```C++ class DBConn { public: void close() { db.close(); closed = true; } ~DBConn() { if (!closed) try { db.close() } catch (...) { // log } } private: DBConnection db; bool closed; } ``` # <br>항목9: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 * Transaction의 생성자에서 가상 함수인 logTransaction()을 호출한다. * 이는 문제가 되는데, Transaction의 생성자 호출 시점에는 파생 클래스인 BuyTransaction의 초기화가 되어 있지 않기 때문이다. * 따라서 가상 함수임에도 불구하고 Transaction::logTransaction()이 호출된다. * 이는 예상치 못한 동작들을 만든다! * 생성자에서 init() 등을 호출해 우회적으로 가상 멤버 함수를 호출하는 경우에 주의하라. ```C++ class Transaction { public: Transaction() { … logTransaction(); } virtual void logTransaction() const = 0; } class BuyTransaction : public Transaction{ public: void logTransaction() const override; }; ``` # <br>항목10: 대입 연산자는 *this의 참조자를 반환하게 하자 * C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있다. * 우측 연관이므로, 다음 두 문장은 같다. * 이는 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있기 때문이다. * 이는 일종의 관례이며, 우리도 지키는 것이 좋다! ```C++ x = y = z = 15; x = (y = (z = 15)); ``` ```C++ class Widget { public: Widget& operator=(const Widget& rhs) { … return *this; } }; ``` # <br>항목11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 자기대입이란 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말한다. 다음 코드는 자기대입에서 문제가 발생한다. ```C++ class Widget { public: Widget &operator=(const Widget &rhs) { delete pb; pb = new Bitmap(*rhs.pb); return *this; } private: Bitmap *pb; }; ``` # <br>자기대입의 해결책 일치성 검사 순서 변경하기 사본 만들기 멋지게 사본 만들기 ```C++ Widget &operator=(const Widget &rhs) { if (this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; } ``` ```C++ Widget &operator=(const Widget &rhs) { Widget temp(rhs); swap(temp); return *this; } ``` ```C++ Widget &operator=(const Widget &rhs) { Bitmap *pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this; } ``` ```C++ Widget &operator=(const Widget rhs) { swap(rhs); return *this; } ``` # <br>항목12: 객체의 모든 부분을 빠짐없이 복사하자 클래스의 멤버 변수를 수정했다면, 반드시 그와 관련한 복사 생성자, 복사 대입 연산자 역시 수정해야 한다. 파생 클래스의 복사 생성자 및 복사 대입 연산자를 구현할 경우, 반드시 부모의 함수 역시 호출해주어야 한다. 복사 생성자와 복사 대입 연산자에서 공통된 부분이 있다면, 함수로 분리해보는 것도 좋다. ```C++ PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), priority(rhs.priority) { } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { Customer::operator=(rhs); priority = rhs.priority; return *this; } ```
댓글
댓글 쓰기