3. 들어가기에 앞서
- 가비지 컬렉터가 없는 C++, 불편하지만 이점도 있다
- 메모리를 수동으로 관리하고 싶다!면 C++!
- Operator new, delete 문제, 다중쓰레드에서의 메모리 관리 문제등등!
- 이번 챕터를 통해서 해결해보자
- STL 컨테이너와 관련된 메모리 관리도!
5. Item 49 : new 처리자의 동작원리를 제대로 이해하자
- operator new를 통해 메모리를 획득하지 못한 경우(즉, 할당할 메모리가 없을 때)
- operator new는 예외를 던진다
- 메모리 할당이 제대로 되지 못한 경우,
예외를 던지기 전에 에러 처리 함수를 먼저 호출하게 되어있다
이 에러 처리 함수를 new 처리자(할당 에러 처리자)라 한다
- 표준에는 이 new 처리자를 사용자가 지정해 줄 수 있는 함수가 존재한다
- 바로 set_new_handler
6. Item 49 : new 처리자의 동작원리를 제대로 이해하자
- set_new_handler의 사용 방법
void outOfMem (){
std : : cerr << "Unable to sat isfy r equest for memoryn ";
std : : abort ( ) ;
}
-----------------------------------------------------------------------------------------------------
int main (){
std : : set_ new_ handler (outOfMem) ; // 사용자 정의 new handler 설정
int *pBigDataArray = new int [1 00000000L] ;
}
1억개의 정수가 할당되지 못하면? outOfMem이 호출된다. 그리고 std::abort에 의해
프로그램이 종료된다.
7. Item 49 : new 처리자의 동작원리를 제대로 이해하자
- new 처리자 함수의 지향점
1. 사용할 수 있는 메모리를 더 많이 확보하는 쪽
2. 다른 new 처리자를 부르는 쪽 -> 현재의 new 처리자로는 메모리를 할당할 수 없으므
로
3. new 처리자를 제거하는 쪽 -> 바로 예외를 던짐
4. 제거고 뭐고 그냥 예외를 던지는 쪽
5. 프로그램을 끝내는 쪽
- 위 5개 중 한 쪽을 택해 new 처리자를 구현해야 한다.
8. Item 49 : new 처리자의 동작원리를 제대로 이해하자
- 클래스 별로 다른 new 처리자를 만들고 싶다면? ( Widget의 예를 통해 알아 보자)
class Widget (
public :
static std: :new_handler set_new_handler(std: :new_handler p) throw();
static void * operator new(std: :size_t size) throw(std : : bad_ alloc) ;
private :
static std::new_handler currentHandler; // static이니 구현은 cpp에서 해준다
}
static을 사용하는 이유는 new를 실행하다 실패하면 new_handler를 부르기 때문에
Widget 생성되기 전에 존재해야 한다.
9. Item 49 : new 처리자의 동작원리를 제대로 이해하자
- 클래스 별로 다른 new 처리자를 만들고 싶다면? ( Widget의 예를 통해 알아 보자)
class Widget (
public :
static std: :new_handler set_new_handler(std: :new_handler p) throw();
static void * operator new(std: :size_t size) throw(std : : bad_ alloc) ;
private :
static std::new_handler currentHandler; // static이니 구현은 cpp에서 해준다
}
static을 사용하는 이유는 new를 실행하다 실패하면 new_handler를 부르기 때문에
Widget 생성되기 전에 존재해야 한다.
10. Item 49 : new 처리자의 동작원리를 제대로 이해하자
- Widget::set_new_handler 구현 부분
std : :new_handler Widget : : set_new_handler(std : :new_handler p) throw() {
std : :new_handler oldHandler = currentHandler ;
currentHandler = p ;
// 원래 표준 set_new_handler처럼 인자를 currentHandler로 설정하고
// old를 반환한다.
return oldHandler ;
}
11. Item 49 : new 처리자의 동작원리를 제대로 이해하자
Widget만의 operator new 구현하기 위해 new Handler 관리 클래스 선언
class NewHandlerHolder {
public :
explicit NewHandlerHolder(std : :new_handler nh) : handler(nh) {}
~NewHandlerHolder()
{ std: :set_new_handler(handler) ; } // 소멸자에서 하는 작업 뒤에서 설명
private :
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder&) ; // 복사 막기위해
NewHandlerHolder&
operator=(const NewHandlerHolder&);
}
12. Item 49 : new 처리자의 동작원리를 제대로 이해하자
Widget만의 operator new 구현
void * Widget : :operator new(std: : size_t size) throw(std : :bad_alloc) {
// set_new_handle의 반환 값은 기존의 new 처리자이다.
// 따라서 NewHandlerHolder::handler는 기존의 new 처리자를 갖는다
NewHandlerHolder h(std: : set_new_handler(currentHandler)) ;
// 표준 할당자로 할당함
return : :operator new(size);
}
// NewHandlerHolder의 소멸자에서 기존의 new 처리자로 되돌리는 것을 알 수 있다.
이후에는 템플릿으로 만드는 법이 나온다. 이는 다음에 자세히 살피고 이번 Item을 넘어가
자
14. Item 50 : custom operator new, delete를 사용하는 경우
1. 잘못된 힙 사용을 탐지하기 위해
- operator new에서 할당된 메모리 주소의 목록을 유지
- operator delete에서 그 목록으로부터 하나씩 제거
- 한 번 new한 객체를 두 번 delete하는 경우가 없게!
- overrun(할당된 메모리 블록 끝을 넘어 기록하는 것)
- underrun(할당된 메모리 블록을 앞서 기록하는 것)
- over,under run 문제 방지
15. Item 50 : custom operator new, delete를 사용하는 경우
2. 효율 향상을 위해
- 컴파일러가 기본으로 제공하는 new, delete는 다양한 요구사항을 맞추기 위해 무난하
게 동작함
- 개발자가 자신에 맞는 동적 메모리 사용 방식을 정확히 안다면 `우수한 성능`의 new,
delete 만들기 가능
- 실행속도가 빠르고(최대 10의 n제곱배) 메모리도 적게 차지하는(50%만 사용) 버전 만
들기 가능
16. Item 50 : custom operator new, delete를 사용하는 경우
3. 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당되는 메모리 블록의 크기 값들에 대한 정보
- 메모리 할당 순서가 FIFO, LIFO 인지
- 각 실행단계에서의 메모리 블록 할당이 어떻게 이루어지고 있는지
- 등등의 정보를 모으기 위해
17. Item 50 : custom operator new, delete를 사용하는 경우
3. 동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해
- 할당되는 메모리 블록의 크기 값들에 대한 정보
- 메모리 할당 순서가 FIFO, LIFO 인지
- 각 실행단계에서의 메모리 블록 할당이 어떻게 이루어지고 있는지
- 등등의 정보를 모으기 위해
18. Item 50 : custom operator new, delete를 사용하는 경우
여기서 잠깐! operator new를 만들 때 주의할 점 : 바이트 정렬(alignment)
- 컴퓨터는 대부분의 경우 특정 종류의 메모리 주소를 시작 주소로 하여 저장될 것을 요
구한다
- 무슨 말이고? 만약 4의 배수를 기준으로 각 객체가 저장되어야 한다면 객체들의 시작
주소는 언제나 0, 4, 8 ,c로 끝난다.
- 컴퓨터에 따라서 8byte일 수도, 4byte일 수도, 조금 다른 수 일 수 도 있다.
- 어떤 아키텍처이냐에 따라 바이트 정렬이 안되면 프로그램이 실행되다가 하드웨어 예
외를 일으킬 수 있다
- x86은 어떤 바이트 단위에 맞추더라도 실행할 수 있지만 8바이트 단위로 정렬하면 런타
임 접근속도가 훨씬 빨라진다
19. Item 50 : custom operator new, delete를 사용하는 경우
4. 할당 및 해제 속력을 높이기 위해
- 기본 제공 new는 느린 경우도 많다
- 특정 크기를 할당할 일이 많으면 직접 만들어 속도에서 향상을 만들어 낼 수 있다
20. Item 50 : custom operator new, delete를 사용하는 경우
5. 임의의 관계를 맺고 있는 객체들을 한 군데에 나란히 모아놓기 위해
- 특정 자료 구조 몇 개가 동시에 쓰이고 이들에 대해서 page fault 발생 횟수를 최소로 하
고 싶을 때
- 해당 자료구조를 담은 힙을 할당해 준다.
6. 원하는 작업을 수행하도록 하기 위해
- operator new, delete에서 해주고 싶은 작업이 있으면 자기가 새로 만들어야 한다
22. Item 51 : new 및 delete 관련 관례들
Item 50을 통해 언제 new, delete를 새로 만들어야 할지 알았다.
이번에는 어떻게 만들까의 문제
먼저 operator new의 경우
반환 값 문제
- 요청한 메모리를 마련해 줄 수 있다면 그 메모리에 대한 포인터를 반환
- 마련해 줄 수 없다면 Item 49를 다시 보자
23. Item 51 : new 및 delete 관련 관례들
- 반환 값을 어떻게 해줄것인가의 문제 말은 쉽다. 구현은?
void * operator new(std : : size_ t size) throw(std : :bad_alloc)
{
using namespace std ;
if (size == 0) {size = 1; } // size 0 일 때도 적법한 주소를 반환해야 한다는 정책 때문에
while (true) { // 무한루프 가능성 있음, new 처리자는 어떤 식으로든 마침표를 찍어야함
if( 할당이 성공했음) { return ( 할당된 메모리에 대한 포인터 ) ; }
// 할당 실패시
new_handler globalHandler = set_new_handler(O) ; // 원래 new 처리자 획득
set_new_handler(global Handler ) ;
If (globalHandler) (*globalHandler) () ; // new 처리자에서 뭔가 해주길 바람
else throw std : : bad_alloc() ; // Item 49의 지향점 부분 참조
}
}
24. Item 51 : new 및 delete 관련 관례들
operator new는 상속 대상
만약 Base와 Derived의 크기가 다르면?
operator new는 Base의 크기만 할당해줄 뿐이다.
그럴 때는
if(size != sizeof(Base)) // size는 operator new로 전달받은 Derived 클래스의 크기
return ::operator new(size); // 크기가 다르면 기본 new 사용
25. Item 51 : new 및 delete 관련 관례들
- operator new[]는 웬만하면 하지 말자.
- operator new[]는 상속되어 사용될 수 있다.
- 이 때 operator new[]의 할당 크기를 정할 수 없다.
- 왜냐하면 상속된 클래스의 경우 Base와 크기가 다를수 있기 때문이다.
- 요구되는 바이트 수 / sizeof(Base) 해도 만들기 원하는 객체의 개수가 몇 개인지 알수 없
다
- 그리고 실제로 operator new[]로 전달된 size보다 더 큰 size가 실제 사이즈일 수 있다.
- 배열에서는 앞 뒤로, 필요한 정보를 더 저장해야 하는 경우가 있기 때문이다.
27. Item 52 : 위치지정 new와 delete의 짝을 맞추자
- 위치지정 new가 뭐죠?
- operator new(std::size_t, + a 매개변수);
- size 뿐만 아니라 +a의 매개변수를 갖는게 위치지정 new이다.
- 왜 이름이 위치 지정인가?
- 보통 operator new(std::size_t, void* memory); 방식으로 많이 쓰였기 때문에
- void* memory로 선언된 곳에 전달된 인자로 메모리 할당을 시작하는 위치를 지정해줬
다
28. Item 52 : 위치지정 new와 delete의 짝을 맞추자
- 우리가 new를 사용하면 두 가지 함수가 호출된다
- 하나는 메모리 할당 함수이고 하나는 해당 타입의 생성자이다
- 메모리를 할당한 상태인데 생성자에서 오류를 던질 수 있다
- 그러면 런타임 때 메모리를 해제해 줘야한다
- 그런데 이 new가 위치지정 new 였다면?
- 런타임 시스템은 위치지정 new와 똑같은 추가 매개변수 개수와 타입을 갖는 위치지정
delete를 찾는다
29. Item 52 : 위치지정 new와 delete의 짝을 맞추자
- 그런데 같은 추가 매개변수를 갖는 위치지정 delete가 없다면?
- 여기서 추가라는 말을 쓰는 이유를 다음의 예에서 설명한다
- operator new(std::size_t, std::osstream& logStream)과
operator delete(void* pMemory, std::osstream& logStream)는
같은 추가 매개변수를 갖는다. 즉 std::size_t와 void* pMemory는
new, delete 각각에 기본인 것이다.
- 다시. 추가 매개변수가 짝이 안맞는다면? 아무 일도 안 한다. 즉 메모리는 유실된 상태다.
30. Item 52 : 위치지정 new와 delete의 짝을 맞추자
- 위치지정 delete는 언제 호출 되는가?
- 기본적으로는 호출되지 않는다
- 위치지정 new에서 오류가 던져졌을 때만 호출된다
- 즉 잘 처리된 객체를 지울 때는 표준 delete가 사용되고 위치지정 new를 사용하는 시점
에
오류가 발생하면 위치지정 delet를 사용하게 된다
31. Item 52 : 위치지정 new와 delete의 짝을 맞추자
- C++ 전역 유효범위에서 제공하는 operator new의 형태는 다음의 3가지가 표준이다.
- void* operator new(std::size_t) throw(std::bad_alloc); // 기본형
- void* operator new(std::size_t, void*) throw(); // 위치지정형
- void* operator new(std::size_t, const std::nothorw_t&) throw(); // 예외불가
- new를 작성할 때 얘네들을 가리지 않도록 하는 것이 목표!
32. Item 52 : 위치지정 new와 delete의 짝을 맞추자
- Class 하나에 기본형을 전부 넣어 둔다
- 해당 클래스를 상속 받는다
- using등을 사용해 표준 형태가 내부에서 보이도록 한다
- 그리고 원하는 new를 새롭게 정의한다.
- 그러면 new를 할 때 어떤 인자를 전달하느냐에 따라 표준, 혹은 사용자 지정 new를 사용
할 수 있게 된다.