C++ malloc vs new vs make_unique | 메모리 할당 완벽 비교

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와 예외 안전성의 중요성을 확인합니다

목차

  1. malloc vs new vs make_unique 차이
  2. 실전 구현
  3. 고급 활용
  4. 성능 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

malloc vs new vs make_unique 차이

비교표

항목mallocnewmake_unique
생성자 호출
타입 안전성❌ (캐스팅 필요)
예외 처리nullptr 반환bad_alloc 던짐bad_alloc 던짐
해제 방법freedelete자동
배열 할당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/free850ms1.0x
new/delete860ms1.01x
make_unique860ms1.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를 사용하세요.

핵심 요약

  1. malloc vs new vs make_unique

    • malloc: 생성자 호출 안 함, free 필요
    • new: 생성자 호출, delete 필요
    • make_unique: 생성자 호출 + 자동 해제
  2. 선택 기준

    • 일반적인 경우: make_unique
    • 공유 소유권: make_shared
    • C 라이브러리 연동: malloc
    • 레거시 코드: new
  3. 예외 안전성

    • malloc/new: 예외 발생 시 메모리 누수
    • make_unique: 자동 해제로 안전
  4. 성능

    • 거의 차이 없음
    • 안전성이 더 중요

선택 가이드

상황권장이유
C++ 객체make_unique자동 해제
공유 소유권make_shared참조 카운팅
C 라이브러리mallocC 호환
레거시 코드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 패턴

참고 자료

한 줄 정리: 현대 C++에서는 make_unique로 자동 해제와 예외 안전성을 보장하고, C 라이브러리 연동 시에만 malloc을 사용한다.