C / C++ - Memory Management
C++: new / delete
Use keywords new and delete, or smart pointers.
Read more: Smart Pointers
char *pBuffer = new char[1024];
delete[] pBuffer;
new vs malloc
Takeaways:
- You cannot mix
new/freeandmalloc/delete. - in C++, don't use
mallocfor C++ without a really good reason
new is more type-safe; malloc is not typesafe in any meaningful way. In C++ you are required to cast the return from void*.
Foo *f1 = malloc(1); // error, no cast
Foo *f2 = static_cast<Foo*>(malloc(sizeof(Foo)));
malloc returns NULL if allocation fails. new will throw std::bad_alloc.
int* p_scalar = new int(5); // Does not create 5 elements, but initializes to 5
int* p_array = new int[5]; // Creates 5 elements
Use malloc if you need to change the size of your buffer of data. realloc can be used to extend the size of a chunk of memory.
C: malloc() / calloc() / free()
Use functions malloc() / calloc() and free().
malloc()
malloc() = memory allocation
It returns a pointer of type void which can be cast into a pointer of any form. It doesn’t iniatialize memory at execution time so that it has initializes each block with the default garbage value initially.
Example:
ptr = (int*) malloc(100 * sizeof(int));
calloc()
calloc() = contiguous allocation, allocate the specified number of blocks of memory of the specified type.
void *calloc(size_t nitems, size_t size)
Example:
int* a = (int*) calloc(5, sizeof(int));
calloc() vs malloc()
malloc()does not set the memory to zero,calloc()sets allocated memory to zero.malloc()has one parameter,calloc()has two.
realloc()
ptr = realloc(ptr, newSize);
The function returns a void pointer to this memory location, which can then be cast to the desired type.
free()
Free the memory when it is no longer used.
Placement new
In C++, placement new is a version of the new operator that allows constructing an object at a specific, pre-allocated memory location. It separates memory allocation from object construction, offering control over where objects are created. Unlike the regular new operator, which allocates memory and then constructs the object, placement new only handles the construction part.
Placement new takes the form new (address) Type(arguments). Here, address is a pointer to the pre-allocated memory, Type is the class of the object to be constructed, and arguments are any constructor arguments. The operator returns a pointer to the constructed object at the specified address.
It's important to note that since placement new doesn't allocate memory, it's the programmer's responsibility to manage the memory lifecycle. This includes ensuring that the memory is properly allocated and deallocated, and that the object's destructor is called when it's no longer needed. Failure to do so can lead to memory leaks or other issues.
// Allocate memory
alignas(MyClass) char buffer[sizeof(MyClass)]; // Ensure proper alignment
// Use placement new to construct the object in the allocated memory
MyClass* obj = new (buffer) MyClass(10);
// Use the object
obj->printValue();
// Destroy the object (manually call the destructor)
obj->~MyClass();
Hardware Cache Lines & False Sharing Under the Hood
Modern multicore CPUs do not transfer memory byte-by-byte; they fetch memory in fixed-size chunks called Cache Lines (typically 64 bytes on modern x86 and ARM processors).
The False Sharing Problem
When two threads running on separate CPU cores modify independent variables that happen to reside within the same 64-byte cache line:
+---------------------------------------------------------------+
| Cache Line (64 Bytes) |
| +-----------------------------+---------------------------+ |
| | Thread 1 modifies: `data_a` | Thread 2 modifies: `data_b`| |
| +-----------------------------+---------------------------+ |
+---------------------------------------------------------------+
- Core 1 writes to
data_a. The CPU cache coherence protocol (MESI/MOESI) invalidates Core 2's entire cache line. - Core 2 writes to
data_b. It must re-fetch the cache line from L3 or main memory, invalidating Core 1's cache line. - The cache line constantly bounces between cores (cache line ping-pong), dropping multicore throughput by up to 90%, even though
data_aanddata_bare completely independent variables.
Prevention: Alignment & Padding
In C++17 and later, align variables to hardware cache boundaries:
#include <new>
// Ensure separate cache lines for concurrently accessed variables
struct alignas(std::hardware_destructive_interference_size) ThreadData {
uint64_t counter;
};
On older standards, manually enforce 64-byte alignment:
struct alignas(64) WorkerState {
int counter;
char padding[60]; // Pad to fill 64-byte line
};
C++ Memory Model & Atomics (std::memory_order)
To optimize performance, optimizing compilers and modern out-of-order CPUs reorder memory reads and writes. The C++11 memory model defines formal ordering guarantees for concurrent operations via std::atomic:
#include <atomic>
std::atomic<bool> ready{false};
int data = 0;
// Thread 1 (Producer)
data = 42;
ready.store(true, std::memory_order_release); // Release barrier
// Thread 2 (Consumer)
while (!ready.load(std::memory_order_acquire)) { // Acquire barrier
// Spin or wait
}
assert(data == 42); // Guaranteed to see data = 42
The Six Memory Orderings
| Memory Order | Synchronization Scope | Hardware Cost |
|---|---|---|
memory_order_relaxed |
Ensures atomicity of the single operation only. No synchronization or instruction ordering constraints. | Minimal (pure CPU load/store). |
memory_order_acquire |
Used on loads. Guarantees no subsequent reads/writes can be reordered before this load. | Free on x86 (hardware TSO); lightweight barrier on ARM (LDAR). |
memory_order_release |
Used on stores. Guarantees all prior writes are visible before this store completes. | Free on x86; lightweight barrier on ARM (STLR). |
memory_order_acq_rel |
Combines Acquire and Release for read-modify-write operations (fetch_add, compare_exchange). |
Used in lock-free queue head/tail updates. |
memory_order_seq_cst |
Sequential Consistency (Default). Enforces a single global total order observed by all threads. | Highest cost; requires bus lock or full memory fence (MFENCE). |
Undefined Behavior (UB) & Memory Corruption Vulnerabilities
Unlike Rust, which checks pointer validity at compile time, C and C++ entrust pointer management entirely to the developer. Violating invariants produces Undefined Behavior (UB), allowing the compiler to assume the condition cannot happen and generate exploitable machine code.
Common Memory Exploits in C/C++
- Buffer Overflow (Spatial): Writing past array bounds (
char buf[16]; buf[20] = 'x';), corrupting adjacent stack frame pointers or return addresses. - Use-After-Free (Temporal): Accessing heap memory after
delete ptr;orfree(ptr);. If an attacker fills the freed address with attacker-controlled data, calling virtual methods invokes shellcode. - Double-Free: Calling
deletetwice corrupts heap allocator chunk metadata, often leading to arbitrary code execution. - Strict Aliasing Violations: Casting
int*tofloat*and dereferencing breaks compiler optimization assumptions, causing silent data corruption.
Sanitizer Toolchain (ASan, TSan, UBSan, MSan)
Because memory safety is not proven statically by the C++ compiler, modern engineering teams rely on LLVM/GCC Sanitizers during testing:
# AddressSanitizer: Detects buffer overflows, Use-After-Free, and memory leaks
$ clang++ -fsanitize=address -g main.cpp -o main
# ThreadSanitizer: Detects data races between threads
$ clang++ -fsanitize=thread -g main.cpp -o main
# UndefinedBehaviorSanitizer: Detects signed integer overflow, null pointer dereferencing
$ clang++ -fsanitize=undefined -g main.cpp -o main
Sanitizers instrument memory accesses with shadow memory checks, serving as the primary defense for large C++ codebases before deploying to production.

