let`s learn ! effective c++

105
Let`s Learn ! About NHN NEXT 2 기 기기기 Effective C++

Upload: -

Post on 03-Aug-2015

99 views

Category:

Engineering


0 download

TRANSCRIPT

Page 1: Let`s learn ! Effective C++

Let`s Learn !AboutNHN NEXT 2 기 김승현

Effec-tive C++

Page 2: Let`s learn ! Effective C++

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

Page 3: Let`s learn ! Effective C++

[Item 1] C++ 은 언어들의 연합체

그렇다면 C++ 이 지원하는 하위 언어는 ?

C

객체 지향 개념의 C++

템플릿 C++

STL

때문에 아래와 같은 특성을 가진다 .

절차적 ( from C )

객체지향적 ( 클래스 구조 )

일반화 ( 템플릿 , STL )

함수형 ( 람다식 )

메타프로그래밍 ( TMP )

Page 4: Let`s learn ! Effective C++

[Item 2] #define 보다는 ..

< const 를 사용하자 >

- #define 으로 선언된 상수는 컴파일 시 , 코드를 상수값으로 단순 치환하도록 구현되어 있다 .

- 이 때문에 const 로 선언된 상수는 그 값을 목적코드에 하나만 복사하는 반면 ,

#define 은 치환된 상수의 수 ( 상수의 사용 횟수 ) 만큼 복사하기 때문에 최종 코드의 크기가 더 커진다 .

- 또한 치환 전 상수의 이름을 알 수 없으므로 디버깅 시 혼란을 줄 수 있다 .

Page 5: Let`s learn ! Effective C++

[Item 2] #define 보다는 ..

< enum, static 을 사용하자 >

- #define 상수는 유효 범위의 제한이 없으므로 객체지향 특성의 캡슐화를 적용할 수 없다 .

그러니 enum 이나 static 을 사용하여 클래스 멤버 상수를 정의하자 .

- static const 를 사용한 예

- 상수가 정수타입 ( int, char, bool 등 ) 인 경우 이렇게도 쓸 수 있다 .

- int 형 상수가 필요하다면 enum 을 사용하여 아래처럼 사용 가능하다 .

Page 6: Let`s learn ! Effective C++

[Item 2] #define 보다는 ..

< inline 을 사용하자 >

- #define 을 사용한 매크로 함수는 전달인자를 단순 치환 하기 때문에 ,

아래의 상황 등에서 다음과 같은 경우 바보같은문제가 발생한다 .

결과적으로 의도한 것과 다른 결과가 나타나는 경우가 있으므로 ,

템플릿 inline 함수를 사용하여 사전에 문제를 방지하는 것이 좋다 .

Page 7: Let`s learn ! Effective C++

[Item 3] 올바른 const 의 사용 방법

- 상수 포인터는 가르킬 대상을 바꿀 수 없다 .

- 상수 데이터는 값을 바꿀 수 없다 .

Page 8: Let`s learn ! Effective C++

[Item 3] 올바른 const 의 사용 방법< const 멤버 변수의 초기화 문제 >

- 클래스 내의 멤버 변수는 객체의 생성 시 메모리 할당이 가장 먼저 일어난다 .

따라서 멤버 변수는 생성자가 호출되기 전에 쓰레기 값으로 한번 초기화된다 .

멤버 변수를 const 로 선언했다면 생성자 호출에서도 값이 바뀌지 않는 문제가 발생한다 .

- 이 문제를 이니셜라이저 initializer( 혹은 초기화 리스트 ) 방법으로 해결할 수 있다 .

- 다음과 같이 함수 head 와 body 사이에 :id(_id) 를 넣으면 ,

메모리 할당 시 쓰레기 값으로 초기화하는 대신

멤버변수 id 를 _id 로 초기화하라는 의미가 된다 .

Page 9: Let`s learn ! Effective C++

[Item 3] 올바른 const 의 사용 방법< const 멤버 함수의 사용 방법 >

- 함수의 head 와 body 사이에 const 를 붙임으로써 만들 수 있다 .

- const 멤버 함수는 클래스의 멤버 변수를 직접적으로 변경할 수 없으며 ,

간접적으로 값이 바뀔 가능성도 남기지 않는다 . ( compile error! )

다음과 같은 Student 클래스의 const 멤버 함수인 Study 함수는

멤버 변수 intelligence 를 바꾸지 못한다 .

하지만 민수객체는 생성자에서 10 억 정도로 초기화해서 상관없겠지 .

Page 10: Let`s learn ! Effective C++

[Item 3] 올바른 const 의 사용 방법< 함수에서 간접적으로 값을 바꿀 가능성이 있는 경우 >

멤버 변수의 포인터를 비 상수형으로 리턴하는 경우 .

- 리턴한 포인터를 통해 값을 변경할 가능성이 있다 .

- 앞에 const 를 붙여 상수형으로 리턴하는 것은 가능하다 .

const 가 아닌 멤버 함수를 호출하는 경우 .

- 오른쪽 예제의 경우 PlayGame() 은 intelligence 를 감소시킬 가능성이 있으므로

( 실제로 값을 바꾸지 않아도 ) const 로 지정된 Study() 함수 내에서 호출할 수 없다 .

- 반대로 const 로 지정된 멤버 함수는 값을 바꿀 수 없으므로 호출할 수 있다 .

Page 11: Let`s learn ! Effective C++

[Item 4] 모든 객체 사용 전 초기화< 멤버 변수의 초기화 >

- C++ 기본 생성자는 객체를 생성할 때 ,

메모리 할당과 동시에 멤버 변수를 쓰레기 값으로 초기화시킨다 .

물론 쓰레기 값이 대입되어 있을 때 사용하면 프로그램은 예상할 수 없는 무빙을 보이므로 ,

새로운 값을 대입하거나 혹은 초기화 할 때 원하는 값으로 초기화해야 한다 .

( 여기서 전자는 초기화를 두번 하는 셈임 , 대입이 불가능한 케이스도 있음 )

- 위 방식은 [Item 3] 에서 설명했던 ,

Initializer( 초기화 리스트 ) 를 사용하여 다음과 같이 구현 가능하다 .

Page 12: Let`s learn ! Effective C++

[Item 4] 모든 객체 사용 전 초기화< 객체의 초기화 순서 >

상위 클래스는 하위 클래스보다 먼저 초기화된다 .

클래스의 멤버 값들은 선언 순서로 초기화된다 .

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

- 번역 단위는 목적 파일을 만드는 바탕이 되는 소스코드의 부분집합을 의미한다 .

즉 , 비지역 정적 객체의 초기화 순서는 각각의 번역 단위 내에서만 정의되므로 ,

서로 다른 번역 단위에서 어떤 비지역 정적 객체가 먼저 초기화 될지는 알 수 없다 .

- 이를 해결할 가장 보편적인 방법은 싱글톤 패턴을 사용하여 ,

비지역 정적 객체를 지역 정적 객체처럼 만드는 것이다 .

Page 13: Let`s learn ! Effective C++

[Item 4] 모든 객체 사용 전 초기화< 비지역 정적 객체 ? >

- 정적 (static) 객체 ?

객체의 생성 시점부터 프로그램이 종료될 때 까지 살아있는 객체를 의미한다 .

- 비지역 (global) 객체 ?

전역 객체 네임 스페이스 내에서 정의된 객체 클래스 내에서 static 으로 선언된 객체 함수 안에서 static 으로 선언된 객체 파일 유효 범위에 선언된 객체

Page 14: Let`s learn ! Effective C++

Chapter 2생성자 , 소멸자 및 대입 연산자

Page 15: Let`s learn ! Effective C++

[Item 5] 이런거 필요하지 않나요 ? 기본 생성자 / 소멸자

- 정의되지 않은 경우 생성한다 .

복사 생성자 / 복사 대입 연산자

- 필요한 경우에만 생성한다 .

- 단 , 상위 클래스에서 복사 대입 연산자를 private 선언하면 ,

하위 클래스에서 복사 대입 연산자를 자동 생성하지 않는다 .

Page 16: Let`s learn ! Effective C++

[Item 6] 그런거 필요 없거든요 ?

< 자동으로 생성되는 함수가 필요 없으면 사용을 금하자 >

- 복사 생성자와 복사 대입 연산자를 private 에 구현하여 자동 생성을 막을 수 있다 .

- 단 , 다른 클래스의 private 멤버에 접근하는 firend 키워드를 통한 접근까지 막으려면 ,

함수의 몸체를 만들지 않고 선언만 하여 , 접근 시 링커 타임에서 에러를 발생하도록 할 수 있다 .

- 상속 관계를 활용하여 컴파일 타임에 에러를 만들수도 있다 . 일종의 기교 .

예시 )

http://gpgstudy.com/forum/viewtopic.php?t=18793&view=next&sid=7724c553f2ffd89aab0a8fc4930407fb

Page 17: Let`s learn ! Effective C++

