C++ new vs malloc | 생성자·타입 안전성·예외 처리 완벽 비교
이 글의 핵심
C++ new vs malloc 차이점. 생성자·소멸자, 타입 안전성, 실패 시 예외 vs nullptr. 성능은 거의 비슷하지만 C++ 객체에는 new·delete를 쓰는 것이 맞는 이유와 실전 선택을 정리합니다.
들어가며
C++는 new와 malloc 두 가지 메모리 할당 방법을 제공합니다. malloc은 C에서 물려받은 함수이고, new는 C++ 전용 연산자입니다.
비유로 말씀드리면, malloc은 빈 방만 열어 주는 것이고, new는 가구를 조립해 놓은 방을 넘겨주는 것에 가깝습니다. C++ 객체에는 생성자·소멸자가 있으므로 “방만”이 아니라 초기화까지 맞추려면 new/delete 쪽이 맞습니다.
이 글을 읽으면
- new와 malloc의 7가지 차이점을 이해합니다
- 생성자 호출, 타입 안전성, 예외 처리의 차이를 파악합니다
- 성능 비교와 실무 선택 기준을 익힙니다
- 혼용 시 주의사항과 C 라이브러리 연동 패턴을 확인합니다
목차
new vs malloc 7가지 차이
비교표
| 항목 | new | malloc |
|---|---|---|
| 언어 | C++ 연산자 | C 함수 |
| 생성자 호출 | ✅ 호출함 | ❌ 호출 안 함 |
| 타입 안전성 | ✅ 안전 (캐스팅 불필요) | ❌ 불안전 (캐스팅 필요) |
| 크기 계산 | 자동 | 수동 (sizeof) |
| 실패 시 | 예외 (bad_alloc) | nullptr 반환 |
| 해제 | delete | free |
| 배열 할당 | new[] | malloc + 크기 |
| 오버로드 | 가능 (operator new) | 불가능 |
실전 구현
1) 기본 타입 할당
malloc
#include <cstdlib>
#include <iostream>
int main() {
// malloc: 캐스팅 필요
int* ptr = (int*)malloc(sizeof(int));
if (ptr == nullptr) { // nullptr 체크 필요
std::cerr << "할당 실패" << std::endl;
return 1;
}
*ptr = 42;
std::cout << *ptr << std::endl;
free(ptr);
return 0;
}
new
#include <iostream>
int main() {
// new: 캐스팅 불필요, 초기화 동시에
int* ptr = new int(42);
std::cout << *ptr << std::endl;
delete ptr;
return 0;
}
2) 클래스 할당
#include <iostream>
class MyClass {
private:
int x_;
public:
MyClass() : x_(0) {
std::cout << "생성자 호출" << std::endl;
}
~MyClass() {
std::cout << "소멸자 호출" << std::endl;
}
void setValue(int x) { x_ = x; }
int getValue() const { return x_; }
};
int main() {
// ❌ malloc: 생성자 호출 안 됨
MyClass* obj1 = (MyClass*)malloc(sizeof(MyClass));
// "생성자 호출" 출력 안 됨
// obj1->x_는 쓰레기 값
free(obj1); // 소멸자 호출 안 됨
// ✅ new: 생성자 호출
MyClass* obj2 = new MyClass();
// "생성자 호출" 출력
obj2->setValue(42);
std::cout << obj2->getValue() << std::endl;
delete obj2; // "소멸자 호출" 출력
return 0;
}
중요: 클래스 객체는 반드시 new를 사용해야 합니다.
3) 배열 할당
malloc
// malloc: 크기 수동 계산
int* arr1 = (int*)malloc(10 * sizeof(int));
for (int i = 0; i < 10; ++i) {
arr1[i] = i;
}
free(arr1);
new
// new: 크기 자동 계산
int* arr2 = new int[10];
for (int i = 0; i < 10; ++i) {
arr2[i] = i;
}
delete[] arr2; // 배열은 delete[]
4) 예외 처리
malloc: nullptr 반환
#include <cstdlib>
#include <iostream>
int main() {
int* ptr = (int*)malloc(1000000000000); // 1TB 할당 (실패)
if (ptr == nullptr) { // 수동 체크 필요
std::cerr << "할당 실패" << std::endl;
return 1;
}
free(ptr);
return 0;
}
new: 예외 던짐
#include <iostream>
int main() {
try {
int* ptr = new int[1000000000000]; // 1TB 할당 (실패)
delete[] ptr;
} catch (const std::bad_alloc& e) {
std::cerr << "할당 실패: " << e.what() << std::endl;
}
return 0;
}
new (std::nothrow)
#include <new>
#include <iostream>
int main() {
int* ptr = new (std::nothrow) int[1000000000000];
if (ptr == nullptr) {
std::cerr << "할당 실패" << std::endl;
return 1;
}
delete[] ptr;
return 0;
}
고급 활용
1) placement new
#include <new>
#include <iostream>
int main() {
// 메모리 할당
char buffer[sizeof(MyClass)];
// placement new: 기존 메모리에 객체 생성
MyClass* obj = new (buffer) MyClass();
obj->setValue(42);
std::cout << obj->getValue() << std::endl;
// 소멸자 수동 호출
obj->~MyClass();
return 0;
}
2) 커스텀 operator new
#include <iostream>
#include <cstdlib>
void* operator new(size_t size) {
std::cout << "커스텀 new: " << size << " bytes" << std::endl;
void* ptr = malloc(size);
if (!ptr) throw std::bad_alloc();
return ptr;
}
void operator delete(void* ptr) noexcept {
std::cout << "커스텀 delete" << std::endl;
free(ptr);
}
int main() {
int* ptr = new int(42);
delete ptr;
return 0;
}
3) C 라이브러리 래핑
#include <memory>
#include <cstdlib>
extern "C" {
char* c_function() {
return (char*)malloc(100);
}
}
std::unique_ptr<char, decltype(&free)> wrap_c_function() {
char* ptr = c_function();
return {ptr, free}; // 커스텀 삭제자
}
int main() {
auto ptr = wrap_c_function();
// 자동으로 free 호출
return 0;
}
성능 비교
기본 타입 할당
테스트: 100만 번 할당/해제
#include <chrono>
#include <iostream>
void benchMalloc() {
for (int i = 0; i < 1000000; ++i) {
int* ptr = (int*)malloc(sizeof(int));
*ptr = i;
free(ptr);
}
}
void benchNew() {
for (int i = 0; i < 1000000; ++i) {
int* ptr = new int(i);
delete ptr;
}
}
int main() {
auto start1 = std::chrono::high_resolution_clock::now();
benchMalloc();
auto end1 = std::chrono::high_resolution_clock::now();
auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
auto start2 = std::chrono::high_resolution_clock::now();
benchNew();
auto end2 = std::chrono::high_resolution_clock::now();
auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
std::cout << "malloc/free: " << time1 << "ms" << std::endl;
std::cout << "new/delete: " << time2 << "ms" << std::endl;
return 0;
}
결과:
| 방법 | 시간 | 상대 속도 |
|---|---|---|
| malloc/free | 850ms | 1.0x |
| new/delete | 860ms | 1.01x |
결론: 성능 차이는 거의 없음
실무 사례
사례 1: 게임 엔진 - 객체 풀
#include <vector>
#include <iostream>
class GameObject {
private:
int id_;
bool active_;
public:
GameObject() : id_(0), active_(false) {
std::cout << "GameObject 생성" << std::endl;
}
~GameObject() {
std::cout << "GameObject 소멸" << std::endl;
}
void activate(int id) {
id_ = id;
active_ = true;
}
void deactivate() {
active_ = false;
}
};
class ObjectPool {
private:
std::vector<GameObject*> pool_;
public:
ObjectPool(size_t size) {
for (size_t i = 0; i < size; ++i) {
pool_.push_back(new GameObject()); // new 사용
}
}
~ObjectPool() {
for (auto* obj : pool_) {
delete obj; // 소멸자 호출
}
}
GameObject* acquire() {
if (pool_.empty()) return nullptr;
auto* obj = pool_.back();
pool_.pop_back();
return obj;
}
void release(GameObject* obj) {
obj->deactivate();
pool_.push_back(obj);
}
};
int main() {
ObjectPool pool(10);
auto* obj = pool.acquire();
obj->activate(1);
pool.release(obj);
return 0;
}
사례 2: C 라이브러리 연동
#include <memory>
#include <cstdlib>
#include <cstring>
extern "C" {
struct CData {
int id;
char name[100];
};
CData* create_data(int id, const char* name) {
CData* data = (CData*)malloc(sizeof(CData));
data->id = id;
strncpy(data->name, name, 99);
data->name[99] = '\0';
return data;
}
void destroy_data(CData* data) {
free(data);
}
}
std::unique_ptr<CData, decltype(&destroy_data)> wrap_data(int id, const char* name) {
CData* data = create_data(id, name);
return {data, destroy_data};
}
int main() {
auto data = wrap_data(1, "test");
std::cout << data->id << ", " << data->name << std::endl;
// 자동으로 destroy_data 호출
return 0;
}
사례 3: 메모리 풀 - placement new
#include <new>
#include <vector>
#include <iostream>
class MemoryPool {
private:
char* buffer_;
size_t size_;
size_t offset_;
public:
MemoryPool(size_t size) : size_(size), offset_(0) {
buffer_ = (char*)malloc(size); // malloc으로 버퍼 할당
}
~MemoryPool() {
free(buffer_);
}
template<typename T, typename... Args>
T* allocate(Args&&... args) {
if (offset_ + sizeof(T) > size_) {
throw std::bad_alloc();
}
void* ptr = buffer_ + offset_;
offset_ += sizeof(T);
return new (ptr) T(std::forward<Args>(args)...); // placement new
}
};
int main() {
MemoryPool pool(1024);
int* p1 = pool.allocate<int>(42);
int* p2 = pool.allocate<int>(100);
std::cout << *p1 << ", " << *p2 << std::endl;
return 0;
}
트러블슈팅
문제 1: malloc-delete 혼용
증상: 크래시 또는 메모리 누수
// ❌ 미정의 동작
int* ptr1 = (int*)malloc(sizeof(int));
delete ptr1; // ❌ malloc-delete 혼용
// ❌ 미정의 동작
int* ptr2 = new int(42);
free(ptr2); // ❌ new-free 혼용
// ✅ 올바른 짝
int* ptr3 = (int*)malloc(sizeof(int));
free(ptr3);
int* ptr4 = new int(42);
delete ptr4;
문제 2: 생성자 호출 누락
증상: 멤버 변수가 쓰레기 값
class MyClass {
private:
int x_;
public:
MyClass() : x_(0) {}
int getValue() const { return x_; }
};
// ❌ malloc: 생성자 호출 안 됨
MyClass* obj = (MyClass*)malloc(sizeof(MyClass));
std::cout << obj->getValue() << std::endl; // 쓰레기 값
free(obj);
// ✅ new: 생성자 호출
MyClass* obj2 = new MyClass();
std::cout << obj2->getValue() << std::endl; // 0
delete obj2;
문제 3: 배열 delete 누락
증상: 메모리 누수
// ❌ delete 사용 (배열은 delete[])
int* arr = new int[10];
delete arr; // ❌ 메모리 누수 가능
// ✅ delete[] 사용
int* arr2 = new int[10];
delete[] arr2;
문제 4: 타입 크기 실수
증상: 버퍼 오버플로우
// ❌ 크기 실수
int* arr = (int*)malloc(10); // 10바이트 (int 2.5개?)
arr[5] = 42; // 버퍼 오버플로우
// ✅ 올바른 크기
int* arr2 = (int*)malloc(10 * sizeof(int)); // int 10개
arr2[5] = 42;
free(arr2);
// 또는 new
int* arr3 = new int[10]; // 크기 자동
arr3[5] = 42;
delete[] arr3;
마무리
C++에서는 new를 사용하세요. malloc은 생성자를 호출하지 않아 객체가 제대로 초기화되지 않습니다.
핵심 요약
-
new vs malloc
- new: 생성자 호출, 타입 안전, 예외
- malloc: 메모리만 할당, 캐스팅 필요, nullptr
-
선택 기준
- C++ 클래스: new (생성자 필요)
- C 라이브러리 연동: malloc
- 일반적인 경우: 스마트 포인터
-
혼용 금지
- malloc-free, new-delete 짝 맞추기
- 혼용 시 미정의 동작
-
성능
- 거의 차이 없음
- new는 malloc + 생성자
선택 가이드
| 상황 | 권장 | 이유 |
|---|---|---|
| C++ 클래스 | new | 생성자 호출 필요 |
| 기본 타입 | new | 타입 안전성 |
| C 라이브러리 연동 | malloc | C 코드와 호환 |
| placement new | malloc + new | 메모리 풀 |
| 일반적인 경우 | 스마트 포인터 | 자동 해제 |
코드 예제 치트시트
// malloc
int* ptr1 = (int*)malloc(sizeof(int));
free(ptr1);
// new
int* ptr2 = new int(42);
delete ptr2;
// 배열
int* arr1 = (int*)malloc(10 * sizeof(int));
free(arr1);
int* arr2 = new int[10];
delete[] arr2;
// 스마트 포인터
auto ptr3 = std::make_unique<int>(42);
auto arr3 = std::make_unique<int[]>(10);
// C 라이브러리 래핑
std::unique_ptr<char, decltype(&free)> ptr4(c_function(), free);
다음 단계
- 메모리 기초: C++ 메모리 기초
- 스마트 포인터: C++ 스마트 포인터
- RAII 패턴: C++ RAII 패턴
참고 자료
- “Effective C++” - Scott Meyers
- “C++ Primer” - Stanley Lippman
- cppreference: https://en.cppreference.com/w/cpp/memory
한 줄 정리: C++에서는 생성자 호출과 타입 안전성을 위해 new를 사용하고, 가능하면 스마트 포인터로 자동 해제를 보장한다.