
C++ 고수에 한걸음 나아가기 위해 오랜만에 다시한번 Effective C++을 꺼냈다.
본 포스팅은 전부 개인 공부를 위한 학습 내용을 정리한 것 입니다.
- C++은 은 여러 언어들의 연합체이다.
- #define -> const, enum, inline(함수)로 대체해보자
- 오류시 컴파일러를 통해 빠르게 해결할 수 있다.
- const는 변경하면 안되는 부분에 어디든 붙이자. 그럼에도 불구하고 변경해야 한다면 mutable 키워드를 사용하자
- const 멤버 함수의 오버로딩을 통해 비상수 멤버 함수와 상수 멤버 함수를 구현하게 된다면, 코드의 중복을 피하기 위해 비상수 멤버 함수가 상수 멤버 함수를 호출하게 구현하자
- 대입보다 멤버 초기화 리스트 사용하자
- 비지역 정적 객체를 지역 정적 객체로 변경해서 사용하자(유의하자)
- 다중스레드시 지역 정적 객체의 초기화를 스레드에 맡기지말고 스레드 실행전에 미리 손으로 호출하자(경쟁상태를 막을 수 있다)
예를 들어보자,
C 스타일로만 사용하고 있으면,
기본제공 타입에 대해서는
"값 전달이 참조 전달보다 대개 효율이 더 좋다"라는
규칙이 통하지만, C++의 사용자 정의 생성자/ 소멸자
개념이 생기면서 참조에 의한 전달 방식이 더 효율적이다.
그러나 STL로 오게되면, 반복자와 함수 객체가
C의 포인터를 본떠 만든 것이라는 점을 알게 되고,
다시 값 전달 규칙이 힘을 발휘
#define ASPECT_RATION 1.653
const double AspectRation = 1.653;
만약 #define을 상수로 교체하려면 2가지만 조심하자
1. 상수 포인터(constant pointer)를 정의하는 경우
- 상수 정의는 대개 헤더 파일에 넣는 것이 상례(다른 소스 파일이 이것을 인클루드해서 사용) 포인터는 꼭 const로 선언해주어야 하며, 아울러 포인터가 가리키는 대상까지 const로 선언하는 것이 보통이다.
const char* const authorName = "Scott Meyers";
혹은
const std::string authorName("Scott Meyers");
(책에서는 char* 기반의 문자열을 구닥다리라고 표현한다. ㅋㅋ)
class GamePlayer {
private:
static const int NumTurns = 5; // 상수 선언 (정의X)
int scores[NumTurns];
...
}원래는 '정의'가 있어야 하는게 보통이다. 하지만 정적 멤버로 만들어지는 정수류 타입의 클래스 내부 상수는 예외이다. 주소를 취하지 않는한, 문제가 없으나 클래스 상수의 주소를 구한다거나 컴파일러가 '잘못 만들어진 관계'로 정의를 달라고 하면 제공해야한다.const int GamePlayer::NumTurns; // 정의클래스 상수의 정의는 구현 파일에 둔다. 정의에는 상수의 초기값이 있으면 안 되는데, 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어짐
근데 대체로 나는 정의에서 초기화를 해주었던 것 같다.
더 읽어보면 "조금 오래된 컴파일러는 위의 문법을 받아들이지 않는 경우가 종종 있습니다. 이유는 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단"
class ConstEstimate {
private:
static const double FudgeFactor;
...
}
.cpp
const double cConstEstimate::FudgeFactor = 1.35;
}
-> 주로 내가 상수를 정의할때도 많이 쓰는 방식이다.
const 상수와 다르게 상수의 주소를 넘겨주지 않고 해당 상수의 값을 사용하고자 할 때 쓰이는 방식
배열의 크기를 상수로 넣었을때, 컴파일 단계에서 구식 컴파일러가 읽지 못하여 에러를 뱉을경우 대처하는 처세술
class GamePlayer
{
private:
enum{NumTurns = 5};
int scores[NumTurns];
};
또한 매크로 함수를 사용한 아래 예시를 보자
#define CALL_WITH_MAX(1,b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a가 두번 증가
CALL_WITH_MAX(++a, b+10); //a가 한번 증가
const 키워드가 붙은 객체는 외부 변경을 불가능하게 한다.
const는 팔방미인이다.
char greeting[] = "Hello";
char *p = greeting; // 비상수 포인터, 비상수 데이터
const char *p = greeting; // 비상수 포인터, 상수 데이터
char* const p = greeting; // 상수 포인터, 비상수 데이터
const char * const p = greeting; // 상수 포인터, 상수 데이터
이 부분 중요하다 매번 내가 헷갈린다
const 키워드가 *표의 왼쪽에 있으면 포인터가 가리키는 대상
const 키워드가 *표의 오른쪽에 있으면 포인터 자체가 상수
즉,
void f1(const widget * pw);
void f1(widget const * pw);
는 같다.
| const | 설명 | 형태 | 예시 |
|---|---|---|---|
| 상수포인터 | 포인터 자체가 상수 즉, 포인터 변경X | const * | const std::vector::iterator iter = vec.begin(); ++iter; //error *iter = 10; //OK |
| 상수데이터 | 데이터가 자체가 상수 즉, 데이터 변경X | * const | std::vector::const_iterator cIter = vec.begin(); *cIter = 10 // error ++cIter; // OK |
이 외에도 함수 반환값, 각각의 매개변수, 멤버 함수 앞에, 함수 전체에 대해 붙일 수도 있다.
중요한 이유
1. 클래스의 인터페이스를 이해하기 좋게 하기 위함
2. 이 키워드를 통해 상수 객체를 사용할 수 있게 하자
여기서 중요한 성질이 하나 나온다.
const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다
const char& operator[] (std::size_t position) const
{ return text[position]; } // 상수 객체에 대한 operator
char& operator[] (std::size_t position)
{ return text[position];} // 비상수 객체에 대한 operator
책에서도 나오지만 const 키워드가 있고 없고 차이만 있는 멤버 함수들은 오버로딩이 가능한 것을 나도 잘 모르고 지나쳤던 것 같다. 꼭 기억하자!
// 제대로 const 하지 않는 경우 예시
char& operator[] (std::size_t position) const
{ return pText[position]; } // 틀린것이다.
const CTextBlock cctb("Hello"); // 상수 객체 선언
char *pc = &cctb[0]; // 상수 버전의 operator[]를 호출하여
// 내부 데이터에 대한 포인터를 얻음
*pc = 'J'; // cctb는 이제 "Jello"
class CTextBlock{
public:
...
// mutable을 붙이면 대입 가능
mutable std::size_t textlength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid)
{
// 상수 멤버 함수 안에서는 대입 불가
textlength = std::strlen(pText); // error
lengthIsValid = true; // error
}
return lengthIsValid;
}
이럴때 mutable을 사용한다.
이게 여기서 나왔구나, 프로젝트하면서 처음 봤었는데 비트수준 상수성을 해결하기 위한 방안으로 나온건 몰랐다..! 알면 알수록 신기한 C++
나는 이러한 문제들을 별도의 멤버 함수(물론 나도 private)에 옮겨두고 양 버전에서 호출하면 괜찮을 것이라고 생각했다. 아니나 다를까 책에서 날 꿰뚫는듯 그래도 중복코드는 여전하고 이렇게 해도 함수 호출은 2번되며 return문 또한 중복 코드이다.라고 말한다...
// 이전과 동일
const char& operator[](std::size_t position) const
{
...
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>(
static_cast<const TextBlock&>(*this)[position]);
}
즉, 정리하면 비상수 operator가 상수버전을 호출하는데 static_cast로 const를 붙여준다. 이는 상수 버전을 호출하게 된다. 그리고 const를 떼어낸다.(반환타입에 캐스팅을 적용)
-> 참고 : const를 붙이는 캐스팅은 안전한 타입 변환(비상수 객체에서 상수 객체로 바꾸는)을 강제로 진행하는 것뿐이기 때문에 static_cast만 써도 딱 맞다.
class ABEntry{
int a;
int b;
int c;
};
ABEntry::ABEntry(int& _a, int& _b)
{
a = _a;
b = _b;
c = 0;
}
위와 같은 방법은 "대입"이다. 기본 생성자 호출 후 복사 대입 연산자를 호출하는 방법이다.
C++ 규칙에 의하면 어떤 객체이든 그 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에! 초기화되어야 한다고 명시
그럼 초기화는 무엇인가? 우리가 흔히 아는 멤버 초기화 리스트이다.
ABEntry::ABEntry(int& _a, int& _b)
:a(_a),b(_b),c(0)
{}
전부 복사 생성자에 의해 초기화된다.
매개변수 없는 생성자가 있다면, 생성자에 아무런 인자도 주지 않으면 된다.
ABEntry::ABEntry()
:a(),b(),c()
{}
나는 이때까지 멤버 초기화 리스트 순서인줄 알았다.. 다들 순서대로 맞추길래 그렇게 했는데, 이런 이유가!
추가적으로 함수 안에 있는 정적 객체는 지역 정적 객체, 그외에는 비지역 정적 객체라고 한다.
class FileSystem{
public:
...
};
extern FileSystem tsf;
class Directory{
public:
Directory( params );
...
}
Directory::Directory( parmas )
{
std::size_t disks = tfs.namDisks();
}
싱글톤이 이러한 문제에서 해결법으로 나왔구나. 너무 자주 사용해서 익숙했다.