[Item 7] 다형성과 virtual 은 실과 바늘- 다형성을 가진 상위 클래스에서는 소멸자를 가상 소멸자로 선언하자 .

그렇지 않으면 하위 클래스의 소멸 시 , 상위 클래스의 소멸자 만이 호출되어

하위 클래스는 소멸조차 되지 않으며 ( 메모리 릭 ), 또다른 문제를 낳을 수 있다 .

- 단 , 가상 함수를 사용하게 되면 가상 테이블과 가상 포인터가 생성되어 ,

메모리를 차지하므로 . 위의 상황이 아니라면 가상 소멸자를 사용하지 않는 것이 좋다 .

Page 18: Let`s learn ! Effective C++

[Item 8] 예외처리와 소멸자- 예외처리가 진행중일때 예외가 발생하면 프로그램을 종료시키는 terminate 함수가 호출된다 .

심지어 이 예상치 못한 종료에 의해 메모리 릭이 발생할 수도 있다 .

소멸자가 호출되는 경우는 아래의 두가지 객체가 통상적인 상태에서 소멸 되는 경우 예외 처리 매커니즘에 의해 객체가 소멸 되는 경우

즉 , 소멸자에서 발생한 예외를 바깥에서 처리하려 할 경우 ,

프로그램이 강제종료되고 , 메모리 릭이 발생할 수 있다 .

- 위 상황을 방지하기 위해 예외처리를 소멸자 안에서 해주거나 ,

예외가 발생할 수 있는 코드를 별도의 함수로 빼야 한다 .

( 후자가 사용자 입장에서 예외에 대응하기에 더 좋다 . )

Page 19: Let`s learn ! Effective C++

[Item 9] 객체 생성 , 소멸 중 가상 함수 호출 ㄴㄴ해- 하위 클래스의 생성자를 호출하면 , 상위 클래스의 생성자가 자동으로 먼저 호출된다 .

이 때 가상 함수를 호출할 경우 하위 클래스에서 호출하고자 한 함수가 ,

상위 클래스의 가상 함수를 호출한 것으로 인식되어 기묘하게 동작하는 프로그램을 볼 수 있다 .

- 반대로 상위 클래스의 소멸자를 호출하면 , 하위 클래스는 이미 소멸자가 호출되어 소멸된 상태이다 .

이 때 가상 함수를 호출할 경우 존재하지 않는 하위 클래스에 접근을 시도하는 문제가 발생한다 .

물론 이 때도 프로그램의 미래를 보장받을 수 없다 .

Page 20: Let`s learn ! Effective C++

[Item 10] a = b = c = d = ...

- C++ 기본 대입 연산자는 아래와 같은 동작이 가능하다 .

- 클래스 환경에서도 이와 같은 동작이 가능하도록 하는 것이 직관적이다 .

이를 대입 연산의 참조자를 리턴하도록 하여 구현할 수 있다 .

Page 21: Let`s learn ! Effective C++

[Item 11] operater= ‘ 나 = 나’ ? 문제자기대입 문제 ?

- 같은 객체를 가리키는 참조자 두개 (A 와 B) 가 A = B 라고 해 놓고 delete B 라고 했을 때 ,

A 가 가리키는 객체는 이미 날라간 상태이다 .

어떻게 이 위험으로부터 보호하겠는가 ?

- 참조자의 경우 주소가 같기 때문에 , 같은 주소인지 비교하여 자기대입인지 판정할 수 있다 .

하지만 자기 대입이 일어날 경우가 적기 때문에 효율은 낮다 . 

- 보다 효과적인 방법으로 , 매개체를 복사한 후에 복사된 매개체와 내자신과 바꾸는 방법이 있다 .

흔히 이 기법을 복사후 맞바꾸기 (copy and swap) 라고 한다 .

- 값에 의한 전달 (reference of value) 로 사본을 전달하여 바꾸는 방법이 있다 .

명확성은 위보다 떨어지나 , 기술적인 (?) 코드 .

Page 22: Let`s learn ! Effective C++

[Item 11] operater= ‘ 나 = 나’ ? 문제

Page 23: Let`s learn ! Effective C++

[Item 12] 객체 복사 ! 이것만 기억하자 !

- 앞서 말했던 컴파일러가 자동으로 만들어주는 복사 생성자 , 복사 대입 연산자는

알아서 상위 클래스의 복사 생성자를 호출하여 , 멤버 변수를 빠짐없이 복사해준다 .

- 하지만 사용자가 복사 생성자 / 복사 대입 연산자를 직접 만들어서 사용하는 경우 ,

해당 클래스의 멤버 변수만 복사할 수 있으니 , 까먹지 말고 상위 클래스의 복사 생성자를 호출해주자 .

Page 24: Let`s learn ! Effective C++

Chapter 3 자원 관리

Page 25: Let`s learn ! Effective C++

[Item 13] 귀찮은 자원관리 .. 객체로 쉽게 !

- 스택에 존재하는 정적 생성 객체는 존재할 수 있는 범위 ( Scope ) 를벗어나면 자동으로 소멸자가 호출되어 메모리가 해제된다 .

- 반면 동적 생성 객체는 메모리를 직접 해제해주기 전까지 존재한다 .

- 메모리를 해제할 때 오른쪽과 같이 delete() 함수를 호출하기도 전에함수를 return 하면 , delete() 는 호출되지 않고 메모리 릭이 일어나게 된다 .

- 위와 같이 동적 생성 객체의 delete 함수를 통한 메모리 해제 방법은 ,

잠재적 문제 가능성이 있다 .

Page 26: Let`s learn ! Effective C++

[Item 13] 귀찮은 자원관리 .. 객체로 쉽게 !

< 동적 생성 객체의 쉬운 자원관리 방법 >

1. 동적 할당으로 얻은 포인터를 인자로 새로운 객체를 생성하며 , 초기화한다 .

2. 새로운 객체의 소멸자에서 할당된 자원을 해제하게한다 .

3. 그 객체가 자신의 소멸시점을 정의하고 , 자동으로 소멸자를 호출하게 한다 .

- 스마트 포인터가 위와 같은 일을 해준다 .

종류에 따라 3 번 항목에 차이가 있으니 필요에 따라 사용하면 좋다 .

( auto, shared 등 )

Page 27: Let`s learn ! Effective C++

[Item 14] 자원 관리 객체의 복사를 신중히 .

- 정적 할당 메모리를 처리하기 위한 자원 관리 클래스를 직접 구현해야 하는 경우가 있다 .

( 왜 스마트 포인터를 안쓰고 새로 만들어야 되는지 모르겠지만 . )

- 하지만 c++ 의 기본 복사 생성자는 단순히 각각의 멤버를 복사하기만 하는 ,

얕은 복사를 수행하기 때문에 , 두 객체가 동일한 포인터를 참조하는 경우가 생긴다 .

- 이는 둘 중 하나의 객체를 소멸했을 때 , 존재하지 않는 자원을 참조하는 문제를 일으킬 수 있다 .

- 때문에 객체의 복사를 어떻게 수행할 지 직접 정의해주는 것이 좋다 .

Page 28: Let`s learn ! Effective C++

[Item 14] 자원 관리 객체의 복사를 신중히 .

1. 복사를 금지하는 방법

복사를 금지하여 문제 발생을 방지할 수 있다 . 유니크한 객체 관리에 사용 .

2. 참조 카운팅 방법

shared_ptr 처럼 자원을 사용하는 객체를 카운팅하여 ,

한 자원의 사용 객체가 모두 소멸될 때 자원을 해제하는 방법이 있다 .

3. 깊은 복사 방법

깊은 복사를 수행하여 , 포인터 자원이 가리키는 값도 복사하도록 한다 .

문자열 객체 관리에 사용한다고 한다 .

4. 자원 이동 방법

복사한 객체에게 자원을 이동시킨다 . 유니크해야 하는 자원을 다룰 때 사용한다 .

Page 29: Let`s learn ! Effective C++

[Item 15] 자원 관리 클래스에서 관리되는 자원의 방임 .

- 자원 관리 클래스에 감싸여진 자원에 접근할 방법이 필요하다 .

이 방법은 명시적 변환과 암시적 변환으로 나뉜다 .

