C++에서 객체를 동적으로 생성할 때 흔히 new와 delete 연산자를 사용합니다. 표면적으로는 malloc/free와 비슷해 보이지만, 실제 내부 동작은 훨씬 더 복잡하고 강력합니다.
이번 포스트에서는 new와 delete가 내부적으로 어떻게 동작하는지 왜, 어떻게 오버로딩할 수 있는지 정리하려고 합니다.
new는 단순히 메모리만 확보하는 것이 아닙니다. 두 단계로 이루어집니다.
1. 메모리 확보 (allocation)
2. 객체 생성 (construction)
즉, 다음 코드:
MyClass* obj = new MyClass(10);
은 내부적으로 대략 이렇게 작동합니다:
void* raw = ::operator new(sizeof(MyClass)); // 메모리 확보
try {
obj = static_cast<MyClass*>(raw);
new (obj) MyClass(10); // placement new로 생성자 호출
} catch (...) {
::operator delete(raw); // 생성자에서 예외 발생 시 메모리 해제
throw;
}
delete 역시 두 단계로 이루어집니다.
1. 객체 소멸 (destruction)
2. 메모리 해제 (deallocation)
예시:
delete obj;
은 내부적으로 이렇게 동작합니다:
if (obj != nullptr) {
obj->~MyClass(); // 소멸자 호출
::operator delete(obj); // 메모리 해제
}
C++에서는 전역 단위 또는 클래스 단위에서 new와 delete를 오버로딩할 수 있습니다.
이때 오버로딩되는 것은 메모리 확보/해제 부분이지, 생성자와 소멸자 호출 과정은 그대로 유지됩니다.
void* operator new(size_t size) {
cout << "[Global new] size: " << size << endl;
return malloc(size);
}
void operator delete(void* ptr) noexcept {
cout << "[Global delete]" << endl;
free(ptr);
}
class MyClass {
public:
void* operator new(size_t size) {
cout << "[MyClass new] size: " << size << endl;
return ::operator new(size); // 전역 new 호출
}
void operator delete(void* ptr) {
cout << "[MyClass delete]" << endl;
::operator delete(ptr); // 전역 delete 호출
}
};
int main() {
MyClass* obj = new MyClass(); // MyClass::operator new 호출
delete obj; // MyClass::operator delete 호출
}
메모리 풀(Pool) 관리: 빈번한 객체 생성/삭제 성능 최적화
로깅/디버깅: 누수 추적, 메모리 사용량 측정
특수 메모리 영역 관리: 공유 메모리, GPU 메모리 등
operator new는 생성자 호출을 포함하지 않는다.operator delete는 소멸자 호출을 포함하지 않는다.::operator new / ::operator delete를 적절히 호출해줘야 한다.new = 메모리 할당 + 생성자 호출delete = 소멸자 호출 + 메모리 해제