effective c++ chapter 1,2 요약

39
Effective C++ Chapter 1,2 요약 NHN NEXT 남현욱

Upload: nam-hyeonuk

Post on 25-Jul-2015

172 views

Category:

Software


1 download

TRANSCRIPT

Effective C++Chapter 1,2 요약

NHN NEXT남현욱

Item 01C++은 각종 언어의 연합체

Item 1 C++은 각종 언어의 연합체

C++은 크게 아래 4가지 부분으로 나눌 수 있다.

- C(절차지향) : 전통적 C언어를 계승 받은 부분

- 객체 지향 : 클래스, 캡슐화, 상속, 다형성, 동적 바인딩 등 OOP 부분

- 템플릿 : 템플릿을 이용한 일반화 프로그래밍 및 메타 프로그래밍

- STL : 자료구조, 알고리즘등을 일반화한 템플릿 라이브러리

각 부분에 따라 효과적인 코딩 규칙이 조금씩 다르기 때문에 자신이 C++언어의 어떤 부분을 이용하고 있는 지 잘 알고 있는게 중요하다.

Item 02전처리기는 되도록 쓰지 말자

Item 2 전처리기는 되도록 쓰지 말자

상수 정의할 때 #define은 웬만하면 쓰지 않기

#define ASPECT_RATIO 1.653보다는const double ASPECT_RATIO = 1.653; 이 낫다.

why?전처리기를 이용해서 상수를 정의하면 컴파일이 일어나기 전에 해당 기호가 쓰인 부분을 모두 기계적으로 정의한 값으로 치환한 다음 컴파일을 시작한다. 이 때문에 1. 디버그 중에 기호로 값 확인 불가(ASPECT_RATIO가 무슨 값인지 알 수 없음) 2. 코드 크기가 비대해짐 같은 문제가 생긴다.

Item 2 전처리기는 되도록 쓰지 말자

클래스 내부 상수는 static!

클래스 내부에서만 쓰이는 상수는 static 상수를 이용하자

클래스 상수는 이런 식으로 선언/ 정의 두 부분으로 나뉜다. 클래스 상수는 선언 시에 값이 주어져야하기 때문에 .h에서 선언을 하고, static 변수이므로 cpp파일에서 정의를 해주는 식으로 사용한다(원래는 정의 안 해도 사용 가능해야 하는데 많은 컴파일러들이 정의를 요구함).

Widget.h

class Widget{private: static const int numTurns = 5;};

Widget.cpp

const int Widget::numTurns;

Item 2 전처리기는 되도록 쓰지 말자

제 컴파일러는 구려서 이렇게 하니까 에러가 납니다

그럴 땐 아래와 같은 방식을 쓰자.

열거형을 이용하면 이렇게 쉽게 해결할 수 있다.

Widget.h

class Widget{private: enum { NumTurns = 5};

int scores[NumTurns];};

Item 2 전처리기는 되도록 쓰지 말자

매크로 함수도 웬만하면 인라인 템플릿 함수로 바꿉시다

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))

C에서 흔히 볼 수 있는 코드다. 그런데 이런 매크로 함수는 전처리기 특유의 기계적인 치환때문에 예상치 못한 결과를 워낙 많이 일으킨다. 그러니 똑같이 일반화된 역할을 할 수 있으면서 안정성까지 갖춘 템플릿 인라인 함수를 이용하자.

template<typename T>inline void callWithMax(const T& a, const T& b){ f( a > b ? a : b); }

Item 03const 애호가가 되자

Item 3 const애호가가 되자

const = 제약. 의도에 벗어나지 않게끔 제약을 가할 수록 안정성 상승!

const가 붙은 건 수정하지 말라라는 의도의 표현이다. 이걸 컴파일러 차원에서 지켜주기 때문에 컴파일 타임에 프로그래머의 의도를 벗어나는 많은 오류를 잡아낼 수 있다.

포인터에 대한 const 규칙const T* const pT;

즉, * 앞에 붙는 const는 포인터가 가리키는 대상에 대한 상수성, 뒤에 붙는 건 포인터 자체에 대한 상수성을 뜻하는 것이다.