명시적 변환 : shared_ptr, auto_ptr 등 스마트 포인터는 get() 이라는 함수를 통해

실재 자원 형태의 포인터를 반환할 수 있다 . 커스텀 자원 관리 클래스의 경우 이를 구현해주어야 한다 .

암시적 변환 : operater * 또는 operater -> 을 이용하여 실재 자원에 접근이 가능하다 .

이 또한 스마트 포인터에 구현된 기능 . 편하지만 명시적 변환에 비해 실수를 일으키기 쉽다 .

Page 30: Let`s learn ! Effective C++

[Item 16] new 면 delete. new[] 면 delete[].

new : 단일 객체를 메모리에 할당하는 연산자 .

new[] : 객체 배열을 메모리에 할당하는 연산자 .

- 이를 구분해줘야 하는 이유는 , new[] 로 복수의 객체를 할당하고 delete 를 사용하면 ,

첫 번째의 한 객체만 해제하고 나머지 객체는 해제되지 않는 문제가 생긴다 .

- 반대로 new 로 포인터를 할당하고 , delete[] 를 사용하면 ,

배열로 인식하여 문제를 일으킬 수 있다 . 그러니 짝을 잘 맞추자 .

- typedef 로 배열을 축약할 경우 실수하기 좋으니 주의해야 한다 .

Page 31: Let`s learn ! Effective C++

[Item 17] new 로 생성한 객체를 스마트 포인터에 넣는 코드는 별도로

- 위의 작업은 func(), (new ClassName), shared_ptr<> 순서로 수행된다면 문제가 없다 .

하지만 이같은 상황에서 연산의 순서는 컴파일러에 따라 통용되지 않을 수 있다 .

때문에 (new ClassName), func(), shared_ptr<> 의 순서로 수행될 수 있으며 ,

이 때 func 함수에서 예외가 발생하면 shared_ptr<> 는 수행되지 않고 ,

메모리 릭이 발생하게 된다 . 그러니 다음과 같이 분리해주자 .

Page 32: Let`s learn ! Effective C++

Chapter 4 설계 및 선언

Page 33: Let`s learn ! Effective C++

[Item 18] 인터페이스 설계 , 제대로 쓰긴 쉽고 실수하긴 어렵게 하자

- 함수 호출의 설계

인자의 타입이 같을 경우 ,

매개변수 전달 순서를 틀리는 실수를 범할 수 있다 .

타입이 올바르기 때문에 컴파일 단계에서 잡아주지 않음 .

이 문제를 우측과 같이 ,

사용자 정의 타입을 사용하여 방지할 수 있다 .

잘못된 타입을 인자로 넣으면 컴파일 에러가 난다 .

Page 34: Let`s learn ! Effective C++

[Item 18] 인터페이스 설계 , 제대로 쓰긴 쉽고 실수하긴 어렵게 하자

- 새로운 타입의 설계

사용자에게 익숙한 기본 제공 타입과 유사하게 동작하도록 설계하는 것을 우선으로 한다 .

사용법이 다르면 사용자의 의도와 다른 결과를 일으킬 수 있다 .

동일하게 동작한다면 기존의 코드에서 타입만을 교체하는 경우에도 유리하다 .

Page 35: Let`s learn ! Effective C++

[Item 18] 인터페이스 설계 , 제대로 쓰긴 쉽고 실수하긴 어렵게 하자

사용 시 숙지해야 하는 제약조건을 줄이자 .

- 팩토리 함수에서 객체를 생성하는 경우 , 사용자에게 자원 관리를 맡기지 말자 .

스마트 포인터를 사용하여 자원 관리를 자동으로 처리하게 하자 .

- 사용자가 스마트 포인터를 사용해야 한다는 사실도 잊어버릴 가능성이 있다 .

팩터리 함수가 스마트 포인터를 반환하도록 구현하자 .

Page 36: Let`s learn ! Effective C++

[Item 19] C++ 의 클래스 설계는 타입 설계와 같다 .

- C++ 에서 클래스를 설계하는 것은 타입을 설계하는 것과 같다 .

왜냐하면 operater 오버로딩 , 메모리 할당 및 해제 ,

객체 초기화 등 모두 클래스 내에서 할 수 있도록 하기 때문이다 .

설계 방법론

1. 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어 져야 하는가 ?

2. 객체 초기화는 객체 대입과 어떻게 달라야 하는가 ?

3. 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에는 어떤 의미를 줄 것인가 ?

4. 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가 ?

5. 기존의 클래스 상속 계통망에 맞출 것인가 ?

Page 37: Let`s learn ! Effective C++

[Item 19] C++ 의 클래스 설계는 타입 설계와 같다 .

6. 어떤 종류의 타입 변환을 허용할 것인가 ?

7. 어떤 연산자와 함수를 두어야 의미가 있는 것인가 ?

8. 표준 함수들 중 어떤 것을 허용하지 말 것인가 ?

9. 새로운 타입의 멤버에 대한 접근 권한을 어느쪽에 줄 것인가 ?

10. 선언되지 않은 인터페이스로 무엇을 둘 것인가 ?

11. 새로 만드는 타입이 얼마나 일반적인가 ?

12. 정말로 꼭 필요한 타입인가 ?

Page 38: Let`s learn ! Effective C++

[Item 20] 값에 의한 전달 < 상수객체 참조자에 의한 전달

- 값에 의한 전달은 , 전달 인자를 복사하여 받는 방식을 의미한다 .

때문에 수행시간이 인자의 크기에 종속된다 .

또한 복사로 인한 생성자 , 소멸자 추가호출에 의한 고비용 문제가 있다 .

- 반면 , 상수 객체 참조자에 의한 전달 방식은 참조자를 전달하기 때문에 ,

인자의 크기와 호출에 의한 비용이 무관하다 .

추가호출 문제에 대해서도 안전하므로 , 기존에값에 의해 전달되도록 설계된

기본 제공 타입 , STL 반복자 , 함수 객체 타입 같은 경우가 아니라면 ,

상수 객체 참조자에 의한 전달을 사용하는 것이 유리하다 .

Page 39: Let`s learn ! Effective C++

[Item 21] 객체를 반환할 때 , 참조자를 반환하지 말자 .

- 함수 내부 스택에 올라간 객체는 ,

함수 종료 후 자동으로 해제된다 . 때문에 그 객체의 참조자를 리턴할 경우 ,

이미 해제된 자원에 접근하는 문제를 일으키게 된다 .

- 힙에 생성된 동적 생성 객체는 ,

사용자가 직접 소멸해줘야 하기 때문에 메모리 누수의 가능성이 있다 .

- 정적 객체 ( static ) 로 생성된 객체는 ,

함수 호출 시 , 처음 생성된 정적 영역만을 다시 접근하게 되기 때문에 ,

이전의 호출에서 받았던 객체가 새로운 호출에 의해 덮어쓰여 진다 .

Page 40: Let`s learn ! Effective C++

[Item 21] 객체를 반환할 때 , 참조자를 반환하지 말자 .

- 그럼 어떻게 해야 하나 ?

생성자 , 소멸자 호출 비용을 감수하고 객체를 반환하는 수밖에 없다 .

대신 반환 최적화 기능 (RVO) 을 가진 컴파일러일 경우 ,

객체의 생성 , 소멸 동작을 제거할 수도 있기 때문에 ,

사용자는 그냥 객체를 반환하는 동작만 구현하면 된다 .

Page 41: Let`s learn ! Effective C++

[Item 22] 멤버 데이터는 private 영역에 .

- 사용자가 public 으로 된 멤버 데이터를 클래스 외부 곳곳에서 수없이 사용했다고 가정하자 .

이 때 , 만약 런타임 중간에 값이 예상치 못한 값으로 바뀌고 에러를 일으키면

어디서 값이 바뀌었는지 알기 매우 어렵다 .

또한 이 데이터를 사용하지 않기로 하여 삭제할 경우에도 ,

코드 전체에 걸쳐 수정해야 하는 문제가 있다 .

이런 문제들은 private 로 선언하고 , public 함수로만 접근할 수 있게하여 방지할 수 있다 .

또한 데이터 접근을 카운팅하거나 , 조건을 추가하는 등 구현상의 융통성을 전부 누릴 수 있다 .

Page 42: Let`s learn ! Effective C++

[Item 23] 비멤버 비프렌드 함수를 사용해보자 .

- 비 멤버 비 프렌드 함수 ?

오른쪽과 같은 함수가 비 멤버 비 프렌드 함수이다 .

클래스 내부 멤버 데이터에 접근할 수 없기 때문에 ,

이 함수를 사용하는 것으로 클래스 구조의 캡슐력 (?) 을 늘릴 수 있다 .

- 어떨 때 쓰는게 좋은가 ?

특정 멤버 함수들을 조합한 편의 함수를 만들 때 사용하면 좋다 .

detail

http://girtowin.tistory.com/19

Page 43: Let`s learn ! Effective C++

