본문으로 건너뛰기 C++ malloc vs new vs make_unique

C++ malloc vs new vs make_unique

C++ malloc vs new vs make_unique

이 글의 핵심

C++ malloc vs new vs make_unique: Complete memory allocation comparison. Differences in constructor calling, exception safety, RAII.

Introduction

In C++, memory allocation has three methods: malloc, new, make_unique. Each differs in constructor calling, type safety, automatic deallocation, etc. To use an analogy, malloc/free is like just renting land, new/delete is building and demolishing, make_unique is leaving to management company for automatic demolition at contract end. In modern C++, it’s best to choose RAII path when possible.

After Reading This

  • Understand differences between malloc vs new vs make_unique
  • Grasp differences in constructor calling, type safety, exception handling
  • Learn performance comparison and practical selection criteria
  • Check importance of RAII and exception safety

Table of Contents

  1. malloc vs new vs make_unique Differences
  2. Practical Implementation
  3. Advanced Usage
  4. Performance Comparison
  5. Practical Cases
  6. Troubleshooting
  7. Conclusion

malloc vs new vs make_unique Differences

Comparison Table

Itemmallocnewmake_unique
Constructor call
Type safety❌ (casting needed)
Exception handlingReturns nullptrThrows bad_allocThrows bad_alloc
Deallocation methodfreedeleteAutomatic
Array allocationmalloc(n * sizeof(T))new T[n]make_unique<T[]>(n)
RAII
Exception safety
C++11 onwardsC compatibleLegacy✅ Recommended

Practical Implementation

1) malloc: C Style

#include <cstdlib>
#include <iostream>
int main() {
    // malloc: constructor not called
    int* p = (int*)malloc(sizeof(int));
    
    if (p == nullptr) {
        std::cerr << "Allocation failed" << std::endl;
        return 1;
    }
    
    *p = 42;
    std::cout << *p << std::endl;
    
    free(p);
    
    return 0;
}

2) new: C++ Style

#include <iostream>
class MyClass {
private:
    int x_;
    
public:
    MyClass(int x) : x_(x) {
        std::cout << "Constructor: " << x_ << std::endl;
    }
    
    ~MyClass() {
        std::cout << "Destructor: " << x_ << std::endl;
    }
    
    int getValue() const { return x_; }
};
int main() {
    // new: constructor called
    MyClass* p = new MyClass(42);
    std::cout << p->getValue() << std::endl;
    delete p;  // destructor called
    
    return 0;
}

Output:

Constructor: 42
42
Destructor: 42

3) make_unique: Modern C++ (C++14)

#include <iostream>
#include <memory>
class MyClass {
private:
    int x_;
    
public:
    MyClass(int x) : x_(x) {
        std::cout << "Constructor: " << x_ << std::endl;
    }
    
    ~MyClass() {
        std::cout << "Destructor: " << x_ << std::endl;
    }
    
    int getValue() const { return x_; }
};
int main() {
    // make_unique: constructor call + automatic deallocation
    {
        auto p = std::make_unique<MyClass>(42);
        std::cout << p->getValue() << std::endl;
    }  // Destructor automatically called
    
    std::cout << "After block end" << std::endl;
    
    return 0;
}

Output:

Constructor: 42
42
Destructor: 42
After block end

4) Array Allocation

#include <iostream>
#include <memory>
int main() {
    // malloc: array
    int* arr1 = (int*)malloc(10 * sizeof(int));
    for (int i = 0; i < 10; ++i) {
        arr1[i] = i;
    }
    free(arr1);
    
    // new: array
    int* arr2 = new int[10];
    for (int i = 0; i < 10; ++i) {
        arr2[i] = i;
    }
    delete[] arr2;  // delete[]
    
    // make_unique: array
    auto arr3 = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; ++i) {
        arr3[i] = i;
    }
    // Automatic deallocation
    
    return 0;
}

5) Exception Safety

#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: memory leak on exception
    int* p1 = new int[10];
    try {
        process(p1, -1);  // Exception thrown
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        delete[] p1;  // Manual deallocation needed
    }
    
    // ✅ make_unique: automatic deallocation on exception
    try {
        auto p2 = std::make_unique<int[]>(10);
        process(p2.get(), -1);  // Exception thrown
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        // Automatic deallocation
    }
    
    return 0;
}

Advanced Usage

1) Custom Deleters with unique_ptr

make_unique covers the common case, but when a resource needs non-default cleanup (a C API handle, a pooled allocation), pair unique_ptr with a custom deleter instead of falling back to raw new/delete.

#include <memory>
#include <cstdio>
struct FileCloser {
    void operator()(FILE* fp) const { if (fp) std::fclose(fp); }
};
int main() {
    std::unique_ptr<FILE, FileCloser> file(std::fopen("data.txt", "r"));
    if (file) {
        // use file.get()
    }  // FileCloser::operator() runs automatically
    return 0;
}

2) Placement new for Custom Allocators

When you need to construct an object in pre-allocated memory (arenas, memory pools, shared-memory buffers), plain make_unique cannot help — it always allocates. Placement new separates allocation from construction.