데이터상수성

포인터상수성

Item 3 const애호가가 되자

STL 반복자는 포인터랑 비스무리하다

STL의 반복자는 포인터랑 거의 똑같이 생각하면 된다. 단, 반복자 앞에 붙는 const는 T* const랑 의미가 동일하다. 포인터 자체의 수정이 불가함을 뜻한다. 데이터 상수성(const T*)을 원한다면 const_iterator를 쓰자.

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin(); //int* const*iter = 10; // 가능++iter; // 불가

std::vector<int>::const_iterator cIter = vec.begin(); //const int**cIter = 10; // 불가++cIter; // 가능

Item 3 const애호가가 되자

함수에서 const는 참 좋은 친구

함수를 선언할 때 const를 적극적으로 활용하자. 함수 반환값을 상수로 해주면 의도치 않은 버그를 방지할 수 있다.

class Rational{ public: const Rational operator *(const Rational& rhs); };

Rational a, b, c;

(a * b) = c; // const 지정 덕분에 이런 대입 방지!

const가 없다면 a*b의 결과로 나오는 임시 객체에 c를 대입하는 어처구니 없는 일이 벌어질 것이다.

Item 3 const애호가가 되자

상수 멤버 함수

상수 멤버 함수는 상수 객체에서도 호출 가능한 함수를 말한다. 함수 맨 뒤에 const를 붙여서 표현.

특징

1. 멤버 변수의 값을 바꾸지 않는다.2. 상수 객체 참조자(const T&)에서 호출 가능3. 상수 멤버 함수냐 아니냐만 갖고 오버로딩도 가능-> T& operator [](std::size_t idx); 와 const T& operator [] (std::size_t idx) const;

Item 3 const애호가가 되자

상수성의 개념

물리적 상수성(비트수준 상수성)

객체 데이터를 구성하는 비트 하나도 바뀌지 않아야 함.

즉 멤버 포인터가 가리키는 대상의 수정 여부는 판단하지 않음.

논리적 상수성

값이 몇 개 바뀌어도 사용자가 그걸 알 수 없고, 논리적으로 상수임이 맞기만 하면 그건 상수.

mutable 키워드를 이용해 상수 객체에서도 수정 가능한 멤버 생성 가능

Item 04항상 초기화부터 하자

Item 4 항상 초기화부터 하자

정의되지 않은 동작 방지하기

C++ 내장 타입의 경우 변수를 선언했을 때 그걸 초기화해 줄 때도 있고 안 해 줄 때도 있다.

초기화 안 된 변수는 정의되지 않은 동작을 유발하므로 무조건 초기화를 해 주자.

* class에서 멤버 변수 초기화할 때 웬만하면 생성자 내부에서 대입 연산자를 이용하는 방식은 쓰지 말자.

데이터 멤버 초기화 -> 생성자 본문 들어가기 전에 이루어져야 함. 따라서 초기화 리스트를 쓰는게 좋다.

Item 4 항상 초기화부터 하자

Class에서의 초기화 순서

기반 클래스 초기화

클래스 멤버 변수 초기화

무조건 변수 선언 순서대로 초기화.초기화 리스트에서의 순서는 영향 x

Item 4 항상 초기화부터 하자

비 지역 정적 객체 초기화 순서

비 지역 정적 객체는 전역 객체, 네임스페이스 내의 전역 객체, 클래스의 정적 멤버 객체, 파일 유효 범위에 선언된 정적 객체를 가리킴.

문제는 별개의 번역 단위에 선언된 비 지역 정적 객체들의 초기화 순서는 정의되어있지 않다는 것.

이런 문제를 해결하기 위해서 싱글톤(Singleton) 패턴을 주로 쓴다.

Item 05자동으로 생성되는 함수를 조심하자

Item 5 자동으로 생성되는 함수를 조심하자

class의 몇몇 함수는 선언하지 않아도 저절로 만들어진다

default 생성자, 소멸자 - 직접 정의한 생성자 / 소멸자가 하나도 없는 경우 자동 생성. - 기반 클래스의 생성자 / 소멸자 호출 작업 및 비정적 데이터 멤버들의 기본 생성자 / 소멸자를 호출해줌.