[Item 24] 비멤버 비프렌드 함수를 사용해보자 .

- 이런 유리수 클래스를 만들면 보통 위와 같이 암시적 변환 및 사칙연산 등을 지원하도록 구현을

할 것이다 . 근데 이 때 위와같이 짜놓으면 Rational * 2 는 돼도 2 * Rational 은 안 되는 문제가 생

긴다 .

이런 식으로 모든 매개변수에 대해 호환되는 타입 변환이 필요한 경우에는 비멤버 함수를

쓰면 된다 .

Page 44: Let`s learn ! Effective C++

[Item 25] 예외를 던지지 않는 swap 에 대한 지원

- std::swap 은 객체를 여러번 복사하기 때문에 , 큰 객체를 다룰 경우 느리게 동작한다 .

객체를 복사하는 것이 어쩔 수 없는 경우도 있겠지만 ,

- 간단하게 포인터만 바꾸는 방법으로 swap 을 동작하게 할 수 있는 경우도 있다 .

바로 우측과 같이 다른 타입의 데이터를 가리키는 포인터를 주 성분으로 가진

타입의 객체를 swap 하는 경우이다 .

Page 45: Let`s learn ! Effective C++

[Item 25] 예외를 던지지 않는 swap 에 대한 지원

< 멤버 포인터 swap 의 구현 >

객체에 swap 이라는 public 멤버 함수를 만든다 .

( 이 때 pimpl 은 private 이기 때문에 외부에서 접근할 수 없다 . )

클래스 ( 혹은 템플릿 ) 을 정의한 네임스페이스안에

비멤버인 swap 함수를 만들고 , 멤버 함수의 swap 을 호출하게 한다 .

Page 46: Let`s learn ! Effective C++

Chapter 5 구현

Page 47: Let`s learn ! Effective C++

[Item 26] 변수 정의는 늦출 수 있는 데까지 늦추자 .

- 왜 늦춰야 하는가 ?

조급하게 선언한 경우 , 사용하기 전 다른 작업을 하다가

오류를 던지고 빠져나온 경우 , 선언이 무의미해진다 .

- 그러니 선행 평가로 이득을 볼 수 있는 경우가 아니라면 ,

되도록 지연 평가를 하는 것이 좋다 .

- 또한 루프 안에서 선언할 경우 , 생성자를 반복해서 호출하게 되므로

다소의 가독성은 포기하더라도 , 루프 밖에서 선언해주자 .

Page 48: Let`s learn ! Effective C++

[Item 27] 절약 절약 캐스팅을 절약

- C++ 의 캐스팅은 C 에서의 묵시적 캐스팅과 달리 , 명시적으로 구현하도록 되어있다 .

명시적 캐스팅은 묵시적 캐스팅보다 직관적이고 ,

잘못된 형변환에서 컴파일 에러를 내주는 장점이 있다 .

- 하지만 .

캐스팅을 위해 임시 객체를 만드는 비용이 있으며 ,

dynamic_cast 의 경우 클래스 이름에 대한 문자열 비교로 런타임 비용이 많다 .

Page 49: Let`s learn ! Effective C++

[Item 27] 절약 절약 캐스팅을 절약

- 애초에 .

캐스팅을 사용하는 것 자체가 사용자에게 불편을 준다 .

상수성을 제거하거나 , 상수화 시키는 캐스팅은 본래의 목적을 뒤엎는 코드이며 ,

한 데이터가 캐스팅 전후를 분기로 서로 다른 타입을 가지는 것이

사용자에게 혼란을 줄 수 있다 .

- 그러니 .

왠만하면 캐스팅은 사용하지 말자 .

캐스팅을 피할 수 없다면 , C++ 스타일의 캐스팅을 사용하며 , 함수 안에 숨기도록 하자 .

Page 50: Let`s learn ! Effective C++

[Item 28] 내부 사용 객체의 핸들을 반환하는 코드는 피하자 .

- private 였던 객체의 핸들을 반환하는 것은 ,

내부 객체의 값을 수정하는 상황을 야기할 수 있다 .

이는 캡슐화의 파괴를 의미한다 .

그러니 반환 타입에 const 를 붙여 수정할 수 없게 하자 .

- 이게 전부 ?

다음 boundingBox 함수에서 임시 객체를 생성한다고 가정하자 .

이 때 , 이 임시 객체의 내부 데이터 주소를 얻는 경우 ,

문장이 끝날 때는 임시 객체와 내부 데이터가 소멸된 상태이다 .

이렇게 얻은 주소 pUpperLeft 는 댕글링 (dangling pointer) 이 된다 .

const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());

Page 51: Let`s learn ! Effective C++

[Item 28] 내부 사용 객체의 핸들을 반환하는 코드는 피하자 .

- 문제가 많네요 /

그러니 객체의 내부 요소에 대한 핸들을 반환하는 코드를 최소화하는게

캡슐화도 줄이고 댕글링도 줄이고 짱짱

Page 52: Let`s learn ! Effective C++

[Item 29] 예외 안전성 확보하자

- 주로 예외적 상황에서 발생하는 , 런타임 에러는

컴파일 타임에 발생한 에러를 찾는 것에 비해 귀찮고 어려운 일이다 .

그러니 함수들의 예외 안전성을 확보해서

디버깅 이점을 가지고 , 프로그램도 견고하게 짜보자 .

- 함수 예외 안전성의 핵심 ?

1. 자원 누수 없도록 하기

2. 자료구조가 dirty 하게 되지 않도록 보호하기

( 더럽혀진 자료는 문제를 일으키거나 , 잘못된 결과를 도출 . )

Page 53: Let`s learn ! Effective C++

[Item 29] 예외 안전성 확보하자

- 예외 안전성 보장을 위한 방법

1. 기본적인 보장 ( basic quarantee )

예외 발생 상황에서도 , 자원이 누수되지 않는다는 보장이다 .

2. 강력한 보장 ( strong quarantee )

실패 시 과거의 상태로 되돌려 없던 일로 되돌린다는 보장이다 .

3. 예외불가 보장 ( nothrow quarantee )

예외를 절대 던지지 않는다는 보장이다 .

Page 54: Let`s learn ! Effective C++

[Item 30] 인라인 함수 , 제대로 알고 쓰자

< 인라인이 무시되는 케이스 >

- 명시적이든 암시적이든 , 컴파일러의 입장에서 인라인을 허용했을 때

성능 저하가 예상되는 함수는 인라인으로 동작하지 않는다 .

- 가상 함수는 인라인을 사용할 수 없다 .

Page 55: Let`s learn ! Effective C++

[Item 30] 인라인 함수 , 제대로 알고 쓰자

- 생성자와 소멸자는 인라인 하기에 좋지 않은 함수이다 . 그 이유는

생성자는 함수 몸체를 비워놔도 컴파일러가 몸체에 아래같은 코드를 추가한다 .

부모의 생성자 멤버 객체의 생성자

이 때 부모의 생성자마저 인라인 함수라면 몸체는 점점 불어나고 ,

성능 저하를 유발한다 . 소멸자도 마찬가지의 문제를 가진다 .

Page 56: Let`s learn ! Effective C++

[Item 31] 파일간 컴파일 의존성을 최소화하자 .

- 파일간의 컴파일 의존성은 , 쉽게 말하자면 #include 로 맺어진 관계를 의미한다 .

- 코드를 수정하면 컴파일 시에 수정한 파일뿐만 아니라 ,

파일을 포함 ( include ) 한 파일까지 재컴파일하기 때문에

#include 관계의 수는 재컴파일 시간에 비례하게 된다 .

이는 큰 프로젝트일수록 개발시간에 많은 영향을 주는 요소이다 .

Page 57: Let`s learn ! Effective C++

[Item 31] 파일간 컴파일 의존성을 최소화하자 .

- 어떻게 해야 컴파일 의존성을 줄일 수 있을까 ?

당연한 방법론이지만 .

구현 시 상대 객체의 세부 사항 ( 크기 등 ) 이 필요한 경우가 아니라면 ,

include 관계를 맺지 않게 한다 .

객체의 크기 등 세부 사항을 몰라도 되는 경우이면서 , 선언이 필요한 경우엔

include 대신 전방선언을 사용하자 .

