C++ malloc vs new vs make_unique

Key takeaways

malloc hands you raw bytes, new builds an object in them, make_unique builds it and also owns it. Covers the full new-vs-malloc difference table, failure handling, over-aligned types, operator new overloading, C library interop and the mismatched-pair bugs.

Introduction

C++ gives you three common ways to get heap memory: malloc, new, and make_unique. They are not three spellings of the same thing; each one takes on one more responsibility than the previous.

  • malloc gives you a block of raw bytes. That’s it.
  • new gives you raw bytes and runs the constructor to turn them into an object, then hands you a correctly typed pointer.
  • make_unique does what new does and hands ownership to a std::unique_ptr, so the destructor and deallocation happen automatically.

A useful analogy: malloc rents you an empty plot of land, new builds a house on it (and you must remember to demolish it), and make_unique signs a contract that has the house demolished automatically when the lease ends. In modern C++ the default should be the last one, and the other two should appear only where you can name the reason.


new vs malloc: the Core Differences

Most of the practical difference is between malloc and new; make_unique inherits all of new’s properties and adds ownership.

Itemmallocnew
What it isC library functionC++ operator (an expression, backed by operator new)
Constructor / destructorNot calledCalled by new / delete
Size calculationManual: malloc(n * sizeof(T))Automatic from the type
Return typevoid*, needs a cast in C++T*, no cast
On failureReturns nullptrThrows std::bad_alloc (or nullptr with new (std::nothrow))
Deallocationfreedelete (delete[] for arrays)
ResizereallocNo equivalent (use std::vector)
CustomizableNo standard hookReplace global or per-class operator new/operator delete
Over-aligned typesOnly up to alignof(std::max_align_t); use aligned_allocHandled automatically since C++17

And the same view with make_unique added:

Itemmallocnewmake_unique (C++14)
Constructor callNoYesYes
Type safetyNo (cast needed)YesYes
On failureReturns nullptrThrows bad_allocThrows bad_alloc
DeallocationManual freeManual deleteAutomatic
Arraysmalloc(n * sizeof(T))new T[n]make_unique<T[]>(n)
Leak-free on exceptionsOnly with manual cleanupOnly with manual cleanupYes

Practical Implementation

malloc: C Style

#include <cstdlib>
#include <iostream>

int main() {
    int* p = static_cast<int*>(std::malloc(sizeof(int)));   // raw bytes, no initialization
    if (p == nullptr) {
        std::cerr << "Allocation failed\n";
        return 1;
    }
    *p = 42;
    std::cout << *p << '\n';
    std::free(p);
}

The cast is mandatory in C++ (C allows implicit void* conversion; C++ does not), and the nullptr check is mandatory in practice because malloc reports failure only through its return value.

new: C++ Style

#include <iostream>

class MyClass {
    int x_;
public:
    explicit MyClass(int x) : x_(x) { std::cout << "Constructor: " << x_ << '\n'; }
    ~MyClass() { std::cout << "Destructor: " << x_ << '\n'; }
    int getValue() const { return x_; }
};

int main() {
    MyClass* p = new MyClass(42);   // allocate + construct
    std::cout << p->getValue() << '\n';
    delete p;                       // destruct + deallocate
}

Output:

Constructor: 42
42
Destructor: 42

Why malloc Is Wrong for Class Types

#include <cstdlib>
#include <string>

struct User {
    std::string name;
    int id = 0;
};

int main() {
    // ❌ No constructor ran: `name` is not a valid std::string, just garbage bytes
    User* u = static_cast<User*>(std::malloc(sizeof(User)));
    // u->name = "alice";   // undefined behavior: assigning to a string that was never constructed
    std::free(u);           // and no destructor would run either

    // ✅
    User* v = new User{"alice", 1};
    delete v;
}

For a type with a non-trivial constructor, memory from malloc does not contain an object at all. Using u->name means calling member functions on something that was never constructed. This is the bug that usually appears when C code is ported to C++ incrementally and a struct gains a std::string member while the old malloc call stays in place. It compiles without a warning and fails somewhere far away, often inside the string’s allocator.

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

#include <iostream>
#include <memory>

class MyClass {
    int x_;
public:
    explicit MyClass(int x) : x_(x) { std::cout << "Constructor: " << x_ << '\n'; }
    ~MyClass() { std::cout << "Destructor: " << x_ << '\n'; }
    int getValue() const { return x_; }
};

int main() {
    {
        auto p = std::make_unique<MyClass>(42);
        std::cout << p->getValue() << '\n';
    }   // destructor runs here automatically
    std::cout << "After block end\n";
}

Output:

Constructor: 42
42
Destructor: 42
After block end

Array Allocation

#include <cstdlib>
#include <memory>

int main() {
    int* arr1 = static_cast<int*>(std::malloc(10 * sizeof(int)));
    for (int i = 0; i < 10; ++i) arr1[i] = i;
    std::free(arr1);

    int* arr2 = new int[10];
    for (int i = 0; i < 10; ++i) arr2[i] = i;
    delete[] arr2;                        // [] is required

    auto arr3 = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; ++i) arr3[i] = i;
}   // arr3 released automatically

