C++ malloc vs new vs make_unique | 메모리 할당 완벽 비교
이 글의 핵심
C++ malloc vs new vs make_unique 차이점. malloc은 생성자 호출 안 함, new는 생성자 호출, make_unique는 생성자 호출 + 자동 해제. 예외 안전성과 RAII를 위해 make_unique를 권장합니다.
들어가며
C++에서 메모리 할당은 malloc, new, make_unique 세 가지 방법이 있습니다. 각각 생성자 호출, 타입 안전성, 자동 해제 등에서 차이가 있습니다.
비유로 말씀드리면, malloc/free는 토지만 임대, new/delete는 건물을 짓고 철거, make_unique는 관리 회사에 맡겨 계약 종료 시 자동 철거에 가깝습니다. 현대 C++에서는 가능하면 RAII가 되는 경로를 택하시는 것이 좋습니다.
이 글을 읽으면
- malloc vs new vs make_unique의 차이를 이해합니다
- 생성자 호출, 타입 안전성, 예외 처리의 차이를 파악합니다
- 성능 비교와 실무 선택 기준을 익힙니다
- RAII와 예외 안전성의 중요성을 확인합니다
목차
malloc vs new vs make_unique 차이
비교표
| 항목 | malloc | new | make_unique |
|---|---|---|---|
| 생성자 호출 | ❌ | ✅ | ✅ |
| 타입 안전성 | ❌ (캐스팅 필요) | ✅ | ✅ |
| 예외 처리 | nullptr 반환 | bad_alloc 던짐 | bad_alloc 던짐 |
| 해제 방법 | free | delete | 자동 |
| 배열 할당 | malloc(n * sizeof(T)) | new T[n] | make_unique<T[]>(n) |
| RAII | ❌ | ❌ | ✅ |
| 예외 안전 | ❌ | ❌ | ✅ |
| C++11 이후 | C 호환 | 레거시 | ✅ 권장 |
실전 구현
1) malloc: C 스타일
#include <cstdlib>
#include <iostream>
int main() {
// malloc: 생성자 호출 안 됨
int* p = (int*)malloc(sizeof(int));
if (p == nullptr) {
std::cerr << "할당 실패" << std::endl;
return 1;
}
*p = 42;
std::cout << *p << std::endl;
free(p);
return 0;
}
2) new: C++ 스타일
#include <iostream>
class MyClass {
private:
int x_;
public:
MyClass(int x) : x_(x) {
std::cout << "생성자: " << x_ << std::endl;
}
~MyClass() {
std::cout << "소멸자: " << x_ << std::endl;
}
int getValue() const { return x_; }
};
int main() {
// new: 생성자 호출
MyClass* p = new MyClass(42);
std::cout << p->getValue() << std::endl;
delete p; // 소멸자 호출
return 0;
}
출력:
생성자: 42
42
소멸자: 42
3) make_unique: 현대 C++ (C++14)
#include <iostream>
#include <memory>
class MyClass {
private:
int x_;
public:
MyClass(int x) : x_(x) {
std::cout << "생성자: " << x_ << std::endl;
}
~MyClass() {
std::cout << "소멸자: " << x_ << std::endl;
}
int getValue() const { return x_; }
};
int main() {
// make_unique: 생성자 호출 + 자동 해제
{
auto p = std::make_unique<MyClass>(42);
std::cout << p->getValue() << std::endl;
} // 자동으로 소멸자 호출
std::cout << "블록 종료 후" << std::endl;
return 0;
}
출력:
생성자: 42
42
소멸자: 42
블록 종료 후
4) 배열 할당
#include <iostream>
#include <memory>
int main() {
// malloc: 배열
int* arr1 = (int*)malloc(10 * sizeof(int));
for (int i = 0; i < 10; ++i) {
arr1[i] = i;
}
free(arr1);
// new: 배열
int* arr2 = new int[10];
for (int i = 0; i < 10; ++i) {
arr2[i] = i;
}
delete[] arr2; // delete[]
// make_unique: 배열
auto arr3 = std::make_unique<int[]>(10);
for (int i = 0; i < 10; ++i) {
arr3[i] = i;
}
// 자동 해제
return 0;
}
5) 예외 안전성
#include <iostream>
#include <memory>
void process(int* data, int size) {
if (size <= 0) {
throw std::invalid_argument("size must be positive");
}
for (int i = 0; i < size; ++i) {
data[i] = i;
}
}
int main() {
// ❌ new: 예외 발생 시 메모리 누수
int* p1 = new int[10];
try {
process(p1, -1); // 예외 발생
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
delete[] p1; // 수동 해제 필요
}
// ✅ make_unique: 예외 발생 시 자동 해제
try {
auto p2 = std::make_unique<int[]>(10);
process(p2.get(), -1); // 예외 발생
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
// 자동 해제
}
return 0;
}
고급 활용
1) 커스텀 삭제자
#include <iostream>
#include <memory>
void customDeleter(int* ptr) {
std::cout << "커스텀 삭제자 호출" << std::endl;
delete ptr;
}
int main() {
std::unique_ptr<int, decltype(&customDeleter)> p(new int(42), customDeleter);
std::cout << *p << std::endl;
return 0;
}
2) C 라이브러리 래핑
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
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) 팩토리 패턴
#include <iostream>
#include <memory>
#include <string>
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "원 그리기" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "사각형 그리기" << std::endl;
}
};
std::unique_ptr<Shape> createShape(const std::string& type) {
if (type == "circle") {
return std::make_unique<Circle>();
} else if (type == "rectangle") {
return std::make_unique<Rectangle>();
}
return nullptr;
}
int main() {
auto shape = createShape("circle");
if (shape) {
shape->draw();
}
return 0;
}
성능 비교
벤치마크
#include <chrono>
#include <iostream>
#include <memory>
void benchMalloc() {
for (int i = 0; i < 1000000; ++i) {
int* p = (int*)malloc(sizeof(int));
*p = i;
free(p);
}
}
void benchNew() {
for (int i = 0; i < 1000000; ++i) {
int* p = new int(i);
delete p;
}
}
void benchMakeUnique() {
for (int i = 0; i < 1000000; ++i) {
auto p = std::make_unique<int>(i);
}
}
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();
auto start3 = std::chrono::high_resolution_clock::now();
benchMakeUnique();
auto end3 = std::chrono::high_resolution_clock::now();
auto time3 = std::chrono::duration_cast<std::chrono::milliseconds>(end3 - start3).count();
std::cout << "malloc/free: " << time1 << "ms" << std::endl;
std::cout << "new/delete: " << time2 << "ms" << std::endl;
std::cout << "make_unique: " << time3 << "ms" << std::endl;
return 0;
}
결과 (GCC 13, -O3):
| 방법 | 시간 | 상대 속도 |
|---|---|---|
| malloc/free | 850ms | 1.0x |
| new/delete | 860ms | 1.01x |
| make_unique | 860ms | 1.01x |
결론: 성능 차이는 거의 없음
실무 사례
사례 1: 리소스 관리 - RAII
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
class FileReader {
private:
std::unique_ptr<std::ifstream> file_;
public:
FileReader(const std::string& filename) {
file_ = std::make_unique<std::ifstream>(filename);
if (!file_->is_open()) {
throw std::runtime_error("파일 열기 실패");
}
}
std::string readLine() {
std::string line;
std::getline(*file_, line);
return line;
}
};
int main() {
try {
FileReader reader("data.txt");
std::cout << reader.readLine() << std::endl;
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
// 자동으로 파일 닫힘
return 0;
}
사례 2: 게임 엔진 - 엔티티 관리
#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>
class Entity {
private:
int id_;
std::string name_;
public:
Entity(int id, const std::string& name) : id_(id), name_(name) {
std::cout << "Entity 생성: " << name_ << std::endl;
}
~Entity() {
std::cout << "Entity 소멸: " << name_ << std::endl;
}
int getId() const { return id_; }
const std::string& getName() const { return name_; }
};
class EntityManager {
private:
std::unordered_map<int, std::unique_ptr<Entity>> entities_;
int nextId_ = 0;
public:
int createEntity(const std::string& name) {
int id = nextId_++;
entities_[id] = std::make_unique<Entity>(id, name);
return id;
}
void destroyEntity(int id) {
entities_.erase(id); // 자동 소멸
}
Entity* getEntity(int id) {
auto it = entities_.find(id);
return it != entities_.end() ? it->second.get() : nullptr;
}
};
int main() {
EntityManager manager;
int id1 = manager.createEntity("Player");
int id2 = manager.createEntity("Enemy");
manager.destroyEntity(id1);
return 0;
}
출력:
Entity 생성: Player
Entity 생성: Enemy
Entity 소멸: Player
Entity 소멸: Enemy
사례 3: 네트워크 - 소켓 관리
#include <iostream>
#include <memory>
class Socket {
private:
int fd_;
public:
Socket(int fd) : fd_(fd) {
std::cout << "Socket 생성: " << fd_ << std::endl;
}
~Socket() {
std::cout << "Socket 닫기: " << fd_ << std::endl;
// close(fd_);
}
void send(const std::string& data) {
std::cout << "전송: " << data << std::endl;
}
};
std::unique_ptr<Socket> createSocket(int port) {
// int fd = socket(...);
int fd = 42; // 예시
return std::make_unique<Socket>(fd);
}
int main() {
try {
auto sock = createSocket(8080);
sock->send("Hello");
if (true) { // 조건부 종료
return 0; // 자동으로 소켓 닫힘
}
sock->send("World");
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
사례 4: 멀티스레드 - 스레드 안전 캐시
#include <iostream>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <string>
template<typename K, typename V>
class ThreadSafeCache {
private:
std::unordered_map<K, std::unique_ptr<V>> cache_;
mutable std::mutex mutex_;
public:
void put(const K& key, std::unique_ptr<V> value) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[key] = std::move(value);
}
V* get(const K& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_.find(key);
return it != cache_.end() ? it->second.get() : nullptr;
}
void remove(const K& key) {
std::lock_guard<std::mutex> lock(mutex_);
cache_.erase(key); // 자동 소멸
}
};
int main() {
ThreadSafeCache<std::string, int> cache;
cache.put("key1", std::make_unique<int>(42));
if (int* val = cache.get("key1")) {
std::cout << *val << std::endl;
}
cache.remove("key1");
return 0;
}
트러블슈팅
문제 1: malloc-delete 혼용
증상: 크래시 또는 메모리 누수
// ❌ 미정의 동작
int* p1 = (int*)malloc(sizeof(int));
delete p1; // ❌ malloc-delete 혼용
// ❌ 미정의 동작
int* p2 = new int(42);
free(p2); // ❌ new-free 혼용
// ✅ 올바른 짝
int* p3 = (int*)malloc(sizeof(int));
free(p3);
int* p4 = new int(42);
delete p4;
auto p5 = std::make_unique<int>(42);
// 자동 해제
문제 2: new 후 예외 발생
증상: 메모리 누수
// ❌ 예외 발생 시 메모리 누수
void badFunction() {
int* p = new int(42);
// 예외 발생 가능
if (true) {
throw std::runtime_error("오류");
}
delete p; // 실행 안 됨
}
// ✅ make_unique로 자동 해제
void goodFunction() {
auto p = std::make_unique<int>(42);
// 예외 발생 가능
if (true) {
throw std::runtime_error("오류");
}
// 예외 발생해도 자동 해제
}
문제 3: 배열 delete 누락
증상: 메모리 누수
// ❌ delete 사용 (배열은 delete[])
int* arr = new int[10];
delete arr; // ❌ 메모리 누수 가능
// ✅ delete[] 사용
int* arr2 = new int[10];
delete[] arr2;
// ✅ make_unique 사용
auto arr3 = std::make_unique<int[]>(10);
// 자동 해제
문제 4: 함수 인자로 전달
증상: 소유권 혼란
// ❌ raw 포인터로 전달 (소유권 불명확)
void process(int* ptr) {
// delete ptr? // 누가 해제?
}
int* p = new int(42);
process(p);
delete p; // 여기서 해제?
// ✅ unique_ptr로 전달 (소유권 이전)
void process2(std::unique_ptr<int> ptr) {
// 자동 해제
}
auto p2 = std::make_unique<int>(42);
process2(std::move(p2)); // 소유권 이전
// p2는 nullptr
// ✅ raw 포인터로 전달 (소유권 유지)
void process3(int* ptr) {
// 읽기만
}
auto p3 = std::make_unique<int>(42);
process3(p3.get()); // 소유권 유지
마무리
현대 C++에서는 make_unique를 사용하세요.
핵심 요약
-
malloc vs new vs make_unique
- malloc: 생성자 호출 안 함, free 필요
- new: 생성자 호출, delete 필요
- make_unique: 생성자 호출 + 자동 해제
-
선택 기준
- 일반적인 경우: make_unique
- 공유 소유권: make_shared
- C 라이브러리 연동: malloc
- 레거시 코드: new
-
예외 안전성
- malloc/new: 예외 발생 시 메모리 누수
- make_unique: 자동 해제로 안전
-
성능
- 거의 차이 없음
- 안전성이 더 중요
선택 가이드
| 상황 | 권장 | 이유 |
|---|---|---|
| C++ 객체 | make_unique | 자동 해제 |
| 공유 소유권 | make_shared | 참조 카운팅 |
| C 라이브러리 | malloc | C 호환 |
| 레거시 코드 | new | 기존 코드 유지 |
| 일반적인 경우 | make_unique | 예외 안전 |
코드 예제 치트시트
// malloc
int* p1 = (int*)malloc(sizeof(int));
*p1 = 42;
free(p1);
// new
int* p2 = new int(42);
delete p2;
// make_unique
auto p3 = std::make_unique<int>(42);
// 자동 해제
// 배열
auto arr1 = std::make_unique<int[]>(10);
// 커스텀 삭제자
std::unique_ptr<int, decltype(&free)> p4((int*)malloc(sizeof(int)), free);
다음 단계
- 스마트 포인터: C++ shared_ptr vs unique_ptr
- 메모리 관리: C++ 메모리 관리
- RAII 패턴: C++ RAII 패턴
참고 자료
- “Effective Modern C++” - Scott Meyers
- “C++ Primer” - Stanley Lippman
- cppreference: https://en.cppreference.com/w/cpp/memory
한 줄 정리: 현대 C++에서는 make_unique로 자동 해제와 예외 안전성을 보장하고, C 라이브러리 연동 시에만 malloc을 사용한다.