Page 58: Let`s learn ! Effective C++

[Item 31] 파일간 컴파일 의존성을 최소화하자 .

- 결국 세부 정보가 필요한 경우라면 .

클래스를 정의하지 않고 , 선언만 하고 있는 헤더 파일을 include 하자 .

#include “datefwd.h” // Date 클래스를 선언만 하고 있는 헤더파일

Date today();

void clearAppointments(Date d);

이 방법은 include 한 객체의 정의부분은 재컴파일하지 않기 때문에

앞에서 말한 문제를 방지할 수 있다 .

Page 59: Let`s learn ! Effective C++

Chapter 6 상속 그리고 객체 지향 설계

Page 60: Let`s learn ! Effective C++

[Item 32] public 상속 모형은 반드시 is-a 모형을 따르게 만들자 .

- 우측 클래스 구조를 is-a 모형을 따라 자연어로 변형시키면 다음과 같다 .

모든 펭귄 ( Penguin ) 은 새 ( Bird ) 다 .

- 이 구조에 따르면 펭귄은 새이기 때문에 날 수 있지만 (?),

새는 펭귄이 아니기 때문에 수영을 할 수는 없다 .

O penguin.fly(); X bird.swim();

- 근데 실제로 펭귄은 날지 못하기 때문에 이는 잘못된 구조이다 .

펭귄 클래스에서 fly() 를 호출하면 에러를 내도록 했지만 ,

이는 컴파일 타임에 penguin.fly() 를 허락하기 때문에 구조적 해결은 되지 않는다 .

현실세계에서의 is-a 모형은 객체지향의 세계에서와 차이가 있다는 것을 유념하기 바란다 .

Page 61: Let`s learn ! Effective C++

[Item 32] public 상속 모형은 반드시 is-a 모형을 따르게 만들자 .

현실세계에서의 is-a 모형은 객체지향의 세계에서와 차이가 있다는 것을 유념하기 바란다 .

- 위 사실을 간과해서 생기는 문제는 조류 뿐만 아니라 기하학에서조차 유효하다 .

- 현실에서 정사각형이 직사각형이라는 것은 당연 야구빠다가 아닐 수 없다 .

Square is a Rectangle.

- 객체지향 구조에서 Rectangle 은 가로 변 길이를 늘리는 함수를 가질 수 있는데 ,

문제는 이 함수를 정사각형에서 허용했을 때 , 정사각형의 정의 ( 가로 세로 변 길이가 같은 직사각형 . )

에 모순을 일으키는 동작이 된다 .

현실의 당연스러운 법칙이 객체지향에선 잘못된 구조일 수 있는 것 .

Page 62: Let`s learn ! Effective C++

[Item 33] 상속된 이름을 가리지 않도록 주의하자 .

- 컴파일러는 어떤 변수명을 만나면 , 자신과 가까운 scope 부터 하나씩 확인한다 .

- 같은 이름으로 선언된 변수가 있다면 사용하고 더이상 찾지 않는다 .

찾지 못하면 scope 를 하나씩 빠져나오며 탐색을 반복한다 .

- 함수의 모든 유효범위에서 찾지 못했다면 , 전역 범위를 확인한다 .

이를 C++ 의 이름가리기 규칙이라 한다 .

Page 63: Let`s learn ! Effective C++

[Item 33] 상속된 이름을 가리지 않도록 주의하자 .

- 이 이름가리지 규칙은 scope 뿐만 아니라 상속 관계에 대해서도 유효하다 .

세 예제를 보면 쉽게 이해할 수 있을 것이다 .

- 컴파일러는 Derived 의 GetX() 함수에서 사용된 x 를 다음의 순서로 찾는다 .

1. 먼저 함수의 scope 에서 찾는다 .

2. 있다면 사용 . 없다면 Derived 의 멤버인지 확인한다 .

3. 역시 있다면 사용하고 , 없다면 기본 클래스인 Base 에서 찾는다 .

여기서 기억할 것은 Base 의 x 가

Derived 의 x 에 의해 가려질 수 있다는 것이다 .

Page 64: Let`s learn ! Effective C++

[Item 33] 상속된 이름을 가리지 않도록 주의하자 .

여기서 기억할 것은 Base 의 x 가 Derived 의 x 에 의해 가려질 수 있다는 것이다 .

- 이런 C++ 의 특성은 단순히 기본 클래스의 변수명을 가리는 것에서 그치지 않을 수 있다 .

- 함수 이름을 찾는 경우 , 일단 함수 이름만 가지고 유효범위를 빠져나오며

함수를 찾기 때문에 , 오른쪽 예제처럼 Derived 가 Base 의 mf1() 을 가리면

mf1(); 는 물론 , mf1(int i) 마저 가려 사용할 수 없게 된다 .

- 즉 , Bird 인 Eagle 이 Fly(int) 할 수 없게 된다는 것 .

is-a 관계가 깨지는 것이다 .

Page 65: Let`s learn ! Effective C++

[Item 33] 상속된 이름을 가리지 않도록 주의하자 .

- 이렇게 가려진 이름은 using 선언으로 끄집어낼 수 있다 .

d.mf1(); 호출은 Derived 의 mf1() 를 호출하며 ,

d.mf1(int); 호출은 Base 의 mf1(int) 를 호출한다 .

- 또다른 이슈가 있다 , Derived 에서 Base 의 mf1() 는 사용 가능하고 ,

mf1(int i) 를 사용하지 못하게 하려면 어떻게 해야할까 .

- 일단 이 경우 public 상속 관계 . is-a 관계는 깨지는 것이므로

private 상속 관계 상황을 가정해야 한다 .

그리고 Derived 에 mf1() 이라는 전달함수로 Base 의 mf1() 을 호출하게 하면

mf1(int i) 의 사용을 막을 수 있다 .

Page 66: Let`s learn ! Effective C++

[Item 34] 인터페이스 상속과 구현 상속의 차이를 식별하자 .

- 인터페이스 상속이란 함수의 선언부를 상속받는 것을 의미한다 .

그리고 구현 상속은 함수의 정의부를 상속받는 것 .

- 경우에 따라 인터페이스 상속만을 받는 경우 , 둘다 받는 경우 , 받지만 오버라이드 가능하게 하는경우 ,

반대의 경우 등등 여러 선택사항이 있을 수 있다 .

이들의 차이를 이해하고 알맞게 사용하려면 직접 case by case 를 따져보는 것이 중요하다 .

Page 67: Let`s learn ! Effective C++

[Item 34] 인터페이스 상속과 구현 상속의 차이를 식별하자 .

이들의 차이를 이해하고 알맞게 사용하려면 직접 case by case 를 따져보는 것이 중요하다 .

오른쪽과 같은 Shape 클래스에 대해 따져보자 .

- draw() 는 순수 가상 함수이기 때문에

하위 클래스는 이 함수를 다시 선언해야 한다 .

즉 , draw() 에 대해 인터페이스 상속을 받는다고 할 수 있다 .

그리고 이는 Shape 의 다양한 하위 클래스들이 고유한 draw() 함수를 가질 수 있게 한다 .

- 사실 Shape 의 순수 가상 함수 draw() 도 정의를 제공할 수 있다 .

단 , 구현이 붙은 순수 가상 함수를 호출하려면 클래스 이름을 한정자로 붙여야 한다 .

Shape::draw(); 이렇게 .

Page 68: Let`s learn ! Effective C++

[Item 34] 인터페이스 상속과 구현 상속의 차이를 식별하자 .

- 인터페이스만을 상속하는 draw() 와 달리 ,

단순 ( 비순수 ) 가상 함수인 error(~) 는

- 인터페이스 뿐만 아니라 그 함수의 기본 구현도

물려받게 한다 . 오버라이딩도 가능하다 .

이는 하위 클래스에서 함수를 다시 선언하지 않아도 함수를 사용할 수 있다는 편의성과

코드 재사용 가능성을 주지만 , 이런 자동성이 의도치 않은 결과를 초래할수도 있다 .

대신 편의성과 코드 재사용성을 남겨두고 , 자동성만을 배제하는 방법이 있다 .

Page 69: Let`s learn ! Effective C++

[Item 34] 인터페이스 상속과 구현 상속의 차이를 식별하자 .

대신 편의성과 코드 재사용성을 남겨두고 , 자동성만을 배제하는 방법이 있다 .

- 일종의 테크닉으로 , 가상 함수의 인터페이스와 그 가상 함수의 정의를 잇는 연결 관계를 끊는 것이다 .

아래는 그 방법 .

가상 함수를 순수 가상 함수로 바꾼다 . ( 선언 뒤에 = 0 을 붙인다 . )

