본문 바로가기
프로그래밍/Effective C++

Chapter 1 C++에 왔으면 C++의 법을 따릅시다

by Ohdumak 2018. 3. 19.

Chapter 1 C++에 왔으면 C++의 법을 따릅시다

'가장' 근본적인 것들을 다루고 있는 단원



항목 1: C++를 언어들의 연합체로 바로보는 안목은 필수


초창기의 C++는 단순히 C 언어에 객체 지향 기능 몇 가지가 결합된 형태였다.

오늘날의 C++는 다중패러다임 프로그래밍 언어(multiparadigm programming)라고 불립니다. 절차적(procedural) 프로그래밍을 기본으로 하여 객체 지향(object oriented), 함수식(functional), 일반화(generic) 프로그래밍을 포함하며 메타프로그래밍(metaprogramming) 개념까지 지원하고 있다.


C++를 잘 이해하는 방법

C++를 단일 언어로 바라보는 눈을 넓혀, 상관관계가 있는 여러 언어들의 연합체(federation)로 봐라! 그리고 각 언어에 관한 규칙을 살펴본다.

- C C++는 여전히 C를 기본으로 하고 있다. C만 쏙 뽑아 써도 된다.

- 객체 지향 개념의 C++ '클래스를 쓰는 C'에 관한 것이 모두 해당된다.

- 템플릿 C++ C++의 일반화 프로그래밍 부분. 템플릿이 C++에 끼치는 영향은 크지만 템플릿 메타프로그래밍(templete metaprogramming: TMP)의 세계는 주류 C++ 프로그래밍과 맞닿아 돌아가지 않는 조금 다른 세계이다.

- STL STL은 대단히 특별한 템플릿 라이브러리이다. STL은 나름대로 독특한 사용규약이 있어서, STL을 써서 프로그래밍하려면 그 규약을 따르면 된다.


C++는 한 가지 프로그래밍 규칙 아래 똘똘 뭉친 통합 언어(unified language)가 아니라 네 가지 하위 언어들의 연합체이다.


이것만은 잊지 말자!

- C++를 사용한 효과적인 프로그래밍 규칙은 경우에 따라 달라진다. 그 경우란, C++의 어떤 부분을 사용하느냐이다.



항목 2: #defined을 쓰려거든 const, enum, inline을 떠올리자


"가급적 선행 처리자보다 컴파일러를 더 가까이 하자"


#define은 C++ 언어 자체의 일부가 아닌 것으로 취급될 수 있다.

#define ASPECT_RATIO 1.653

해결법은 매크로 대신 상수를 쓰는 것이다.

const double AspectRatio = 1.653;


#define을 상수로 교체하려면 두 가지 경우만 특별히 조심

문자열 상수을 쓸 때는 char* 문자열보다는 string 객체가 대체적으로 사용하기 괜찮다.

const char * const authorname = "Scott Meyers";

const std::string authorName("Scott Meyers");


두 번째 경우는 클래스 멤버로 상수를 정의하는 경우이다.

어떤 상수의 유효범위를 클래스로 한정하고자 할 때는 그 상수를 멤버로 만들어야 하는데, 그 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 한다.