- 기반 클래스의 소멸자가 가상이었을 경우 마찬가지로 가상으로 생성.

복사 생성자, 복사 대입 연산자 - 필요한 경우 자동으로 생성됨(복사 생성, 대입 등의 연산이 코드에 있는데 클래스에 정의가 되어있지 않은 경우). - 내장 타입은 단순히 비트 수준 복사를 수행함. 사용자 정의 타입은 해당 타입의 복사 생성자 호출. - 기반 클래스의 복사 대입 연산자가 private이거나 클래스 멤버 중 상수 멤버, 참조 타입 멤버가 있는 경우 자동 생성 안 됨.

Item 06필요없는 함수는 사용 금지

Item 6 필요없는 함수는 사용 금지

private 선언 -> 정의하지 않기!

자동 생성되는 복사 생성자 같은 함수들을 아예 쓰지 못하게 하고 싶을 때가 있다. 그럴 땐 아래와 같은 방법을 쓰자.

class Widget{private: Widget(Widget& other); Widget& operator = (Widget& rhs); };

위와 같이 선언만 해놓고 저 함수들의 정의를 하지 않는 것이다.

* C++ 11에서는 deleted function이라는 더 세련된 방법을 제공한다

Item 07다형성엔 가상 소멸자

Item 7 다형성엔 가상 소멸자

다형성을 위한 기반 클래스엔 가상 소멸자를 쓰자

가상 소멸자를 쓰지 않으면 포인터의 정적 바인딩과 동적 바인딩의 차이 때문에 제대로 소멸이 이루어지지 않아 메모리 릭 등의 문제가 발생한다. 가상 소멸자를 써야 소멸이 순서대로 올바르게 일어나니 꼭 가상 소멸자를 쓰자.