protected 인 다른 이름의 함수를 하나 선언한다 . 가상 함수의 정의부분을 잘라내고 ,

새로 선언한 함수의 정의로 사용한다 .

순수 가상 함수이기 때문에 하위 클래스는 함수의 정의에 직접 접근할 수 없다 .

대신 받아온 순수 가상 함수에서 이 함수를 호출하게 해서 사용할 수 있다 .

Page 70: Let`s learn ! Effective C++

[Item 34] 인터페이스 상속과 구현 상속의 차이를 식별하자 .

- 앞의 방식처럼 기묘한 방식으로 얽힌 두개의 함수로 클래스의 네임 스페이스가 더럽혀지는 것이 ,

그다지 좋게 보이지 않을수도 있다 .

- 다행히도 , 이보다 직관적인 방식으로 같은 상속 환경을 제공하는 것이 가능하다 .

역시 순수 가상 함수를 사용한다 .

그리고 이 순수 가상 함수의 기본 제공 구현부를 작성한다 .

( 물론 순수 가상 함수의 구현부는 상속되지 않지만 . )

하위 클래스에서 순수 가상 함수의 인터페이스를 받게 하고 ,

구현부에서 상위 클래스의 이름을 한정자로 붙여 호출한다 .

이 방식으로도 기본 제공 함수를 제공받을 수 있다 .

Page 71: Let`s learn ! Effective C++

[Item 34] 인터페이스 상속과 구현 상속의 차이를 식별하자 .

- 끝으로 , 비가상 함수인 objectID() 를 따져보자 .

- 비가상 함수는 하위 클래스에서 오버라이딩이

불가능하다 . 즉 , 어떤 특수한 클래스라도 동일하게

동작해야 하는 경우에 사용하는 상속 함수다 .

이런 상속 관계들의 차이를 숙지하고 , 알맞게 사용하는 것이 좋은 코드 .

Page 72: Let`s learn ! Effective C++

[Item 35] 가상 함수를 대신할 것도 생각하는 자세를 기르자 .

가상 함수의 미미한 코스트를 그리 달갑게 여기지 않는 사람들도 있다 .

이런 분들의 코드를 읽기 위해서라도 가상 함수를 대신할 상속 방법을 생각해보자 .

- private 가상 함수를 사용한 구현부 상속 .

- 가상 함수를 private 로 하는 오른쪽과 같은 구현은 ,

하위 클래스에서 함수를 재정의할수도 있으며 func() 를 호출하기 때문에

사전 - 사후 동작이 가능하다 . 이는 다양한 가능성을 제공한다 .

뮤텍스 잠금을 건다든지 , 로그를 만든다던지 .

- 이런 방식을 비가상 함수 인터페이스 NVI 관용구 라고 부른다 .

Page 73: Let`s learn ! Effective C++

[Item 35] 가상 함수를 대신할 것도 생각하는 자세를 기르자 .

- 함수 포인터를 사용한 간접적인 상속

- 이는 디자인 패턴 중 하나인 전략 ( strategy ) 패턴의

응용 예이기도 하다 .

- 이 방식으로 특이한 융통성을 가질 수 있는데 ,

class c 를 상속받은 하위클래스의 객체 각각에 대해

서로 다른 함수를 제공할수도 있다 .

런타임 중에 다른 함수로 교체할 수도 있음 .

- 대신 이 함수는 멤버가 아니기 때문에 , 이 함수 내에서 멤버를 직접 접근할 수 없다는 단점이 있다 .

Page 74: Let`s learn ! Effective C++

[Item 35] 가상 함수를 대신할 것도 생각하는 자세를 기르자 .

- tr1::function 를 사용한 간접적인 상속

- tr1::function 타입의 객체는 함수호출성 개체

( 함수 포인터 , 함수 객체 혹은 멤버 함수 포인터 ) 를 가질 수 있고 ,

이를 사용한다면 앞의 멤버 접근 문제를 해결할 수 있다 .

- 앞의 방식과 구현상에 큰 차이는 없다 . 단지 class c 가

함수 포인터를 물게 했던 구조에서 , 이제는 좀더 일반화된

함수 포인터인 function 객체를 물게 했을 뿐이다 .

Page 75: Let`s learn ! Effective C++

[Item 35] 가상 함수를 대신할 것도 생각하는 자세를 기르자 .

- 가상 함수를 다른 클래스 계통의 가상 함수로 대체하는 방법

( 전략 패턴의 전통적인 구현 형태 )

대체할 가상 함수를 가진 클래스와 추가로 제공할

가상 함수를 가진 하위 클래스로 이루어진 클래스 계통을 만든다 .

그것을 호출할 가상 함수를 가진 기본 클래스가

앞의 클래스 계통을 멤버로 가지게 한다 .

- 표준적인 전략 패턴 구현에 친숙하다면 이해하기 쉬우며 ,

오른쪽의 경우 func 클래스 계통에 하위클래스를 추가하여

계산 함수를 커스텀할 수 있다 .

Page 76: Let`s learn ! Effective C++

[Item 36] 상속된 비가상 함수는 파생 클래스에서 재정의ㄴㄴ

- 비가상 함수는 , 하위 클래스에서 재정의할 경우 상위 클래스 함수의 이름을 가려버린다 .

이 때문에 오른쪽과 같은 경우에서 , 같은 D 의 참조자로 정의된

pb 와 pd 가 서로 다른 함수를 호출하는 경우가 생긴다 .

( 아래의 경우가 이름이 가려진 경우 . )

- Item 37 에서 자세히 다루겠지만 , 비가상 함수는

정적 바인딩으로 묶이며 , 가상 함수는 동적 바인딩으로 묶이기 때문에

생기는 문제이다 .

Page 77: Let`s learn ! Effective C++

[Item 37] 상속받은 기본 매개변수 값은 절대 재정의 ㄴㄴ

- 앞에서 말했듯이 가상 함수는 동적 바인딩 된다 .

하지만 기본 매개변수는 ? 정적으로 바인딩된다 .

- 오른쪽과 같이 기본 매개변수가 상위 클래스의 것으로

입력되는 문제가 생길 수 있다 .

- 객체의 동적 타입이란 , 그 객체의 실체에 따라 결정되는 타입이다 .

가상 클래스의 경우 생성한 직후에 , 아무런 실체를 가지지 않는다 .

그리고 프로그램 실행 중 하위 클래스의 포인터를 대입하는 것으로

타입이 결정된다 . 런타임 중에 타입이 결정되기 때문에 ,

상대적으로 느리다 . 이것이 동적 바인딩 .

Page 78: Let`s learn ! Effective C++

[Item 37] 상속받은 기본 매개변수 값은 절대 재정의 ㄴㄴ

- 기본 매개변수를 사용하는 것은 이런 문제도 있지만 ,

함수 사용법을 모호하게 만들기도 한다 .

- 예를 들어 , 기본 매개변수를 써도되는 경우와 안되는 경우가 있을 때 ,

기본 매개변수를 사용한 코드를 보고 , 매개변수를 인자로 넘겨야 하는 상황에서도

인자를 안넣어도 된다는 오해를 불러일으킬 수 있다 .

- 기본 매개변수를 사용하는 것은 당장의 편의를 주지만 ,

유지보수 측면에서 불리하니까 사용하지 않는게 좋다 .

( Effective C++ 에 기재된 내용은 아니지만 . )

Page 79: Let`s learn ! Effective C++

[Item 39] private 상속은 필요할 때만 쓰자

- private 상속을 하면 상위 클래스의 모든 public, protected 멤버가 private 멤버로 상속된다 .

즉 , 이 상속은 상위 클래스에서 쓸 수 있는 기능을 하위 클래스에서도 쓰기 위함이지 ,

하위 클래스는 상위 클래스라는 is-a 관계가 아니라는 것이다 .

그리고 이런 기능을 사용하기 위한 관계를 is-implemented-terms-of 관계라 한다 .

- Item 38 의 객체 합성을 통한 imple 관계와의 차이점은 ,

private 멤버에도 접근할 수 있다는 것과 가상 함수를 사용하여 재정의할 수 있다는 것이다 .

이런 경우를 제외한 경우엔 객체 합성을 사용하자 .