For dynamic arrays, std::vector<int> is almost always the better choice than any of the three: it knows its size, can grow, and is just as fast to index. make_unique<T[]> is for the rare case where you want a fixed-size buffer with no capacity bookkeeping.

Failure Handling

#include <cstdlib>
#include <iostream>
#include <new>

int main() {
    // malloc: check the return value
    void* p1 = std::malloc(static_cast<std::size_t>(-1) / 2);
    if (!p1) std::cerr << "malloc failed\n";
    std::free(p1);   // free(nullptr) is a no-op

    // new: catch the exception
    try {
        std::size_t n = static_cast<std::size_t>(-1) / 16;   // far more than any machine has
        int* p2 = new int[n];
        delete[] p2;
    } catch (const std::bad_alloc& e) {
        std::cerr << "new failed: " << e.what() << '\n';
    }

    // nothrow new: malloc-style failure, but still runs constructors
    int* p3 = new (std::nothrow) int[1000];
    if (!p3) std::cerr << "nothrow new failed\n";
    delete[] p3;
}

One caveat about all of these: on Linux with memory overcommit enabled (the default), a large allocation often succeeds and the process is killed later by the OOM killer when the pages are actually touched. nullptr and bad_alloc reliably show up for absurd sizes or when address space runs out, but not as a general “out of RAM” signal.

Exception Safety