class gamerPlager {

private:

static const int Numbog = 5; // 상수 선언

int scores[NumTurns];            // 상수를 사용하는 부분

NumTurns는 '선언(declaration)'된 것이다.


나열자 둔갑술(enum hack) 알아야 하는 이유

첫 번째, 동작 방식이 const보다는 #define에 더 가깝다. enum은 #define처럼 어떤 형태의 쓸데없는 메모리 할당을 저지르지 않는다.

두 번째, 상당히 많은 코드에서 이 기법이 쓰이고 있고, 템플릿 메타프로그램밍의 핵심 기법이다.


기존 메크로의 효율을 그대로 유지하고 정규 함수의 모든 동작방식 및 타입 안전성까지 완벽히 취할 수 있는 인라인 함수에 대한 템플릿을 활용하자.


이것만은 잊지 말자!

- 단순한 상수를 쓸 대는, #define보다 const 객체 혹은 enum을 우선 생각하자.

- 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각하자.



항목 3: 낌새만 보이면 const 를 들이대 보자!


const에서 가장 멋지다고 말할 수 있는 부분이 있다면 '의미적인 제약'(const 키워드가 붙은 객체는 외부 변경이 불가능하다)을 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다.

const 키워드는 클래스 바깥에서는 전역 혹은 네임스페이스 유효범위의 상수를 선언(정의)하는 데 쓸 수 있고, 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있다. 클래스 내부는 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있고 포인터 자체를 상수로, 포인터를 가리키는 데이터를 상수로 지정할 수 있다.

char greeting[] = "Hello";

char *p  = greeting;				// 비상수 포인터,
									// 비상수 데이터
const char *p = greeting;			// 비상수 포인터,
									// 상수 데이터
char * const p = greeting;			// 상수 포인터,
									// 비상수 데이터
const char * const p = greeting;	// 상수 포인터,
									// 상수 데이터

const 키워드가 * 표의 왼쪽에 있으면 포인터가 가리키는 대상이 상수

const 가 * 표의 오른쪽에 있으면 포인터 자체가 상수

const 가 * 표의 양쪽에 다 있으면 포인터가 가리키는 대상 및 포인터가 다 상수


포인터가 가리키는 대상을 상수로 만들 때 const를 사용하는 스타일이 다르다. 타입 앞에 const를 붙이기도 하고, 타입의 뒤쪽이자 *표의 앞에 const를 붙이기도 한다.

void f1(const Widget *pw);			// f1은 상수 Widget 객체에 대한
									// 포인터를 매개변수로 취합니다.
void f2(Widget const *pw);			// f2도 동일

두 가지 형태 모두 현업에서 자주 쓰임



STL 반복자(iterator)는 기본적인 동작 원리가 T* 포인터와 흡사하다. 반복자를 const로 선언하는 일은 포인터를 상수로 선언하는 것으로 T* const 포인터와 같다.


가장 강력한 cosnt의 용도는 함수 선언에 쓸 경우이다.

함수 반환 값을 상수로 정해 주면, 안전성이나 효율을 포기하지 않고도 사용자측의 에러 돌발 상화을 줄이는 효과를 볼 수 있다.



상수 멤버 함수

멤버 함수에 붙는 const 키워드의 역할은 "해당 멤버 함수가 상수 객체에 대해 호출될 함수이다"라는 사살을 알려 주는 것이다.

class TextBlock {
public:

	const char& operator[](std::size_t position) const {		// 상수 객체에 대한 operator[]
		return text[position];
	}
	char& operator[](std::size_t position) {					// 비상수 객체에 대한 operator[]
		return text[position];
	}

private:
	std::string text;
};


어떤 멤버 함수가 상수 멤버(const)라는 것은 비트수준 상수성[bitwise constness, 다른 말로 물리적 상수성(physical constness), 논리적 상수성(logical constness)이다.


황당한 상황을 보완하는 대체 개념으로 나오게 된것이 논리적 상수성이다.


데이터 멤버가 mutable로 선언된 경우 const 멤버 함수에서 이 데이터 멤버에 값을 할당 할 수 있다.


상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법

mutable은 '생각지도 않던 비트 수준 상수성이 웬 말이냐' 문제를 해결하는 괜찮은 방법이다.


캐스팅은 일반적으로도 통념적으로도 썩 좋지 않은 아이디어이다. 하지만 코드 중복을 피하는 방법은 비상수 operator[]가 상수 버전을 호출하도록 구현하는 것이다.


const를 붙이는 캐스팅은 안전한 타입 변환을 강제로 진행하는 것뿐이기 때문에 static_cast만 써도 딱 맞지만, const를 제거하는 캐스팅은 const_cast밖에 없으므로 별다른 선택의 여지가 없다.


이것만은 잊지 말자!

- const를 붙여 선언하면 컴파일러가 사용사으이 에러를 잡아내는 데 도움을 준다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있다.

- 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인(논리적인)상수성을 사용해서 프로그래밍해야 한다.

- 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만들어라.



항목 4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자


객체의 값을 초기화하는 데 있어 C++의 행보는 이랬다저랬다 하는게 영 마음에 안들지만, C++의 객체(변수) 초기화가 중구난방인 것은 절대 아니다.


C++의 C부분만을 쓰고 있으며 초기화에 런타임 비용이 소모될 수 있는 상황이라면 값이 초기화된다는 보장이 없다.


가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화하는 것이다.

C++ 초기화의 나머지 부분은 생성자로 귀결된다. 생성자에서 지킬 규칙은 객체의 모든 것을 초기화 하자! 대입(assignment)을 초기화(initialization)와 햇갈리지 않는 것이 가장 중요하다.

멤버 초기화 리스트를 항상 사용하자

ABEntry::ABEntry(const std::string& name, const std::string& address,
		const std::list& phones){
	theName = name;				// 지금은 모두 '대입을 하고 있다.
	theAddress = address;		// '초기화'가 아니다.
	thePhones = phones;
	numTimesConsulted = 0;
}			

ABEntry::ABEntry(const std::string& name, const std::string& address,
		const std::list& phones) :
		theName(name), 
		theAddress(address), 	// 이제 이들은 모두 초기화 되고 있다.
		thePhones(phones), 
		numTimesConsulted(0) {
}								// 생성자 본문엔 이제 아무것도 들어가 있지 않고요.

ABEntry::ABEntry() :
		theName(), 				// theName의 기본 ctor를 호출한다.
		theAddress(), 			// theAddress에 대해서도 동일
		thePhones(), 			// thePhones에 대해서도 동일
		numTimesConsulted(0) {	// numTimesConsulted는 명시적 0으로 초기화한다.
}

객체를 구성하는 데이터의 초기화 순서

1. 기본 클래스는 파생 클래스보다 먼저 초기화된다.

2. 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화된다.

멤버 초기화 리스트에 넣는 멤버들의 순서도 클래스에 선언한 순서와 동일하게 맞춰주자.


비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.


정적 객체(static object)는 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체이다.

정적 객체의 범주에 들어가는 것

1. 전역 객체가 있다

2. 네임스페이스 유효범위에서 정의된 객체

3. 클래스 안에서 static으로 선언된 객체

4. 함수 안에서 static으로 선언된 객체

5. 파일 유효범위에서 static으로 정의된 객체

이들 중 함수 안에 있는 정적 객체는 지역 정적 객체(local static object)라고 하고, 나머지는 비지역 정적 객체(non-local static object)라고 한다.


번역 단위(translation unit)는 컴파일을 통해 하나의 목적 파일(object file)을 만드는 바탕이 되는 소스 코드를 말한다.

별개의 번역 단위에서 정의된 비지역 정적 객체들의 초기화 순서는 '정해져 있지 않다'라는 사실 때문에 그렇다.


어떤 객체가 초기화도기 전에 그 객체를 사용하는 일이 생기지 않도록 하려면 딱 세가지만 기억해 두고 실천하면 된다.

1. 멤버가 아닌 기본제공 타입 객체는 직접 초기화하라.

2. 객체의 모든 부분에 대한 초기화에는 멤버 초기화 리스트를 사용하라.

3. 별개의 번역 단위에 정의된 비지역 정적 객체에 영향을 끼치는 불확실한 초기화 순서를 염두에 두고 이러한 불확실성을 피해서 프로그램을 설계해야 한다.


이것만은 잊지 말자!

- 기본제공 타입의 객체는 직접 속으로 초기화

- 생성자에서는 데이터 멤버에 대한 대입문을 생성자 본문 내부에 넣는 방법으로 멤버를 초기화하지 말고 멤버 초기화 리스트를 즐겨 사용하자. 초기화 리스트에 데이터 멤버를 나열할 때는 클래스에 각 데이터 멤버가 선언된 순서와 똑같이 나열하자.

- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 한다. 비지역 정적 객체를 지역 정적 객체로 바꾸면 된다.


728x90

'프로그래밍 > Effective C++' 카테고리의 다른 글

Chapter 5 구현  (0) 2018.05.23
Chapter 4 설계 및 선언  (0) 2018.04.17
Chapter 3 자원 관리  (0) 2018.04.11
Chapter 2 생성자, 소멸자 및 대입 연산자  (0) 2018.03.28
독자 여러분 반갑습니다.  (0) 2018.03.15

댓글