Page 80: Let`s learn ! Effective C++

[Item 40] 다중 상속은 필요할 때만 쓰자

- 다중 상속의 가장 큰 약점 (?) 의 하나로 , 모호성이 있다 .

하나의 클래스에 대해 , 두개의 상위 클래스가 상동하는 멤버를 가지고 있으면 ,

둘 중 어느 쪽을 사용할 지 모호해진다 .

그러니 함수를 호출할 때 접근제어 지시자 ( :: ) 를 통해 어느쪽을 호출할 지 정해야 한다 .

- 다른 문제로 다이아몬드 상속이 있다 .

위처럼 두개의 상위 클래스를 가지고 있는데 , 그 상위 클래스들이 각각 상위 클래스를 가지고 있고 ,

그 클래스가 서로 같다면 다이아몬드 꼴의 클래스 구조가 된다 .

이 때 가장 하위의 클래스는 두 상위 클래스 ( 어떤 상위클래스를 공유하는 ) 로부터 ( 어떤 상위클래스의 )중복된 두개의 멤버 받게 된다 . 이렇게 중복된 데이터 멤버를 갖도록 의도한 것이 아니었다면 ,

최상위 클래스를 가상 기본 클래스로 만드는 것으로 해결할 수 있따 .

Page 81: Let`s learn ! Effective C++

[Item 40] 다중 상속은 필요할 때만 쓰자

- 근데 가상 상속은 비싸다 . 또한 가상 기본 클래스의 초기화 규칙은

비가상의 경우 보다 훨씬 복잡하다 . 그니까 왠만하면 가상 기본 클래스의 사용은 자제하자 .

쓰더라도 왠만하면 데이터 멤버는 넣지 않는 쪽으로 신경을 쓰자 . 그러는 것이 초기화가 편함 .

- 다중 상속은 대체 어떨때 쓰일까 .

일례로 , 인터페이스 클래스로부터 public 상속을 시킴과 동시에

구현을 돕는 클래스로부터 private 상속을 시키는 것이다 .

Page 82: Let`s learn ! Effective C++

Chapter 8 new 와 delete 를 내 맘대로

Page 83: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

- 자바나 닷넷 등 가비지 컬렉션을 지원하는 환경들과 달리 ,

C++ 은 메모리 관리를 사용자가 해줘야 한다 .

그만큼 속도면에서 이득을 볼 수 있지만 !

이는 new ( 메모리 할당 ), delete ( 메모리 해제 ) 를 직접 요청해야 한다는 것이니 ,

메모리 관리 요청에 대한 실패 등의 처리에도 신경을 써줘야 한다 .

Page 84: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

< new 처리자에 대한 이해 >

- 사용자가 operator new 함수를 통해 메모리 할당을 요청했는데 ,

메모리 부족으로 할당을 실패했을 때 , 함수는 예외를 던진다 .

- 근데 메모리가 부족할걸 알았으면 , 예외를 던지기 전에 처리해주면 되지 않는가 ?

C++ 은 이런 처리를 위해 new 처리자 ( new-handler ) 라는 처리 함수를 제공한다 .

- 이 처리 함수는 메모리 할당에 실패하면 예외를 던지기 전에 호출된다 .

set_new_handler() 함수를 통해 호출할 함수를 사용자가 정할 수 있다 .

Page 85: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

< set_new_handler() >

이 함수의 인자로 들어가는 것은 , 반환과 매개변수가 없는 함수의 포인터다 .

인자로 받은 함수 포인터를 다시 반환하는 형태로 되어있다 .

예외를 던지지 않을 것이라 선언되어 있다 .

이 함수를 호출하면 , 이후의 메모리 할당 예외 직전에

매개변수로 들어간 함수가 자동으로 호출된다 .

그렇다면 예외처리 함수는 어떻게 설계해야 할까

Page 86: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

< new 처리자 함수 설계 >

- 다음의 5 가지 방법 중 임의 하나의 방법으로

프로그램에 좋은 영향을 미치도록 처리함수를 작성해야 한다 .

메모리를 더 확보하여 메모리 할당을 성공하게 한다 .

일례로 , 프로그램 시작 시 큰 메모리 블록 하나를 할당해놓고 ,

new 처리자가 처음 호출될 때 그 메모리를 사용하도록 하는 방법이 있다 .

다른 처리자를 설치한다 .

new 처리자 안에서 set_new_handler 함수를 호출하여 ,

메모리를 확보할 수 있는 다른 new 처리자를 설치하는 방법 .

Page 87: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

처리자의 설치를 제거한다 .

set_new_handler 에 nullptr( 널 포인터 ) 를 넘겨 그냥 예외를 던지게 한다 .

임의로 예외를 던진다 .

bad_alloc 또는 여기서 파생된 타입의 예외를 던지는 방법으로 ,

커스텀 예외를 사용할 수 있다 .

프로그램을 종료한다 .

Page 88: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

- 앞의 다른 처리자를 설치하는 방법을 활용해서 .

다양한 객체의 메모리 할당 실패에 대해 ,

서로 다른 알맞은 처리자를 설치하는 방법을 알아보자 .

- set_new_handler 라는 넘어온 함수 포인터를 저장하는 함수를 만든다 .

return 으로 저장했던 함수 포인터를 넘겨준다 .

Page 89: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

전역 operator new 호출로 메모리 할당하려고 하면 ,

1. 전역 new 처리자를 Widget 의 new 처리자로 설치한다 .

2. 객체의 처리자로 설치된 상태로 operator new 가 호출된다 .

- 할당이 성공하고 호출이 종료되면 전역 처리자를 바뀌기 전으로 복원해준다 .

- 할당이 실패할 경우에는 bad_alloc 예외를 던지고 , 전역 처리자를 복원하게 한다 .

- 이 전역처리자를 쉽고 안전하게 다루기 위해

자원관리 객체를 사용하여 관리하면 좋다 . 자세한 내용은 Item 14 참고 .

Page 90: Let`s learn ! Effective C++

[Item 49] 처리자의 동작 원리를 제대로 이해하자 .

- 그래서 이거 어떻게 써요 ?

- 근데 생각해보면 , 어떤 클래스를 쓰더라도 자원관리 객체를 통한 예외처리 방식은

앞의 Widget 을 정의한 부분과 거의 비슷하게 구현될 것 같다 .

- 여러번 구현할 것 없이 . 이 기능을 가진 상위클래스를 만들고 ,

기능을 사용할 클래스를 하위 클래스로 하여 기능을 물려받게 하면 된다 .

이를 클래스 템플릿을 사용하여 객체에 따라 currentHandler 멤버를 가질 수 있게 구현하면 완벽 .

- 템플릿을 이처럼 사용하는 패턴을 CRTP 이라 부른다 .

Page 91: Let`s learn ! Effective C++

[Item 50] new 와 delete 는 언제 바꿔야 할까

new 와 delete 를 어떨 때 바꿔야 할 지 생각해보자 .

잘못된 힙 사용 & 대책 new 한 메모리를 delete 하지 않는 경우 delete 한 메모리를 다시 delete 하는 경우

operator new 가 할당된 메모리 주소의 목록을 기억하고 ,

operator delete 가 그 목록에서 하나씩 해제하게 구현하는 방식으로 위같은 실수에 대비할 수 있다 .

할당된 메모리 블록의 끝을 넘는 오버런과 반대의 경우인 언더런 .

메모리를 약간 더 할당하여 , 앞 뒤에 오버런 / 언더런을 탐지하는 바이트 패턴을 기록하여 방지가 가능 .

Page 92: Let`s learn ! Effective C++

[Item 50] new 와 delete 는 언제 바꿔야 할까

new 와 delete 를 어떨 때 바꿔야 할 지 생각해보자 .

효율을 향상시키기 위해

기본 제공 new delete 는 대부분의 상황에 대처할 수 있는 보편성에 특화되어 있다 .

이는 개발 환경이나 프로그램의 특성에 따라 기본 new delete 가 부적합할 수 있음을 의미한다 .

속도가 중요할수도 있고 , 메모리 용량이 부족할수도 있다 .

환경에 맞도록 메모리를 사용하자 .

Page 93: Let`s learn ! Effective C++

[Item 50] new 와 delete 는 언제 바꿔야 할까

new 와 delete 를 어떨 때 바꿔야 할 지 생각해보자 .

동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해

커스텀 new, delete 는 로그를 남겨 메모리 사용 현황을 확인할 수 있다 .

메모리 사용 분포 . 시점 . 순서 등 자신이 작성한 프로그램의 메모리 사용 특성을 파악해두면 ,

cache 가 얼마나 효율적으로 사용되는지 . 메모리가 부족하다면 그 양은 얼마인지 .

메모리의 단편화는 어떤지 . 언제 메모리 할당에 의해 부하가 걸리는지 . 등등

다양하게 응용될 수 있다 .

Page 94: Let`s learn ! Effective C++

