1. C++를 언어들의 연합체로 바라보는 안목은 필수
C++이 단일 언어가 아닌 여러개의 하위 언어를 제공한다고 생각하자.
◾ C
◾ 객체 지향 개념의 C++
◾ 템플릿 C++
◾ STL
◾ C++을 사용한 효과적인 프로그래밍 규칙은 경우에 따라 달라집니다. 그 경우란, 바로 C++의 어떤 부분을 사용하느냐입니다.
2. #define을 쓰려거든 const, enum, inline을 떠올리자
#define을 상수로 교체할 때 상수 포인터를 정의하는 경우와 클래스 멤버를 상수로 정의하는 경우를 유의해야한다.
우선 클래스 멤버를 #define으로 정의하는 것 자체가 불가능 하다는 것을 알아야한다.
클래스 멤버를 상수로 정의하는 경우, 상수의 유효 범위를 클래스로 한정짓고자 한다면 해당 상수를 멤버 변수로 만들어야한다. 다만 사본 개수가 한 개를 넘지 못하게 하려면 정적 멤버로 만들어주어야 한다.
class GamePlayer
{
private:
static const int NumTurns = 5; // 구식 컴파일러에서는 오류 발생
int scores[NumTurns];
};
구식 컴파일러에서는 나열자 둔갑술을 대신 사용할 수 있다.
class GamePlayer
{
private:
enum { NumTurns = 5 };
int scores[NumTurns];
};
◾ 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각합시다.
◾ 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각합시다.
3. 낌새만 보이면 const를 들이대 보자!
const가 * 왼쪽에 있으면 포인터가 가리키는 대상이 상수, 오른쪽에 있으면 포인터 자체가 상수이다
void f1(const Widget *pw);
void f2(Widget const *pw); // 둘 다 같은 의미
매개변수, 지역 객체를 수정할 수 없게 하는 것이 목적이라면 const를 반드시 붙여주도록 한다. 실수 발생시 컴파일 에러 발생으로 오류를 쉽게 잡을 수 있다.
◾ 상수 멤버 함수
객체를 reference-to-const로 전달하면 실행 성능을 높일 수 있다. (상수 멤버 함수 필요)
또한 const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다.
const char& operator[](std::size_t position) const // 상수 객체
char& operator[](std::size_t position) // 비상수 객체
어떤 멤버 함수가 상수 멤버라는것은 비트수준(물리적) 상수성, 논리적 상수성을 만족한다는 의미이다.
하지만 상수 멤버 함수는 물리적 상수성은 만족하지만 논리적 상수성은 만족하지 못한다.
class CTextBlock {
public:
char& operator[](std::size_t position) const
{ return pText[position]; }
private:
char *pText;
};
...
const CTextBlock cctb("Hello"); // 상수 객체 선언
char *pc = &cctb[0]; // 상수 버전 연산자 호출
*pc = 'J'; // 값이 바뀐다..?
컴파일러 입장에서는 비트수준 상수성을 만족하기 때문에 컴파일 오류 없이 정상 동작한다.
우리는 추가적으로 논리적 상수성을 지켜서 프로그래밍 해야 한다.
◾ 상수 멤버 및 비상수 멤버 함수에서 코드 중복을 피하는 방법
두 멤버 함수가 상수/비상수 차이만 있을 뿐 로직과 기능이 동일하다면 비상수 버전이 상수 버전을 호출하도록 구현하면 코드 중복을 피할 수 있다.
class TextBlock{
public:
const char& operator[](std::size_t position) const
{
...
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>( // 캐스팅으로 const 제거
static_cast<const TextBlock&> // *this 타입에 const 추가
(*this)[position]; // 상수 버전 호출
);
}
};
두 번의 캐스팅 없이 그냥 operator[]를 호출하면 자기 자신(비상수)을 재귀적으로 무한하게 호출한다.
그래서 *this를 const 캐스팅 시켜서 상수 버전을 호출하고 다시 캐스팅을 통해 const를 떼버린다.
반대로 상수 버전에서 비상수 버전을 호출하는것은 상수 멤버가 깨지기 때문에 불가능하다.
◾ const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.
◾ 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인(논리적인) 상수성을 사용해서 프로그래밍해야 합니다.
◾ 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요.
4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자
대입과 초기화를 헷갈려서는 안된다.
대입의 경우 기본 생성자-복사 생성자 순으로 이뤄지지만 초기화는 인자가 직접 생성자의 인자로 쓰이기 때문에 호출 비용이 줄어든다.
데이터 멤버를 기본 생성자로 초기화 하는 경우에도 초기화 리스트를 사용하는 것이 좋다.
기본제공 타입의 멤버가 상수나 참조자인 경우에는 반드시 초기화가 이루어져야 한다.
C++은 기본 클래스가 파생 클래스보다 먼저 초기화되고 클래스 데이터 멤버는 선언된 순서대로 초기화된다.
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.
◾ 정적 객체: 함수 내부의 정적 객체를 제외한 나머지
◾ 번역 단위: obj 파일을 만드는 기반 소스 파일
서로 다른 번역 단위에 정의된 비지역 정적 객체들의 상대적인 초기화 순서는 정해져 있지 않다.
초기화 순서를 정하고싶다면 비지역 정적 객체를 하나씩 맡는 함수를 만들어서 지역 정적 객체로 바꾸면 된다. (≒싱글톤)
class FileSystem { ... };
FileSystem& tfs()
{
static FileSystem fs; // 지역 정적 객체 정의/초기화
return fs; // 참조자 반환
}
다중스레드 시스템에서 비상수 정적 객체(지역/비지역)는 문제가 발생할 수 있다.
참조자 반환 함수를 모두 호출해주면 초기화에 관계된 경쟁 상태가 발생하지 않는다.
◾ 기본제공 타입의 객체는 직접 손으로 초기화합니다. 경우에 따라 저절로 되기도 하고 안되기도 하기 때문입니다.
◾ 생성자에서는, 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용합시다. 그리고 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열합시다.
◾ 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 합니다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 됩니다.
'도서 > Effective C++' 카테고리의 다른 글
6. 상속, 그리고 객체 지향 설계 (0) | 2022.12.04 |
---|---|
5. 구현 (0) | 2022.12.03 |
4. 설계 및 선언 (0) | 2022.12.01 |
3. 자원 관리 (0) | 2022.11.30 |
2. 생성자, 소멸자 및 대입 연산자 (0) | 2022.11.30 |