이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 132p ~ 142p
요약
항목 16: new 및 delete를 사용할 때는 형태를 반드시 맞추자
- new 연산자를 사용해서 어떤 객체를 동적 할당하면 내부적으로는 두 가지가 진행된다.
- 메모리가 할당된다.
- 할당된 메모리에 대해 한 개 이상의 생성자가 호출된다.
- delete 연산자를 사용할 때에는 내부적으로 다음과 같은 동작이 진행된다.
- 기존에 할당된 메모리에 대해 한 개 이상의 소멸자가 호출된다.
- 메모리가 해제된다.
- 이때, delete 연산자가 적용되는 객체의 수는 소멸자가 호출되는 횟수와 같다.
삭제되는 포인터는 객체 하나만 가리킬까, 아니면 객체의 배열을 가리킬까?
- new로 힙에 만들어진 단일 객체의 메모리 배치구조는 객체 배열에 대한 메모리 배치구조와 다르다.
- 배열을 위해 만들어지는 힙 메모리는 대개 배열원소의 개수가 박혀 들어간다.
- 이 때문에 delete 연산자는 소멸자가 몇 번 호출될지를 알 수 있다.
- 단일 객체용 힙 메모리에는 이런 정보가 없다.
// 한 개의 객체
// [ object ]
// 객체의 배열
// [ n ] [ object ] [ object ] [ ...
- 따라서 어떤 포인터에 대해 delete를 적용할 때, 단일 객체인지 아니면 배열인지 알려주어야 한다.
std::string *stringPtr1 = new std::string;
std::string *stringPtr2 = new std::string[100];
delete stringPtr1;
delete[] stringptr2;
- new 표현식에
[]
를 사용했으면, 여기에 대응되는 delete 표현식에도[]
를 사용하면 된다.
항목 17: new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int prioirty);
- 처리 우선순위에 따라 처리를 적용하는 함수가 있다.
// 컴파일 오류
// tr1::shared_ptr의 생성자는 explicit이기 때문에 new Widget으로 만들어진 포인터가
// tr1::shared_ptr 객체로 암시적 변환이 안됨
processWidget(new Widget, priority());
// 정상 컴파일
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
- 자원 관리 객체를 사용하고 있는데도, 자원을 흘릴 가능성이 있다.
- 컴파일러는
processWidget
호출 코드를 만들기 전에 우선 이 함수의 매개변수로 넘겨지는 인자를 평가하는 순서를 밟는다.- 두 번째 인자는
priority
함수의 호출문만 있다.(한 부분) - 첫 번째 인자는 두 부분으로 나누어져 있다.
new Widget
표현식 실행tr1::shared_ptr
생성자 호출
- 결과적으로
processWidget
함수 호출을 위해 세 가지 코드를 만들어야 한다.
- 두 번째 인자는
- 문제는 각각의 코드가 실행되는 순서는 컴파일러 제작사마다 다르다.
- 어떤 컴파일러에서 다음과 같은 순서로 코드가 호출된다고 가정해보자.
new Widget
→priority
→tr1::shared_ptr
priority
함수를 실행하는 과정에서 예외가 발생하면new Widget
으로 만든 포인터가 유실될 가능성이 있다.
- 어떤 컴파일러에서 다음과 같은 순서로 코드가 호출된다고 가정해보자.
- 따라서 Widget을 생성해서 스마트 포인터에 저장하는 코드를 별도의 문장으로 만들고, 그 포인터를
processWidget
에 넘겨야 안전하게 자원을 관리할 수 있다.
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
4. 설계 및 선언
- 소프트웨어 설계에서 가장 중요한 지침은
제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게
이다. - 이 틀 위에서 정확성, 효율, 캡슐화, 유지보수성, 확장성, 그리고 규약 준수에 이르는 다양한 문제에 대한 구체적인 지침들이 있다.
항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
- 이상적으로는 어떤 인터페이스를 어떻게 써 봤는데, 결과 코드가 사용자가 생각한 대로 동작하지 않는다면 그 코드는 컴파일되지 않아야 맞다.
- 반대로 어떤 코드가 컴파일이 되었다면 그 코드는 사용자가 원하는 대로 동작해야 할 것이다.
제대로 쓰기엔 쉽고, 엉터리로 쓰기에 어려운
인터페이스를 개발하려면 우선 사용자가 저지를 만한 실수의 종류를 머리에 넣어두고 있어야 한다.
class Date {
public:
Date(int month, int day, int year);
}
- 언뜻 보기에는 문제가 없어 보이는 인터페이스이지만, 다음과 같은 상황에서 문제가 발생할 수 있다.
// 1. 매개변수의 전달 순서가 잘못될 여지가 있다.
Date d(30, 3, 1995);
// 2. 월과 일에 해당하는 숫자가 어이없는 숫자일 수 있다.
Date d(3, 40, 1995);
- 이렇게 사용자 실수에 가까운 문제들은 인터페이스를 강화하면 막을 수 있다.
- 어처구니 없는 코드가 컴파일되는 부조리로부터 우리들을 지켜주는 방법이 바로 타입 시스템이다.
struct Day {
explicit Day(int d): val(d) {}
int val;
}
struct Month {
explicit Month(int m): val(m) {}
int val;
}
struct Year {
explicit Year(int y): val(y) {}
int val;
}
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
};
// 에러! 타입 오류
Date d(30, 3, 1995);
// 에러! 타입 오류
Date d(Day(30), Month(3), Year(1995));
// 정답
Date d(Month(3), Day(30), Year(1995));
- 이렇게 적절한 타입만 제대로 준비되어 있으면, 각 타입의 값에 제약을 가하더라도 괜찮은 경우가 생긴다.
- Month의 경우 유효한 값은 12개뿐이기 때문에 이 제약을 이용할 수 있다.
enum
을 사용하는 방법도 있지만,enum
은 int 처럼 사용될 수 있기 때문에 타입 안정성이 뛰어난 방법은 아니다.- 타입 안정성을 지키기 위해선 유효한 집합을 미리 정의해 두어도 괜찮다.
class Month {
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
static Month Dec() { return Month(12); }
private:
explicit Month(int m);
}
- 특정한 월을 나타내는 데 객체를 쓰지 않고 함수를 쓴 것은 비지역 정적 객체의 초기화 시점을 컴파일 단계에서 명확하게 파악할 수 없기 때문이다.
메모
항목 16: new 및 delete를 사용할 때는 형태를 반드시 맞추자
- new 표현식에
[]
를 썼으면, 대응되는 delete 표현식에도[]
를 사용해야 한다.
항목 17: new로 생성한 객체를 스마트 포인터에 저장한느 코드는 별도의 한 문장으로 만들자
- new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만들자. 이것이 안 되어 있으면 예외가 발생할 때 디버깅하기 힘든 자원 누출이 초래될 수 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(149p ~ 160p) (0) | 2024.01.19 |
---|---|
이펙티브 C++(142p ~ 149p) (0) | 2024.01.18 |
이펙티브 C++(121p ~ 132p) (0) | 2024.01.16 |
이펙티브 C++(111p ~ 121p) (0) | 2024.01.15 |
이펙티브 C++(99p ~ 111p) (0) | 2024.01.14 |