[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;
}
```
댓글
댓글 쓰기