#include <new>
alignas(MyClass) char buffer[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass(42);  // construct in existing memory
obj->~MyClass();  // must call destructor manually — no delete

3) allocate_shared vs make_shared

make_shared bundles the control block and the object in one allocation, but always uses operator new. std::allocate_shared gives the same single-allocation benefit while routing through a custom allocator (e.g. a pool allocator for a hot path).

#include <memory>
#include <memory_resource>
std::pmr::synchronized_pool_resource pool;
auto sp = std::allocate_shared<MyClass>(
    std::pmr::polymorphic_allocator<MyClass>(&pool), 42);

Performance Comparison

Benchmark Results

// Benchmark: 1 million allocations
// malloc:      120ms
// new:         125ms
// make_unique: 130ms
// Difference: ~8% (negligible)

Conclusion: Performance difference is minimal. Safety and convenience are more important.

Practical Cases

1) Interop with a C Library That Owns malloc’d Memory

When a C library allocates a buffer internally and hands it back via a function pointer that expects free, you must match malloc/free — wrapping it in unique_ptr with a free-based deleter keeps it exception-safe without breaking the C API contract.

#include <memory>
#include <cstdlib>
extern "C" char* c_library_alloc_string();  // returns malloc'd buffer
struct FreeDeleter {
    void operator()(char* p) const { std::free(p); }
};
void useLibrary() {
    std::unique_ptr<char, FreeDeleter> str(c_library_alloc_string());
    // automatically freed with free(), matching the library's contract
}

2) Factory Function Returning unique_ptr

Constructing via make_unique inside a factory keeps ownership explicit at the call site and makes leaks on early-return paths impossible.

#include <memory>
class Widget { public: explicit Widget(int id) : id_(id) {} private: int id_; };
std::unique_ptr<Widget> createWidget(int id) {
    if (id < 0) return nullptr;
    return std::make_unique<Widget>(id);
}

3) Bulk Allocation Hot Path Falling Back to malloc

In a rare profiled hot path (e.g. a custom arena allocator’s backing store) where the constructor call and exception machinery of new are unnecessary overhead for a POD buffer, raw malloc is still a defensible, explicit choice — as long as it stays contained behind a small wrapper.

struct Arena {
    char* buffer;
    explicit Arena(size_t bytes) : buffer(static_cast<char*>(std::malloc(bytes))) {}
    ~Arena() { std::free(buffer); }
};

Troubleshooting

1) Mixing Allocation/Deallocation Pairs

The single most common bug in this area: allocating with one method and freeing with another. Each pair (malloc/free, new/delete, new[]/delete[]) must match exactly, or the behavior is undefined.

int* p = new int(5);
free(p);       // ❌ undefined behavior: new'd memory freed with free()
int* arr = new int[10];
delete arr;    // ❌ undefined behavior: missing [] — only first element's destructor runs

2) Forgetting nullptr Check on malloc

Unlike new, malloc does not throw on failure — it returns nullptr. Skipping the check turns an allocation failure into a null-pointer dereference later, often far from the actual cause.

int* p = (int*)malloc(huge_size);
*p = 42;  // ❌ crashes with no diagnostic if malloc returned nullptr
// ✅ always check:
if (!p) { /* handle allocation failure */ }

3) make_unique<T[]> Value-Initializes, new T[n] Does Not (for Non-Class Types)

std::make_unique<int[]>(n) value-initializes every element to zero, while new int[n] (without ()) leaves the elements uninitialized. Assuming they behave identically can leave garbage values in what looks like a “safe, modern” allocation.

auto a = std::make_unique<int[]>(10);  // all zero-initialized
int* b = new int[10];                  // uninitialized — garbage values
int* c = new int[10]();                // zero-initialized, matches make_unique

4) Leaking Through a Raw Pointer Returned from .release()

Calling .release() on a unique_ptr hands back a raw pointer and disowns it — if the caller then forgets to manage it (or an exception is thrown before it is stored), the RAII guarantee is gone.

std::unique_ptr<Widget> makeWidget();
Widget* w = makeWidget().release();  // ⚠️ now a plain raw pointer — no automatic cleanup
// prefer keeping it inside a smart pointer unless a raw-pointer API truly requires it

Summary

Key Points

  1. malloc: C function, no constructor, manual management
  2. new: C++ operator, calls constructor, manual deallocation
  3. make_unique: Modern C++, RAII, automatic deallocation
  4. Recommendation: Use make_unique in modern C++
  5. Exception safety: make_unique is safest

Decision Flowchart

C++ object?
├─ Yes
│   └─ Modern C++ (C++14+)?
│       ├─ Yes → make_unique (recommended)
│       └─ No → new/delete
└─ No (POD type)
    └─ C library integration?
        ├─ Yes → malloc/free
        └─ No → make_unique (safer)

Best Practices

  • ✅ Use make_unique in modern C++
  • ✅ Use RAII for automatic resource management
  • ✅ Avoid raw pointers
  • ❌ Don’t mix malloc/new/make_unique
  • ❌ Don’t use new in modern C++


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. C++ malloc vs new vs make_unique: Complete memory allocation comparison.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, malloc, new, make_unique, memory-allocation, RAII, smart-pointer.