41 / 163 · C++11 · 11 min
malloc and the Allocator's Responsibilities
malloc provides uninitialized storage, returns a null pointer on failure, and must be paired with free; it does not run C++ constructors. An allocator may cache, bin, or request mappings from the system. Concrete implementations are not language guarantees. Requesting bytes and constructing objects should be understood as separate layers.
In this lesson
The Byte Interface Does Not Perform Construction
malloc requests storage by byte count. On success it returns void*; on failure it returns a null pointer. It does not call constructors and does not establish a valid internal state for a string or any other class type. The result is storage, not an object with class invariants. Modern standards have object-creation rules for some implicit-lifetime types, but that is not permission to skip construction of arbitrary classes. Explicit placement new makes it clear that an object is being created in existing storage.
The alignment malloc provides is enough for fundamental alignment requirements. It cannot be generalized to every over-aligned type. Over-aligned types should use an interface that supports their alignment. The result of a zero-byte request is implementation-defined; a non-null return does not let you infer that one byte may be accessed. The simplest business policy is to handle empty requests separately rather than decoding what a particular library returns for size zero.
Allocator Implementation and System Calls Are Not the Same Layer
A general-purpose allocator usually splits large chunks obtained from the system, reuses them, and maintains size classes or free blocks. One malloc need not issue a system call, and one free need not immediately shrink the process's resident memory. Caching, fragmentation, and the operating system's commit policy can all affect the numbers you monitor. Those numbers are therefore a poor stand-in for 'the block is still owned'.
These are common models for understanding performance, not an assertion that every malloc uses one fixed algorithm. Internal fragmentation comes from rounding block sizes and similar overhead; external fragmentation comes from free regions that are hard to combine. A C++ allocator is a replaceable allocation protocol for containers, separating the source of storage from management of element construction. The default allocator does not promise to call malloc directly.
Put a C Resource into an Owner Immediately
The example uses a custom deleter to hand malloc storage to unique_ptr<void, Free>. After checking for failure it constructs a non-throwing Record. The Record is destroyed explicitly first; the owner then frees the storage. The cleanup order of object versus storage is therefore visible, and delete is not misused. Matching free to malloc is part of that visible protocol.
When you need to allocate n elements, first check that multiplying n by sizeof(T) does not overflow; otherwise you may request a block that is too small. Receive realloc results through a temporary as well; on failure the original block must still be retained. For ordinary C++ containers or non-trivial objects, prefer vector. Do not use realloc to move object representations as a substitute for a move.
Pitfalls
malloc/freeandnew/deletemust not be mixed; the release function must match the protocol that obtained the resource.- Resident memory that does not drop immediately after
freeis not the same as a leak. First distinguish objects still owned by the business, allocator caches, and a genuine lost release path.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
#include <cstdlib>
#include <memory>
#include <new>
struct Free {
void operator()(void* p) const noexcept { std::free(p); }
};
struct Record {
int value;
explicit Record(int n) noexcept : value(n) {}
~Record() noexcept {}
};
int main() {
std::unique_ptr<void, Free> storage(std::malloc(sizeof(Record)));
if (!storage) throw std::bad_alloc();
Record* record = ::new (storage.get()) Record(42);
assert(record->value == 42);
record->~Record();
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-malloc.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
When computing the malloc byte count for n objects of type T, how do you avoid unsigned multiplication wraparound?
Show a reference answer
Handle n equal to zero separately. For a non-zero request, check n > std::numeric_limits<std::size_t>::max() / sizeof(T); if that holds, report a length error, and only then compute n * sizeof(T). This only fixes the size calculation. It does not solve alignment, element construction, or failure recovery. When you need a complete C++ element sequence, vector<T> is usually more appropriate than filling in every low-level step.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.