이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 51p ~ 60p
요약
1. C++에 왔으면 C++의 법을 따릅시다.
항목 1: C++를 언어들의 연합체로 바라보는 안목은 필수
- 초창기의 C++는 단순히 C 언어에 객체 지향 기능 몇 가지가 결합된 형태였다.
- 이후 C++는 꾸준한 성장을 거쳐, 단순하게 객체 지향의 개념이 추가된 C라고 부를 수 없을 정도로 다양한 개념을 지원하게 됐다.
- 따라서 C++는 다중패러다임 언어라고 부를 수 있으며, 절차적 프로그래밍을 기본으로 객체 지향, 함수식, 일반화 프로그래밍을 포함하여 메타 프로그래밍 개념까지 지원하고 있다.
- C++는 다음과 같이 4개의 하위 언어로 구성되어 있다고 생각해야 한다.
C
- C++의 가장 기본적인 베이스는 C이다.
- 블록, 문장, 선행 처리자, 기본제공 데이터타입, 배열, 포인트 등 모든 것이 C에서 왔다.
객체 지향
- 클래스를 사용하는 C에 대한 모든 개념이 해당된다.
- 클래스(생성자, 소멸자), 캡슐화, 상속, 다형성, 가상 함수(동적 바인딩) 등이 모두 포함되며, 객체 지향 설계의 규칙들 대부분이 그대로 들어맞는다.
템플릿
- C++에 있는 일반화 프로그래밍 부분이다.
- 템플릿 프로그래밍을 기반으로 템플릿 메타프로그래밍이라는 새로운 패러다임도 파생되었다.
STL
- 표준 템플릿 라이브러리로, 컨테이너, 반복자, 알고리즘, 함수 객체가 복잡하게 얽혀있다.
- STL을 사용하려면 규약을 따라야 한다.
항목 2: #define
을 쓰려거든 const, enum, inline을 떠올리자
- 핵심은
#define
과 같은 선행 처리자보다 컴파일러를 더 가까이 하자는 것이다.
2.1 const 사용하기
#define ASPECT_RATIO 1.653
- 위와 같은 코드는 기호식 이름으로 보이지만 컴파일러에게는 보이지 않는다.
- 그 이유는 위 코드가 컴파일러로 넘어가기 전에 선행 처리자가 숫자 상수로 바꾸어 버리기 때문이다.
- 그 결과
ASPECT_RATIO
는 컴파일러가 사용하는 기호 테이블에 들어가지 않게 된다. - 이는 숫자 상수로 대체된 코드에서 컴파일 에러가 발생하면 문제가 될 수 있는 여지를 준다.
- 이를 해결하기 위해 매크로 대신 상수를 사용한다.
const double AspectRatio = 1.653;
const
상수는 언어 차원에서 지원하는 타입의 데이터이기 때문에 당연히 컴파일러에게 전달이 가능하며 기호 테이블에도 들어간다.#define
을 상수로 교체하는 경우 다음 두 가지 경우를 주의해야 한다.
// 1. 상수 포인터를 정의하는 경우
// 상수 정의는 대개 헤더 파일에 넣는 것이 컨벤션이다.
// 따라서 포인터는 반드시 const로 선언해야 하고, 포인터가 가리키는 대상까지 const로 선언해야 한다.
// 그래야지 포인터 변수와 포인터 변수가 가리키는 값까지 상수로 사용할 수 있다.
const char * const authorName = "Scott Meyers";
// 문자열을 상수를 사용하는 더 좋은 방법은 string을 사용하는 것이다.
const std::string authorName("Scott Meyers");
// 2. 클래스 멤버로 상수를 정의하는 경우(클래스 상수)
// 어떤 상수의 유효범위를 클래스로 한정하고자 할 때는 그 상수를 멤버로 만들어야 한다.
// 이때, 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 static 멤버로 만들어야 한다.
class GamePlayer {
private:
static const int NumTurns = 5;
int scores[NumTurns];
}
- 두 번째 케이스에서
NumTurns
는 선언된 것이다.(정의가 아니다.)- C++에서는 사용하고자 하는 것에 대한 정의가 있어야 하는데, 정적 멤버로 만들어지는 정수류(각종 정수 타입, char, bool 등) 타입의 클래스 내부 상수는 예외이다.
- 단, 클래스 상수의 주소를 구하거나, 컴파일러가 잘못 만들어진 관계로 정의를 달라고 하는 경우에는 별도의 정의를 제공해야 한다.
// NumTurns 정의 const int GamePlayer::NumTurns;
- 이때 클래스 상수의 정의는 구현 파일에 둔다.(헤더 파일에 두지 않는다.)
- 정의에는 상수의 초기값이 있으면 안된다.
- 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다.( =
NumTurns
는 선언과 동시에 초기화된다.)
- 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다.( =
- 이런 방법이 먹히지 않는 컴파일러는 정의를 할 때 초기값을 주면 된다.
2.2 enum 사용하기
- 마지막 남은 하나의 예외는 특정 클래스를 컴파일하는 시점에 클래스 상수의 값이 필요한 경우이다.
- 정수류 타입의 클래스 내 초기화를 금지하는 컴파일러의 경우 이런 문제를 해결하기 위해 enum을 사용할 수 있다.
- enum 타입의 값은 int가 놓일 곳에 사용할 수 있다는 C++의 진실을 적극 활용하는 방법이다.
class GamePlayer { private: enum { NumTurns = 5 }; int scores[NumTurns]; }
- enum을 사용할 때의 몇 가지 장점이 있다.
- enum의 주소를 취할 수 없기 때문에 선언된 정수 상수를 가지고 다른 사람이 주소를 얻는다든지 참조자를 쓴다든지 하는 것이 싫다면 enum을 사용할 수 있다.
- enum은
#define
과 마찬가지로 어떤 형태의 쓸데없는 메모리 할당을 절대 저지르지 않는다. - 상당히 많은 코드에서 이러한 방식을 사용하고 있기 때문에 눈에 단련시킬 수 있다.(메타 프로그래밍의 핵심 기법이 된다.)
2.3 inline 사용하기
void f(int num) {
//std::cout << num << std::endl;
};
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int main()
{
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);
std::cout << a << std::endl;
CALL_WITH_MAX(++a, b + 10);
std::cout << a << std::endl;
}
- 매크로 함수를 사용하는 것은 다음과 같은 단점을 가지고 있다.
- 매크로를 작성할 때는 매크로 본문에 들어 있는 인자마다 반드시 괄호를 사용해야 한다.
- 매크로 함수 내부에서 비교를 통해 처리한 결과값에 따라 반환되는 결과값도 달라질 수 있다.
- 따라서 매크로 함수 대신 인라인 함수를 사용하자.
template<typename T> inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); }
- 위의 함수는 템플릿이기 때문에 동일 계열 함수군을 만들어낸다.
- 실제 함수를 만드는 것이기 때문에 유효범위 및 접근 규칙을 그대로 따라간다.
항목3: 낌새만 보이면 const를 들이대 보자!
- const를 사용하면 의미적인 제약을 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다는 장점이 있다.
- const는 다양한 곳에서 유용하게 사용할 수 있다.
- 클래스 바깥에서는 전역, 네임스페이스 유효범위의 상수를 선언(정의)하는 데 쓸 수 있다.
- 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있다.
- 클래스 내부의 경우에는 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있다.
- 포인터 자체를 상수로, 혹은 포인터가 가리키는 데이터를 상수로 지정할 수 있다.
// 포인터와 상수 // const 키워드가 *표의 왼쪽에 있으면 포인터가 가리키는 대상이 상수 // const 키워드가 *표의 오른쪽에 있으면 포인터 자체가 상수 using namespace std;`
char greeting\[\] = "Hello";
char text\[\] = "Text";
char\* p = greeting;
cout << p << endl;
p = text;
cout << p << endl;
p\[0\] = 'Z';
cout << p << endl;
const char\* cp = greeting; // 포인터가 가리키는 대상이 상수
cout << cp << endl;
cp = text; // 포인터 cp의 값은 변경 가능(포인터 자체는 상수가 아님)
cout << cp << endl;
// cp\[0\] = 'k'; // 포인터 cp가 가리키는 대상의 값은 변경 불가능(상수 데이터)
char\* const ccp = greeting; // 포인터 자체가 상수
cout << ccp << endl;
// ccp = text; // 포인터 ccp의 값 변경 불가능
ccp\[0\] = 'k';
cout << ccp << endl; // 포인터 ccp가 가리키는 대상의 값은 변경 가능
const char\* const cccp = greeting; // 포인터 자체와, 포인터가 가리키는 값이 상수
cout << cccp << endl;
// cccp = text;
// cccp\[0\] = 'z';
- 따라서 다음 함수의 매개변수는 모두 똑같은 역할을 한다.
- STL의 이터레이터(iterator)는 포인터를 본뜬 것이기 때문에 기본적인 동작 원리가
T*
포인터와 흡사하다.
// const가 *표 왼쪽에 있기 때문에 pw가 가리키는 대상이 상수
void f1(const Widget* pw);
void f2(Widget* const pw);
- const는 함수 선언에 사용할 경우 강력하다.
// 함수에 const 사용
class Rational {};
const Rational operator*(const Rational& lhs, const Rational& rhs) {};
Rational a, b, c;
// ..
//(a \* b) = c; // 상수 결과값에 대입연산자(=) 사용하여 오류발생
// 만약, a, b가 기본 제공타입(int와 같은)이었다면, a와 b의 곱셈으로 만들어진 상수에 대입연산자를 사용하면 당연하게 에러가 날 것이다. // 잘 만든 사용자 지정 타입은 쓸데없는 예외를 만들지 않아 (a\*b)와 같은 실수를 컴파일러에서 알 수 있도록 만들어준다.
- 함수 리턴값에 const를 사용하는 것은 어처구니 없는 실수를 방지할 수 있고, 기본제공 타입과의 쓸데없는 비호환성을 피할 수 있게 된다.
- const 함수 매개변수는 const 타입의 지역 객체와 같은 특성을 가진다.
메모
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(80p ~ 89p) (1) | 2024.01.11 |
---|---|
이펙티브 C++(71p ~ 79p) (1) | 2024.01.10 |
이펙티브 C++(61p ~ 70p) (0) | 2024.01.09 |
이펙티브 C++(35p ~ 50p) (0) | 2024.01.04 |
이펙티브 C++(0p ~ 34p) (1) | 2024.01.03 |