[Item 50] new 와 delete 는 언제 바꿔야 할까

new 와 delete 를 어떨 때 바꿔야 할 지 생각해보자 .

할당 & 해제 속력을 높이기 위해

기본 제공 할당자는 커스텀 할당자보다 꽤 느린 경우가 적지 않다 .

특히 커스텀 할당자가 특정 타입에 맞추어 설계된 경우 차이가 크다 .

임의의 관계를 맺는 객체들을 모으기 위해

동시에 쓰이는 특정 자료구조들의 메모리 군집화는 ,

page fault 와 cache miss 를 줄여 속도 향상에 엄청난 도움을 줄수도 있다 .

Page 95: Let`s learn ! Effective C++

[Item 50] new 와 delete 는 언제 바꿔야 할까

- 근데 new, delete 를 막상 바꾸려해도 문제가 많다

Item 51 에서 이야기할 operator new 함수를 만들 때의 관례 나

바이트 정렬 문제같은 조건을 을 갖추는건 제법 어려운 일 .

그러니 반드시 필요한 경우가 아니면 굳이 엎치락 뒤치락 생고생 하지 말자 .

대부분의 경우 기본 제공 new, delete 만 하더라도 충분한 기능을 제공하며 ,

상업용 제품이나 오픈 소스로 뛰어난 메모리 관리자를 지원받을수도 있다 .

오픈 소스의 대표적 예로 boost::pool 라이브러리가 있다 .

그럼에도 직접 구현해보고자 하는 경우 , 오픈소스나 컴파일러 문서를 보는것이 큰 도움이 될 것 .

Page 96: Let`s learn ! Effective C++

[Item 51] new, delete 를 작성하려면 기존의 관례를 잘 알아두자 .

- operator new 의 관례 메모리가 충분하면 , 할당된 메모리의 위치 ( 포인터 ) 를 반환해야 한다 .

할당이 실패하면 new 처리자 함수를 호출하게 한다 .

new 처리자에서 적당한 대책을 내놓을 때까지 이를 무한반복한다 .

0 바이트 메모리 요청에 대한 대비책이 있어야 한다 .

중복 위치 할당을 막기 위해서 1 바이트로 처리하던가 해야한다 .

기본 new 가 가려지지 않게 한다 . 이는 Item 52 에서 자세히 .

Page 97: Let`s learn ! Effective C++

[Item 51] new, delete 를 작성하려면 기존의 관례를 잘 알아두자 .

- operator new 는 하위 클래스 쪽으로 상속이 되는 함수다 .

이는 하위 클래스를 위해 메모리를 할당하는데 상위 클래스의 new 가 호출될수 있음을 의미한다 .

- 이에 대한 근본적인 ( 전체 설계를 바꾸지 않아도 되는 ) 해결방법은 ,

커스텀 operator new 호출 시 잘못된 메모리 크기가 들어올 경우

기본 operator new 를 호출하게 하는 것이다 .

- 오른쪽 코드에서 0 크기 검사는

객체의 최소크기가 1Byte 라는 C++ 의 특성에 의해

하지 않아도 된다 .

Page 98: Let`s learn ! Effective C++

[Item 51] new, delete 를 작성하려면 기존의 관례를 잘 알아두자 .

- operator new[] 의 구현

배열에 대한 메모리 할당의 경우 , 이 시점에서 알 수 있는 것은 할당할 공간의 크기 뿐이다 .

1. 상속 관계에서 상위 클래스의 생성자를 호출함으로인해 크기가 다를 수 있으며 ,

2. 인자로 넘어가는 size_t 타입의 인자는 ,

원소의 수를 담는 것에 추가로 메모리를 사용하기 때문에

배열에 몇 개의 객체가 들어있는지 이 시점에서는 알 수 없다 .

- operator delete 의 관례

삭제될 메모리의 크기를 확인하는 코드만 추가하면 된다 .

크기가 틀릴 경우엔 표준 operator delete 가 삭제를 맡도록 하게 하면 된다 .

Page 99: Let`s learn ! Effective C++

[Item 52] 위치지정 new 작성하려면 위치지정 delete 도 준비하자 .

- 다음과 같은 new 표현식에선

operator new 함수가 먼저 호출된 뒤 ,

Widget 의 기본 생성자가 호출된다 .

- 근데 만약 operator new 로 메모리는 할당이 됬는데 ,

Widget 생성자에서 예외가 발생해서 객체를 생성하지 못하는 경우 .

- pw 에 포인터 ( 메모리가 할당된 위치 ) 가 대입되지 않기 때문에

delete pw 로 메모리를 해제할 수 조차 할 수 없다 .

즉 , 메모리 할당 자체를 취소시키는 수밖에 없다 . 그런데 어떻게 ?

Page 100: Let`s learn ! Effective C++

[Item 52] 위치지정 new 작성하려면 위치지정 delete 도 준비하자 .

- 사실 이 메모리 할당을 되돌리는 일은 C++ 런타임 시스템이 알아서 해준다 .

하지만 다 알아서 하는것은 아니고 .

- 단지 호출한 new 와 짝이 되는 버전의 delete 함수를 호출해주는 일만 한다 .

기본 operator new 은 기본 operator delete 와 짝이기 때문에 별 상관없지만 ,

- 기본형이 아닌 new 라면 그에 대한 짝 delete 가 준비된 상태여야 한다 .

기본형이 아닌 operator new ?

Page 101: Let`s learn ! Effective C++

[Item 52] 위치지정 new 작성하려면 위치지정 delete 도 준비하자 .

잠깐 용어설명하고 넘어가자면

기본형이 아닌 operator new 란 placement new 이다 .

- operator new 함수를 작성하는 경우 , 매개변수를 추가로 받는 형태로 선언할 수 있다 .

그리고 이 형태중 가장 유용한 것은 , 객체를 생성시킬 위치를 매개변수로 받는 것 .

즉 , 추가 매개변수는 대부분 위치를 받는 경우이기 때문에 .

- 매개변수를 추가로 받는 비 기본형 operator new 함수를 위치지정 (placement) new 라고 부른다 .

Page 102: Let`s learn ! Effective C++

[Item 52] 위치지정 new 작성하려면 위치지정 delete 도 준비하자 .

정리 +

1. 기본 operator new 로 메모리를 할당하고 Widget 에서 예외가 발생하면 ,

C++ 런타임 시스템이 알아서 기본 new 와 짝이 되는 기본 operator delete 를 호출하여 할당을 취소한다 .

2. 근데 위치지정 new 로 메모리를 할당하고 예외가 발생한 경우엔 ,

짝이 되는 위치지정 delete 를 클래스 내에서 정의한 상태여야 C++ 런타임 시스템이 이를 호출해준다 .

그러니까 위치지정 new 작성시엔 위치지정 delete 도 준비해야 한다 .

3. 만약 예외가 발생하지 않고 제대로 객체가 생성되고 사용하고 , delete 하는 경우엔 ?

그냥 기본 operator delete 가 호출되어 객체가 해제된다 .

Page 103: Let`s learn ! Effective C++

[Item 52] 위치지정 new 작성하려면 위치지정 delete 도 준비하자 .

- C++ 가 전역 유효 범위에서 제공하는 operator new 는 세 가지의 표준 형태가 있다 .

- 어떤 형태이든 간에 operator new 가 클래스 안에서 선언되는 순간 ,

클래스 내에서 위의 표준 형태들은 가려져 모두 사용할 수 없게 된다 .

- 사용할 수 없게 만들 목적이 아니라면 , 위 세 가지 형태 모두 선언해주는 것이 좋다 .

물론 이에 짝이 되는 delete 에 대해서도 세 가지를 선언해줘야 한다 .

Page 104: Let`s learn ! Effective C++

[Item 52] 위치지정 new 작성하려면 위치지정 delete 도 준비하자 .

- 아나 . 위치지정 new 하나 작성하려한건데 총 3+3=6 개나 선언해줘야 된다 .

선언한 함수에서 전역 범위의 operator new, 예외불가 new 를 호출하게 하면 되는데 ,

아래 방법을 사용하면 일일히 정의하지 않아도 된다 .

1. 기본 클래스를 하나 만들고 , 이 안에 new, delete 기본형태 6 가지를 전부 넣는다 .

2. 사용자 정의 형태를 사용하려는 클래스에서 이 기본 클래스를 상속받도록 한다 .

3. using 선언으로 기본 클래스에서 정의한 표준 형태를 가져오고 ,

원하는 사용자 정의 형태를 선언한다 .

Page 105: Let`s learn ! Effective C++

[ 끝 ]