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
newdoes and hands ownership to astd::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.
| Item | malloc | new |
|---|---|---|
| What it is | C library function | C++ operator (an expression, backed by operator new) |
| Constructor / destructor | Not called | Called by new / delete |
| Size calculation | Manual: malloc(n * sizeof(T)) | Automatic from the type |
| Return type | void*, needs a cast in C++ | T*, no cast |
| On failure | Returns nullptr | Throws std::bad_alloc (or nullptr with new (std::nothrow)) |
| Deallocation | free | delete (delete[] for arrays) |
| Resize | realloc | No equivalent (use std::vector) |
| Customizable | No standard hook | Replace global or per-class operator new/operator delete |
| Over-aligned types | Only up to alignof(std::max_align_t); use aligned_alloc | Handled automatically since C++17 |
And the same view with make_unique added:
| Item | malloc | new | make_unique (C++14) |
|---|---|---|---|
| Constructor call | No | Yes | Yes |
| Type safety | No (cast needed) | Yes | Yes |
| On failure | Returns nullptr | Throws bad_alloc | Throws bad_alloc |
| Deallocation | Manual free | Manual delete | Automatic |
| Arrays | malloc(n * sizeof(T)) | new T[n] | make_unique<T[]>(n) |
| Leak-free on exceptions | Only with manual cleanup | Only with manual cleanup | Yes |
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
- malloc: raw bytes, no constructor,
nullptron failure,freeto release. Reserve it for C interop and allocator internals. - new: allocation plus construction, typed, throws on failure, must be paired with
delete/delete[], customizable viaoperator new. - make_unique: everything
newdoes, 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.