#include <iostream>
#include <memory>
#include <stdexcept>

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() {
    // ❌ raw new: every catch/return path must remember delete[]
    int* p1 = new int[10];
    try {
        process(p1, -1);
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
    delete[] p1;   // easy to forget, and skipped entirely if the exception isn't caught here

    // ✅ make_unique: released on any exit from the scope
    try {
        auto p2 = std::make_unique<int[]>(10);
        process(p2.get(), -1);
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
}

Advanced Usage

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. See custom deleters for more patterns.

#include <cstdio>
#include <memory>

struct FileCloser {
    void operator()(std::FILE* fp) const { if (fp) std::fclose(fp); }
};

int main() {
    std::unique_ptr<std::FILE, FileCloser> file(std::fopen("data.txt", "r"));
    if (file) {
        // use file.get()
    }   // FileCloser::operator() runs automatically
}

Placement new: Construction Without Allocation

When you need to construct an object in memory you already have (arenas, memory pools, shared-memory buffers), make_unique cannot help because it always allocates. Placement new separates construction from allocation:

#include <new>

alignas(MyClass) unsigned char buffer[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass(42);   // construct in existing storage
obj->~MyClass();                           // destroy manually; never delete

malloc has no equivalent, because it never constructs anything. The two can be combined, though: malloc the storage, placement-new the object into it, call the destructor explicitly, then free. That is exactly what a custom allocator does internally.

Overloading operator new / operator delete

A new expression does two things: it calls a function named operator new to get memory, then runs the constructor. You can replace that function globally or for one class:

#include <cstdlib>
#include <new>

void* operator new(std::size_t size) {
    if (size == 0) size = 1;              // new must return a unique pointer even for size 0
    if (void* p = std::malloc(size)) return p;
    throw std::bad_alloc();
}
void operator delete(void* p) noexcept { std::free(p); }
void operator delete(void* p, std::size_t) noexcept { std::free(p); }   // sized delete (C++14)

This is how allocation tracking, leak detectors and memory pools hook into every new in a program. Standard C has no comparable mechanism for malloc; tools that intercept it rely on linker tricks such as LD_PRELOAD. A complete replacement also needs the array forms (operator new[]/operator delete[]) and, for C++17, the std::align_val_t overloads; otherwise some allocations bypass your hook.

Over-Aligned Types (C++17)

struct alignas(64) CacheLineAligned { int data[16]; };

auto* p = new CacheLineAligned();                  // C++17: calls operator new(size, std::align_val_t{64})
delete p;
auto q = std::make_unique<CacheLineAligned>();     // same, via make_unique

malloc only guarantees alignment for fundamental types (alignof(std::max_align_t), usually 16). Before C++17, new had the same limitation, and over-aligned types silently got under-aligned memory. For raw buffers, std::aligned_alloc (C11/C++17) exists, but portable code must pass a size that is a multiple of the alignment, and MSVC does not provide it at all (use _aligned_malloc/_aligned_free there).

allocate_shared vs make_shared

make_shared puts the control block and the object into one allocation, always via operator new. std::allocate_shared keeps the single allocation but routes it through an allocator you choose:

#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

In mainstream implementations the default operator new is a thin wrapper around malloc, so the allocation cost is the same. What new adds is the constructor call you asked for, and what make_unique adds over new at runtime is nothing: unique_ptr with the default deleter is the size of a raw pointer, and its destructor compiles down to the same delete you would have written by hand.

The one real cost difference is initialization. make_unique<int[]>(n) value-initializes every element to zero, while new int[n] leaves them uninitialized. For a multi-megabyte buffer you are about to overwrite anyway, that zeroing is measurable. C++20 added std::make_unique_for_overwrite<T[]>(n), which default-initializes like new T[n] while still returning a unique_ptr.

If allocation shows up in a profile, the fix is almost never switching new to malloc; it’s allocating less often (reserve, reuse buffers, pool allocators, std::pmr).


Practical Cases

Interop with a C Library That Owns malloc’d Memory

When a C library returns a buffer it allocated with malloc and documents that you must free it, match that exactly, and wrap it so it is exception-safe:

#include <cstdlib>
#include <memory>

extern "C" char* c_library_alloc_string();   // returns a malloc'd buffer

struct FreeDeleter {
    void operator()(void* p) const noexcept { std::free(p); }
};

void useLibrary() {
    std::unique_ptr<char, FreeDeleter> str(c_library_alloc_string());
    // freed with free(), matching the library's contract
}

You’ll often see std::unique_ptr<char, decltype(&free)> p(ptr, free); instead. It works on current compilers, but it makes the unique_ptr twice as large (it stores the function pointer), and since C++20 taking the address of a standard library function is formally unspecified. A small deleter struct avoids both problems.

The same rule applies the other way around: if the C library expects to free() memory you pass in, allocate it with malloc, not new.

Factory Function Returning unique_ptr

#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);
}

The return type documents that the caller now owns the object, and there is no path on which it can leak.

A Low-Level Arena Backed by malloc

Inside an allocator, where you want raw bytes and will construct objects yourself with placement new, malloc is a legitimate choice as long as it stays contained behind a small RAII wrapper:

#include <cstdlib>
#include <new>

class Arena {
    char* buffer_;
public:
    explicit Arena(std::size_t bytes) : buffer_(static_cast<char*>(std::malloc(bytes))) {
        if (!buffer_) throw std::bad_alloc();
    }
    ~Arena() { std::free(buffer_); }
    Arena(const Arena&) = delete;
    Arena& operator=(const Arena&) = delete;
};

Deleting the copy operations matters: without them, copying an Arena would lead to a double free.


Troubleshooting

Mixing Allocation/Deallocation Pairs

Each pair must match exactly: malloc/calloc/realloc with free, new with delete, new[] with delete[].

int* p = new int(5);
std::free(p);    // ❌ undefined behavior

int* arr = new int[10];
delete arr;      // ❌ undefined behavior: must be delete[]

new and malloc are not required to use the same heap, and even when they do, new[] for types with a non-trivial destructor usually stores the element count in a hidden header before the array; delete without [] passes the wrong address to the allocator. “It works on my machine” is common here and means nothing: a custom operator new, a debug heap, or a sanitizer turns it into a crash. AddressSanitizer reports this directly as alloc-dealloc-mismatch.

Forgetting the nullptr Check on malloc

int* p = static_cast<int*>(std::malloc(huge_size));
*p = 42;   // ❌ null dereference if malloc failed
if (!p) { /* handle allocation failure */ }   // ✅ check first

Destructor Never Runs

Allocating a class type with malloc skips the constructor, and releasing it with free skips the destructor, so any members that own resources (std::string, std::vector, a smart pointer) leak. Never malloc a non-trivial C++ type. If you must use a C allocator, use placement new into the storage and call the destructor explicitly before free.

realloc on a new[] Buffer

realloc assumes the block came from malloc; calling it on memory from new[] is undefined behavior. Even on malloc’d memory it copies bytes with memcpy semantics, which is only correct for trivially copyable types; it never calls move constructors the way std::vector does when it grows. Use std::vector for growable arrays.

make_unique<T[]> Zeroes, new T[n] Does Not

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

Code that “worked” with make_unique can start reading garbage after someone “optimizes” it back to new int[n].

Leaking Through release()

std::unique_ptr<Widget> makeWidget();
Widget* w = makeWidget().release();   // ⚠️ now a raw pointer, nobody owns it

release() gives up ownership without destroying anything. Use it only to hand the pointer to an API that takes ownership; otherwise you want get().


Which allocation to use

  1. malloc: raw bytes, no constructor, nullptr on failure, free to release. Reserve it for C interop and allocator internals.
  2. new: allocation plus construction, typed, throws on failure, must be paired with delete/delete[], customizable via operator new.
  3. make_unique: everything new does, plus automatic cleanup on every path. The default for single-owner heap objects since C++14.

Decision Flowchart

Need a heap object?
├─ Growable array → std::vector
├─ Single owner   → std::make_unique
├─ Shared owner   → std::make_shared
├─ Memory you already have → placement new (+ explicit destructor call)
└─ C API requires malloc/free → malloc, wrapped in unique_ptr with a free deleter

Raw new/delete in application code is now mostly a sign that a smart pointer or container was skipped. It still has a place in the implementation of those abstractions.