class Base{ Base(); virtual Base(); // 이렇게 기반 클래스의 소멸자는 꼭 가상으로 };

Item 7 다형성엔 가상 소멸자

물론 무턱대고 가상 소멸자를 쓰는 건 안 된다

다형성을 위한 기반 클래스가 아니라면 가상 소멸자로 선언하지 말자. 메모리만 낭비한다.

* C++11의 경우 final 키워드를 이용하면 상속을 원천 방지할 수 있다.

상속받을 땐 주의하자

내가 상속받으려는 클래스의 소멸자를 잘 확인하자. 가상 소멸자가 아니라면 이 함수를 상속 받아서는 안 된다.

Item 08소멸자에서의 예외 처리

Item 8 소멸자에서의 예외처리

소멸자에서의 예외는 어떻게 처리하는게 좋을까

특정 컨테이너를 비울 때 등 많은 소멸자가 한 번에 호출되는 경우가 많음. 이 때 이 처리를 나중으로 미뤄버리면 예외가 쌓이고 쌓여서 문제를 일으킬 소지가 굉장히 크다

그러니까 소멸자 안의 예외는 소멸자 안에서 끝내자

예외를 소멸자 밖으로 던지지 말고 소멸자 안에서 발생한 예외는 소멸자 내에서 그 뒷처리까지 끝냅시다.

Item 8 소멸자에서의 예외처리

예외 처리 방법

프로그램 끝내기

abort 함수를 호출해서 그냥 프로그램을 끝내버리자. try{ ... } catch { std::abort(); }

예외 삼키기

로그만 남기고 계속 진행. ( * 로그 함수는 어떤걸 쓸지 쓰기 나름. 아래 코드의 log는 그냥 예시. 실제 존재하는 함수 아님)try { ... } catch { log(“예외 발생했습니다”); }

Item 8 소멸자에서의 예외처리

문제는 두 방법 다 그리 좋은 방법은 아니다라는 것

두 방법 모두 처음 예외를 던지게 된 원인에 대해 어떤 조치를 취할 지에 대한 내용이 전무하기 때문에 별로 좋은 방법은 아니다.

예외가 발생할 여지가 있는 부분을 함수를 따로 빼자

예외 처리를 소멸자에서 하지 않도록 예외가 발생할 여지가 있는 부분을 별도로 함수로 빼서 클래스의 사용자가 이걸 호출하도록 만들자. 이게 사용자가 더 능동적으로 예외 상황에 대처할 수 있기 때문에 좋은 방법이다.

Item 09생성, 소멸자에선 가상 함수 호출 금지

Item 9 생성, 소멸자에선 가상 함수 호출 금지

파생 클래스의 생성자 호출시 기반 클래스의 생성자가 먼저 호출된 후 파생 클래스의 생성자가 호출된다.

이 때 기반 클래스의 생성자 처리 중에는 해당 클래스가 기반 클래스 그 자체인 것처럼 취급된다. 즉 이 타이밍에 가상 함수 호출해봤자 동적 바인딩이 안 된다.

따라서 이 타이밍에 가상함수를 쓰면 예상치 못한 동작을 일으킬 수 있으므로 생성자에서 가상함수를 쓰는 짓은 하지 말자.

Item 10대입 연산자의 리턴 타입

Item 10 대입 연산자의 리턴 타입

연쇄 대입이 가능하도록 참조자를 리턴하자.

기본 내장 타입의 경우 다음과 같은 동작이 가능하다.

int a = 5, b = 3, c = 7; a = b = c;

클래스는 기본 타입과 되도록 똑같은 동작을 해야하므로 클래스의 대입도 이런 동작이 가능하도록 대입 연산은 참조자를 리턴하게 만들자.

T& operator = (T& rhs); T& operator +=(T& rhs); T& operator *=(T& rhs); ... 기타 등등 대입 시리즈들.

Item 11자기 자신을 대입하는 경우 처리

Item 11 자기 자신을 대입하는 경우 처리

은근히 이런 경우가 많다

반복문을 돌거나 하는 상황에서 은근히 자신에게 자기 자신을 대입하는 경우가 은근히 많이 발생한다. 이런 경우를 제대로 처리해놓지 않으면 문제가 발생할 수 있다(연산자 내부에서 delete 연산 등을 하는 경우 큰 일 난다).

해결 방법

1. 일치성 검사2. 사본 복사 후 대입, 삭제3. copy & swap

Item 11 자기 자신을 대입하는 경우 처리

일치성 검사

먼저 동일한 지 검사한 후 복사를 진행하는 방식이다.if (&rhs == this) return; //같으면 복사 안 함

사본 복사 후 대입, 삭제

delete 되는 등의 문제를 방지하기 위해 따로 사본을 만들어 처리.

Member* pSave = pData; //원래 데이터 저장pData = new Member(*rhs.pData); //사본을 대입delete pSave; // 원래 데이터 삭제

Item 11 자기 자신을 대입하는 경우 처리

copy & swap

자기 자신을 대입하는 경우가 그리 많지 않다. 그런데 그 많지 않은 경우를 위해서 매번 일치성 검사를 하는 건 별로 효율이 좋다고 보기가 힘들다. 두 데이터를 스왑하는게 빠르다면 이 방법을 추천.

Widget temp(rhs); //사본 생성swap(rhs); //현재 객체와 사본의 내용을 서로 뒤바꿈

말 그대로 복사한 후 서로 내용 바꾸기를 하는 것이다.

Item 12복사는 완벽하게

Item 12 복사는 완벽하게

직접 복사 생성자 / 복사 대입 연산자를 만들어 쓸 때

컴파일러가 알아서 만들어주는 녀석들은 최소한 모든 멤버를 기계적으로 완벽하게 복사는 해 준다. 하지만 직접 만들 경우에는 실수해서 하나씩 빠뜨리기가 쉽다. 항상 아래 두 가지 사항을 체크하자.

1. 해당 클래스의 데이터 멤버를 모두 복사했는 지2. 상속받은 기반 클래스의 복사 함수도 제대로 호출해 주었는지

보통 기반 클래스의 데이터는 건드릴 수 없는 경우가 많기 때문에 제대로 복사 함수를 호출해서 복사를 해 주어야 한다(직접 복사해줄 수 없으니까).