Skip to content
Shafin Chowdhury
Go back

malloc() in C++: Deep Dive, Memory Mechanics, and Modern Alternatives

Software Developer & Computer Science Student

Published:
31 min read
Edit page
Table of Contents85 topics

In C++, malloc() (short for memory allocation) is a standard library function inherited from C that requests a contiguous block of raw, uninitialized memory of a specified size in bytes from the system at runtime. If the request succeeds, it returns a generic void* pointer to the first byte of the allocated storage; if the system cannot satisfy the request, it returns a null pointer (nullptr).

While modern C++ programmers primarily rely on RAII (Resource Acquisition Is Initialization), standard containers like std::vector, and smart pointers like std::unique_ptr, understanding malloc() is essential. It underpins legacy codebases, operating system interfaces, embedded firmware, custom memory allocators, and C-to-C++ interoperability. More importantly, learning how malloc() operates bridges the gap between high-level C++ abstractions and the physical reality of how bytes and object lifetimes exist in computer memory.


SEO & Metadata Summary

PropertyValue
SEO Titlemalloc() in C++: Memory Mechanics, Pitfalls, and Modern Alternatives
Meta DescriptionMaster malloc() in C++. Learn dynamic heap allocation, void* casting, object lifetimes, malloc vs new, calloc, realloc, common pitfalls, and modern RAII alternatives.
URL Slug/posts/malloc-in-cpp/
Primary Keywordmalloc in C++
Secondary Keywordsmalloc() function in C++, dynamic memory allocation in C++, malloc vs new in C++, malloc syntax in C++, malloc example in C++, free() function in C++, malloc vs calloc, realloc in C++, memory allocation in C++, heap memory in C++
Open Graph Titlemalloc() in C++: The Complete Guide to Low-Level Dynamic Memory
Open Graph DescriptionA comprehensive, standards-compliant guide to std::malloc in C++: syntax, memory diagrams, lifetime semantics, malloc vs new, safety bugs, and std::vector alternatives.
Listing ExcerptLearn how malloc() works in C++ from the ground up: explore raw byte allocation, heap mechanics, void* type casting, object lifetimes, calloc, realloc, free, and why modern C++ prefers RAII containers.

What You’ll Learn

  • What malloc() does and why programs require dynamic memory allocation at runtime
  • How to include and use the standard C++ <cstdlib> header correctly
  • The exact syntax, parameters, return type (void*), and alignment guarantees of std::malloc
  • What the C++ standard dictates about edge cases like malloc(0)
  • How to allocate fundamental types and arrays safely, including byte calculations and integer overflow prevention
  • The critical difference between raw storage allocation and C++ object construction (implicit lifetime vs. non-trivial classes)
  • How malloc() compares conceptually to calloc(), realloc(), and the C++ new operator
  • Why memory allocated with malloc() must be released with std::free() and never delete or delete[]
  • 20 common memory bugs, including dangling pointers, double frees, buffer overflows, and memory leaks
  • Low-level class construction using placement new and explicit destructor calls
  • Idiomatic modern C++ replacements: std::vector, std::string, std::unique_ptr, and std::make_unique
  • Frequently Asked Questions answered clearly and directly

Introduction: Why Dynamic Memory Allocation Matters

When you write a basic C++ function, you typically declare local variables:

void processData() {
    int count = 100;
    double temperatures[50];
    // ...
}

These variables have automatic storage duration (often referred to informally as living on the “stack memory”). The compiler determines their sizes ahead of time during compilation. When processData() begins, space for these variables is reserved; when the function returns, that memory is automatically reclaimed by the runtime.

However, real-world software encounters scenarios where compile-time sizing fails completely:

  1. Unknown Input Sizes: A user uploads a CSV file containing an unpredictable number of rows. An array of fixed size 1000 is either wasteful (if the file has 3 rows) or disastrous (if the file has 1,000,000 rows, leading to stack overflow or memory exhaustion).
  2. Persistent Lifetimes: An object created inside a factory function needs to survive long after that specific function returns to its caller.
  3. Large Datasets: The runtime call stack allocated to a process is typically limited (commonly 1 MB to 8 MB on desktop operating systems). Declaring an array like double matrix[2000][2000]; consumes roughly 32 MB of memory and will instantly crash your program with a stack overflow.

To solve these problems, programs request dynamic memory allocation. This requests a chunk of memory from the operating system’s general memory pool (commonly called the heap or free store) whose size is calculated at runtime and whose lifetime persists until the programmer explicitly instructs the program to release it.

In C++, dynamic memory can be managed through several mechanisms:

  • The legacy C standard library functions: std::malloc(), std::calloc(), std::realloc(), and std::free()
  • The C++ operators: new and delete
  • Modern Standard Template Library (STL) containers and smart pointers: std::vector, std::string, std::unique_ptr, and std::shared_ptr

Even though modern C++ strongly favors RAII abstractions, every serious C++ engineer must master malloc() because it exposes the raw mechanics of computer hardware, byte layouts, and object lifetimes without syntactic sugar.


Understanding the <cstdlib> Header

In C++, malloc() is declared within the standard library header <cstdlib>.

#include <cstdlib>

<cstdlib> vs. <stdlib.h>

In older C code, memory functions lived in <stdlib.h>. In C++, the ISO committee established <c...> wrappers for all inherited C headers:

  • <stdlib.h> declares malloc, free, calloc, and realloc in the global namespace (::malloc).
  • <cstdlib> declares these functions inside the std namespace (std::malloc, std::free), and compilers may optionally also declare them in the global namespace.

Best Practice in Modern C++

In modern C++, you should include <cstdlib> and explicitly prefix calls with std:::

#include <iostream>
#include <cstdlib> // Required for malloc and std::free

int main() {
    // Explicitly scope with std:: to guarantee namespace correctness
    void* raw_memory = malloc(sizeof(int));

    if (raw_memory != nullptr) {
        cout << "Successfully allocated memory at: " << raw_memory << "\n";
        free(raw_memory);
    }

    return 0;
}

[!NOTE] Including <cstdlib> simply introduces function prototypes to the compiler. It does not allocate any memory on its own.


Syntax and Parameters of malloc()

The standard C++ declaration of malloc() is:

void* malloc(size_t size);

Let’s dissect each component of this signature:

1. The Parameter: std::size_t size

  • size represents the total number of bytes to allocate.
  • std::size_t is an unsigned integer type (defined in <cstddef> and <cstdlib>) guaranteed to be large enough to represent the size of the largest possible object on the target architecture (typically 32 bits on 32-bit systems, and 64 bits on 64-bit systems).
  • Because size is in bytes—not number of elements—you almost always multiply the number of items by the sizeof() operator (e.g., 10 * sizeof(int)).

2. The Return Type: void*

  • A void* is a generic pointer to raw, untyped memory. It represents a memory address without associating any data type or object layout with that address.
  • In pure C, a void* converts implicitly to any other pointer type. In C++, however, implicit conversion from void* to a typed pointer T* is ill-formed (a compile-time error).
  • Therefore, in C++, you must explicitly cast the returned pointer using static_cast<T*>():
// Valid in C, but fails to compile in C++:
// int* p = malloc(sizeof(int));

// Correct in C++:
int* p = static_cast<int*>(std::malloc(sizeof(int)));

3. Allocation Success

If the system allocator finds an unallocated block of memory that satisfies the requested byte count, std::malloc:

  1. Reserves that block so subsequent allocations will not overwrite it.
  2. Returns a pointer to the first byte of that block.
  3. Guarantees that the address returned is suitably aligned for any fundamental type whose alignment requirements fit within default alignment (alignof(std::max_align_t)).

4. Allocation Failure

If the operating system cannot satisfy the request (for instance, the process has hit virtual memory limits or physical RAM and swap are exhausted), std::malloc does not throw an exception. Instead, it returns a null pointer (nullptr / NULL).

Because dereferencing a null pointer causes undefined behavior (usually an immediate segmentation fault / crash), every call to malloc() must be checked before use:

int* data = static_cast<int*>(std::malloc(100 * sizeof(int)));
if (data == nullptr) {
    // Handle error gracefully (e.g., log, cleanup, throw, or return)
    std::cerr << "Memory allocation failed!\n";
    return 1;
}

5. The Edge Case: What Happens with malloc(0)?

What happens if you execute std::malloc(0)?

Under ISO C++ and ISO C standards, calling malloc(0) is implementation-defined:

  • The allocator may return a null pointer (nullptr).
  • Or it may return a non-null pointer to a zero-sized block. If a non-null pointer is returned, that pointer must not be dereferenced, but it must still be passed to std::free() to prevent memory leaks.
void* p = std::malloc(0);
// p might be nullptr, or it might be a valid, unique pointer handle!
// In either case, dereferencing *p is undefined behavior.
std::free(p); // Completely valid and safe.

Never assume malloc(0) returns nullptr to test whether zero bytes were requested.


First Working Example: Allocating an Integer

Here is a minimal, complete C++ program that allocates, initializes, uses, and frees an integer using std::malloc:

#include <iostream>
#include <cstdlib>

int main() {
    // Step 1: Request 4 bytes (or sizeof(int)) of raw memory
    int* ptr = static_cast<int*>(std::malloc(sizeof(int)));

    // Step 2: Validate that allocation succeeded
    if (ptr == nullptr) {
        std::cerr << "Fatal Error: Failed to allocate memory on the heap.\n";
        return 1;
    }

    // Step 3: Initialize and use the allocated memory
    *ptr = 42;
    std::cout << "Value stored in heap memory: " << *ptr << "\n";
    std::cout << "Memory address: " << ptr << "\n";

    // Step 4: Deallocate the memory back to the system
    std::free(ptr);

    // Step 5: Prevent dangling pointer hazards
    ptr = nullptr;

    return 0;
}

Line-by-Line Breakdown:

  1. sizeof(int) evaluates at compile-time to the size of an integer on the host platform (almost universally 4 bytes on modern 32-bit and 64-bit systems).
  2. std::malloc(sizeof(int)) asks the OS heap manager for 4 contiguous bytes.
  3. static_cast<int*>(...) transforms the untyped void* into an int*, allowing the compiler to enforce type checking and pointer arithmetic.
  4. if (ptr == nullptr) guards against out-of-memory states.
  5. *ptr = 42; dereferences the pointer to write the binary value 42 into the allocated storage.
  6. std::free(ptr) informs the allocator that the 4-byte block is no longer needed.
  7. ptr = nullptr neutralizes the pointer so subsequent inadvertent uses can be caught immediately rather than silently corrupting memory.

The C++20 Object Lifetime Rule

Notice something subtle here: std::malloc merely reserves raw, untyped bytes. It does not invoke constructors. Why was it legal to write *ptr = 42; without calling a constructor?

In C++ (specifically clarified in C++20 via proposal P0593R6: Implicit Object Creation), functions like std::malloc, std::calloc, and std::realloc are recognized as implicit object creation functions. When they allocate storage, they implicitly begin the lifetime of objects of implicit-lifetime types (which includes scalar types like int, double, char*, and C-style structs consisting of such types).

However, for non-trivial C++ classes (types with custom constructors, destructors, or virtual functions), std::malloc does not create a valid live object! We explore this critical distinction in the Using malloc() with C++ Classes section.


How malloc() Works Conceptually

To truly understand dynamic memory, visualize how a running program interacts with your computer’s virtual address space.

When your program launches, the operating system assigns it a virtual memory layout typically segmented as follows:

+-------------------------------------------------------+
| Text (Code) Segment: Compiled machine instructions   |
+-------------------------------------------------------+
| Data / BSS Segments: Global and static variables      |
+-------------------------------------------------------+
| Stack: Local variables, function frames (grows down)  |
|        |                                              |
|        v                                              |
|                                                       |
|        ^                                              |
|        |                                              |
| Heap / Free Store: Dynamic allocations (grows up)     |
+-------------------------------------------------------+

When you invoke std::malloc(sizeof(int)):

STACK (Local Scope)                            HEAP (Dynamic Storage)
+-----------------------+                      +-----------------------+
| ptr: [ 0x0050A000 ] --+--------------------> | Memory Address:       |
| (holds 8-byte address)|                      | 0x0050A000: [ 42 ]    |
+-----------------------+                      | (4 bytes reserved)    |
                                               +-----------------------+

The Allocation Flow:

  1. The Request: Your application executes std::malloc(1024).
  2. The Heap Manager Check: The C runtime library’s allocator (such as glibc ptmalloc, Windows Heap, jemalloc, or tcmalloc) inspects internal free lists, arenas, or bins to locate a chunk of already-available virtual memory that can accommodate 1024 bytes plus allocator metadata (bookkeeping headers that record the chunk’s size and status).
  3. OS System Calls: If the allocator’s free pools do not have enough memory, it issues low-level system calls (such as brk() / sbrk() or mmap() on Linux/macOS, or VirtualAlloc() on Windows) to map new virtual memory pages from the kernel into the process space.
  4. Pointer Return: The allocator records bookkeeping metadata just before or alongside the block and returns the memory address directly after the metadata header to your application as a void*.
  5. Deallocation: When you call std::free(ptr), the allocator reads the hidden metadata header preceding ptr to determine how many bytes were in that chunk, and places that block back onto its internal free list for future recycling.

[!IMPORTANT] The exact layout, bookkeeping headers, and pooling algorithms are implementation-dependent. The C++ standard only guarantees that if malloc() returns a non-null pointer, that pointer refers to a contiguous, unshared block of the requested size, aligned for any standard data type.


Allocating Arrays with malloc()

To allocate a contiguous array of elements at runtime using malloc(), multiply the desired number of elements by the size of an individual element:

Total Bytes=n×sizeof(element)\text{Total Bytes} = n \times \text{sizeof(element)}

Array Allocation Example

#include <iostream>
#include <cstdlib>

int main() {
    std::size_t num_elements = 5;

    // Calculate total bytes: 5 * sizeof(int) = 20 bytes
    int* array = static_cast<int*>(std::malloc(num_elements * sizeof(int)));

    if (array == nullptr) {
        std::cerr << "Memory allocation failed!\n";
        return 1;
    }

    // CRITICAL: malloc does NOT zero-initialize.
    // Elements contain garbage values until explicitly initialized.
    for (std::size_t i = 0; i < num_elements; ++i) {
        array[i] = static_cast<int>((i + 1) * 10); // Safe initialization
    }

    // Access elements via subscript indexing
    std::cout << "Array elements: ";
    for (std::size_t i = 0; i < num_elements; ++i) {
        std::cout << array[i] << " ";
    }
    std::cout << "\n";

    // You can also access elements using pointer arithmetic: *(array + i)
    std::cout << "Third element via pointer arithmetic: " << *(array + 2) << "\n";

    // Clean up memory
    std::free(array);
    array = nullptr;

    return 0;
}

Key Array Rules:

  1. sizeof(array) vs sizeof(*array): If you execute sizeof(array), the compiler returns the size of the pointer variable itself (typically 8 bytes on a 64-bit architecture), not the size of the allocated heap array! To refer to the element type dynamically, use sizeof(*array) or sizeof(int).
  2. Uninitialized Memory Hazard: Unlike some high-level languages, malloc() does not fill your array with zeros. Reading from array[i] before writing to it reads arbitrary leftover bits (“garbage values”), triggering undefined behavior if treated as initialized data.
  3. Matching Deallocation: An array allocated with malloc() must be freed using std::free(array). You must never use delete[] array on memory obtained from malloc(). Mixing allocators causes heap corruption and program crashes.

Allocating Memory for Different Data Types

Because malloc() allocates raw bytes, it can allocate storage for any primitive or C-compatible type, provided you pass the correct byte count calculated via sizeof().

Common Sizing Idioms

// 1. Primitive single variables
char* ch_ptr    = static_cast<char*>(std::malloc(sizeof(char)));
double* dbl_ptr = static_cast<double*>(std::malloc(sizeof(double)));

// 2. Arrays of primitive types
std::size_t n = 50;
double* matrix_row = static_cast<double*>(std::malloc(n * sizeof(double)));

// 3. Robust idiom: sizeof(*ptr)
// If the type of 'buffer' changes from int* to long*, this line remains correct!
long* buffer = static_cast<long*>(std::malloc(n * sizeof(*buffer)));

Allocating Simple C-Style Structs

You can use malloc() with Plain Old Data (POD) structs:

#include <iostream>
#include <cstdlib>

struct Point2D {
    float x;
    float y;
};

int main() {
    Point2D* pt = static_cast<Point2D*>(std::malloc(sizeof(Point2D)));
    if (!pt) return 1;

    pt->x = 12.5f;
    pt->y = 45.0f;

    std::cout << "Point: (" << pt->x << ", " << pt->y << ")\n";

    std::free(pt);
    pt = nullptr;
    return 0;
}

Memory Alignment & Over-Aligned Types

Every type in C++ has an alignment requirement (obtainable via alignof(T)). For instance, an int on x86-64 requires 4-byte alignment, meaning its memory address must be a multiple of 4. A double typically requires 8-byte alignment.

The C++ standard guarantees that std::malloc returns an address aligned suitably for any fundamental type (up to std::max_align_t, which is usually 16 bytes on modern 64-bit systems).

However, if you define an over-aligned type using alignas(32) or alignas(64) (such as AVX-512 SIMD vector types), std::malloc is not guaranteed to return a sufficiently aligned address. Using standard malloc() on over-aligned types causes undefined behavior unless aligned allocation functions like std::aligned_alloc (C++17) or platform-specific APIs (_aligned_malloc on Windows, posix_memalign on POSIX) are used.


Memory Initialization: malloc() vs. calloc()

The C++ standard library <cstdlib> also provides calloc() (contiguous allocation).

Syntax of calloc()

void* std::calloc(std::size_t num, std::size_t size);

Unlike malloc(), which takes a single byte parameter, calloc() takes two arguments:

  1. num: The number of elements to allocate.
  2. size: The size of each element in bytes.

Furthermore, calloc() clears all allocated bytes to zero.

#include <iostream>
#include <cstdlib>

int main() {
    std::size_t count = 4;

    // Allocate 4 integers, all zeroed out
    int* zero_data = static_cast<int*>(std::calloc(count, sizeof(int)));

    if (!zero_data) return 1;

    std::cout << "calloc values: ";
    for (std::size_t i = 0; i < count; ++i) {
        std::cout << zero_data[i] << " "; // Guaranteed output: 0 0 0 0
    }
    std::cout << "\n";

    std::free(zero_data);
    return 0;
}

malloc() vs. calloc() Comparison

Featurestd::malloc()std::calloc()
Number of Parameters1 (std::size_t size)2 (num, size)
CalculationCaller multiplies: n * sizeof(T)Function multiplies internally
InitializationLeaves memory uninitialized (garbage)Initializes all bytes to bitwise zero (0x00)
Overflow ProtectionCaller must check before multiplyingAllocator fails gracefully if num * size overflows
PerformanceFaster (no clearing overhead)Slightly slower (must zero memory, though OS page optimizations can mitigate this)
Deallocationstd::free()std::free()

[!WARNING] While setting all bits to zero produces the integer value 0 on all modern platforms, the C++ standard does not technically guarantee that an all-zero bit pattern represents a valid null pointer representation for all architectures, nor does it construct C++ objects! Never use calloc() to instantiate classes containing non-trivial types.


Resizing Allocations with realloc()

Frequently, you do not know the final size of a dynamic buffer in advance. The function std::realloc() adjusts the size of a previously allocated block of memory without losing existing data:

void* std::realloc(void* ptr, std::size_t new_size);

How realloc() Operates:

  1. In-Place Expansion: If there is contiguous free space immediately following the current block on the heap, the allocator simply extends the boundary of the existing block and returns the exact same pointer ptr.
  2. Relocation: If adjacent memory is already occupied, the allocator allocates a completely new block of new_size bytes elsewhere on the heap, copies the contents of the old block up to min⁡(old_size,new_size)\min(\text{old\_size}, \text{new\_size}), deallocates the old block, and returns the pointer to the new block.
  3. Shrinking: If new_size is smaller than the original size, the allocator shrinks the allocation in place and reclaims the excess bytes.
  4. Failure: If the system cannot allocate new_size bytes, the original memory block is left untouched and remains completely valid, and realloc() returns nullptr.

The Safe realloc() Pattern (Avoiding Memory Leaks)

A classic rookie mistake is reassigning the returned pointer directly to the original variable:

// DANGEROUS ROOKIE ERROR:
int* data = static_cast<int*>(std::malloc(10 * sizeof(int)));
// ...
data = static_cast<int*>(std::realloc(data, 1000000000 * sizeof(int)));
// IF REALLOC FAILS: data becomes nullptr, and the original 10-int block is LEAKED FOREVER!

To resize safely, always store the result in a temporary pointer first:

#include <iostream>
#include <cstdlib>
#include <limits>

int main() {
    std::size_t initial_count = 3;
    int* numbers = static_cast<int*>(std::malloc(initial_count * sizeof(int)));
    if (!numbers) return 1;

    for (std::size_t i = 0; i < initial_count; ++i) {
        numbers[i] = static_cast<int>((i + 1) * 100);
    }

    // We want to expand the array to hold 6 integers
    std::size_t new_count = 6;

    // Check for multiplication overflow
    if (new_count > std::numeric_limits<std::size_t>::max() / sizeof(int)) {
        std::cerr << "Requested size causes arithmetic overflow.\n";
        std::free(numbers);
        return 1;
    }

    // Step 1: Assign to a temporary pointer
    int* temp = static_cast<int*>(std::realloc(numbers, new_count * sizeof(int)));

    // Step 2: Validate the result
    if (temp == nullptr) {
        std::cerr << "Reallocation failed! Original buffer is preserved.\n";
        // numbers is still valid and must be freed
        std::free(numbers);
        return 1;
    }

    // Step 3: Successfully reallocated! Update the primary pointer
    numbers = temp;

    // Step 4: Initialize the newly added elements
    for (std::size_t i = initial_count; i < new_count; ++i) {
        numbers[i] = static_cast<int>((i + 1) * 100);
    }

    std::cout << "Expanded array: ";
    for (std::size_t i = 0; i < new_count; ++i) {
        std::cout << numbers[i] << " ";
    }
    std::cout << "\n";

    std::free(numbers);
    numbers = nullptr;
    return 0;
}

Critical realloc() Warnings:

  • Pointers Invalidation: If realloc() relocates the buffer, any other pointer that pointed into the old memory block is now a dangling pointer.
  • No Class Support: Never use std::realloc() on memory containing non-trivially copyable C++ class objects (such as std::string or std::vector). realloc() performs a bitwise copy (std::memcpy) and does not invoke move constructors or copy constructors!

Releasing Memory with free()

Every single byte allocated with std::malloc(), std::calloc(), or std::realloc() must eventually be returned to the runtime system using std::free():

void std::free(void* ptr);

Core Rules of free():

  1. Exact Pointer Requirement: The pointer passed to std::free() must be an exact address previously returned by malloc, calloc, or realloc. Passing an offset pointer (e.g., free(ptr + 2)) triggers undefined behavior and crashes the heap manager.
  2. Free Exactly Once: Attempting to free the same heap block twice without an intervening allocation is known as a double free. It corrupts allocator internal linked lists or buckets and creates serious security vulnerabilities (e.g., heap exploitation).
  3. Pairing Matching Allocators:
    • malloc() ⟷\longleftrightarrow free()
    • new ⟷\longleftrightarrow delete
    • new[] ⟷\longleftrightarrow delete[] Never cross these pairs! Calling delete or delete[] on a pointer obtained from malloc() is undefined behavior.
  4. free(nullptr) is Safe: Passing a null pointer to std::free() is guaranteed by both the C and C++ standards to be a safe no-op. You do not need to write if (ptr != nullptr) free(ptr);.
  5. Dangling Pointers and Re-assignment: Calling std::free(ptr) releases the memory block, but the variable ptr itself still holds the dead address. To prevent accidental use-after-free bugs, set the pointer to nullptr immediately after freeing it:
std::free(ptr);
ptr = nullptr; // Neutralized

malloc() vs. new in C++

The comparison between malloc() and the C++ new operator is one of the most fundamental topics in C++ software engineering.

Deep Comparative Analysis

+---------------------+-----------------------------------+-----------------------------------+
| Feature             | std::malloc()                     | C++ new Operator                  |
+---------------------+-----------------------------------+-----------------------------------+
| Origin              | Inherited from C (<cstdlib>)      | Native C++ Language Keyword       |
| Return Type         | void* (requires explicit cast)    | Typed pointer T* (type-safe)      |
| What It Allocates   | Raw, uninitialized bytes         | Fully initialized live objects    |
| Constructor Calling | NEVER calls constructors          | ALWAYS calls constructors         |
| Destructor Calling  | std::free() NEVER calls dtors     | delete ALWAYS calls destructors   |
| Failure Handling    | Returns nullptr                   | Throws std::bad_alloc exception   |
| Size Calculation    | Manual: n * sizeof(T)             | Automatic: handled by compiler    |
| Resizing Support    | std::realloc() supported          | No direct resizing mechanism      |
| Customization       | Cannot easily overload per-type   | Can overload operator new / delete|
| Deallocation Call   | std::free(ptr)                    | delete ptr or delete[] ptr        |
+---------------------+-----------------------------------+-----------------------------------+

The Fundamental Distinction: Allocation vs. Construction

The most profound difference between malloc and new is the separation of memory allocation and object lifetime:

  • std::malloc is a library function that allocates a chunk of raw memory. It knows nothing about types, constructors, or invariants.
  • The new expression performs two distinct tasks under the hood:
    1. It calls an allocation function (operator new(sizeof(T))) to obtain raw storage.
    2. It invokes the constructor of type T to construct an active object in that storage.

Conversely, delete:

  1. Invokes the destructor ~T() of the object to release any resources held by the object.
  2. Calls the deallocation function (operator delete(ptr)) to return the raw memory to the heap.

Side-by-Side Demonstration with a Class

Consider this program illustrating the difference:

#include <iostream>
#include <cstdlib>
#include <string>

class Student {
public:
    std::string name;
    int rollNumber;

    Student(std::string n, int r) : name(std::move(n)), rollNumber(r) {
        std::cout << "[Constructor] Student " << name << " created.\n";
    }

    ~Student() {
        std::cout << "[Destructor] Student " << name << " destroyed.\n";
    }
};

int main() {
    std::cout << "--- 1. Using C++ new and delete ---\n";
    // 1. Allocates memory AND calls the constructor
    Student* s1 = new Student("Shafin", 101);
    std::cout << "Student name: " << s1->name << "\n";
    // 2. Calls the destructor AND deallocates memory
    delete s1;

    std::cout << "\n--- 2. What happens with malloc() and free()? ---\n";
    // Allocates bytes, BUT DOES NOT CALL CONSTRUCTOR!
    Student* s2 = static_cast<Student*>(std::malloc(sizeof(Student)));
    if (!s2) return 1;

    // DANGEROUS UNDEFINED BEHAVIOR:
    // s2->name is an unconstructed std::string object!
    // Reading or writing to s2->name directly will likely crash or corrupt memory:
    // std::cout << s2->name; // CRASH!

    // std::free releases bytes, BUT DOES NOT CALL DESTRUCTOR!
    std::free(s2);
    std::cout << "Raw memory freed without invoking destructor.\n";

    return 0;
}

Allocation Failure: Exceptions vs. nullptr

By default, when a standard C++ new expression fails to allocate memory, it throws a std::bad_alloc exception (defined in <new>), rather than returning nullptr.

If you specifically want new to return a null pointer on failure instead of throwing an exception, use the non-throwing variant:

#include <new>

// Returns nullptr on failure instead of throwing std::bad_alloc
int* ptr = new (std::nothrow) int[1000000];
if (!ptr) {
    std::cerr << "Allocation failed!\n";
}

malloc() vs. calloc() vs. realloc() vs. new

To solidify your understanding, review this complete comparison matrix across all four allocation methods:

Criteriamalloc()calloc()realloc()C++ new Operator
Primary PurposeAllocate raw bytesAllocate and zero-fillResize existing allocationAllocate and construct object
Number of Arguments1 (size_t bytes)2 (count, size)2 (void* ptr, size_t new_size)Operates on types (new T or new T[n])
Zero-Initialization❌ No (garbage bits)✅ Yes (all bits 0)❌ No (new bytes are garbage)Optional (new int vs new int())
Calls Constructor❌ Never❌ Never❌ Never✅ Always
Calls Destructor❌ Never❌ Never❌ Never✅ Always (via delete)
Failure IndicationReturns nullptrReturns nullptrReturns nullptrThrows std::bad_alloc (by default)
Can Resize In-Place❌ No❌ No✅ Yes (if heap permits)❌ No
Matching Releasestd::free()std::free()std::free()delete or delete[]
Best Used ForC APIs, custom memory poolsZeroed buffers for POD typesResizing dynamic byte buffersNormal C++ object creation

Dynamic Memory Allocation vs. Stack Allocation

In computer science discussions, programmers frequently compare the “stack” and the “heap”.

[!NOTE] Strictly speaking, the ISO C++ standard does not mention the hardware terms “stack” or “heap”. The standard defines automatic storage duration, static storage duration, thread storage duration, and dynamic storage duration. Modern operating systems and compilers implement automatic storage using a call stack, and dynamic storage using a heap/free store.

Key Conceptual Differences

AspectAutomatic Storage (“Stack”)Dynamic Storage (“Heap / Free Store”)
Allocation MechanismAutomatic pointer adjustment (SP register)Dynamic allocator algorithms (free lists, page tables)
SpeedBlazing fast (a few CPU clock cycles)Moderate to slow (system calls, thread locks)
LifetimeTied strictly to enclosing function scopeExplicitly controlled by the programmer
Size LimitSmall and rigid (commonly 1 MB to 8 MB)Vast (limited only by system RAM and virtual swap)
FragmentationImpossible (strictly ordered LIFO push/pop)Possible (holes created by varied allocation sizes)
Management ResponsibilityThe compilerThe programmer (or RAII container)
RiskStack overflow on excessive recursion/large arraysMemory leaks, dangling pointers, heap corruption

Neither approach is superior in all dimensions:

  • Use automatic variables for small, short-lived variables whose sizes are fixed at compile time.
  • Use dynamic allocation for large datasets, dynamic arrays, and resources whose lifetimes must outlive their creating function.

20 Common Mistakes When Using malloc() in C++

Working with raw pointers and unmanaged memory invites errors. Here are 20 of the most common mistakes, along with clear explanations and corrections.

1. Forgetting to Include <cstdlib>

  • Error: Calling malloc() without including <cstdlib> causes compilation errors.
  • Fix: Always include #include <cstdlib>.

2. Forgetting to Check for Allocation Failure

  • Error: Immediately dereferencing the returned pointer without checking if it is nullptr.
  • Fix: Always check if (ptr == nullptr).

3. Allocating Too Few Bytes

  • Error: Writing malloc(10) expecting to store 10 integers (allocating 10 bytes instead of 10×4=4010 \times 4 = 40 bytes).
  • Fix: Always multiply by the type size: malloc(10 * sizeof(int)).

4. Using sizeof(ptr) Instead of sizeof(*ptr)

  • Error: int* ptr = static_cast<int*>(malloc(10 * sizeof(ptr)));. sizeof(ptr) yields 8 (pointer size), which might happen to equal 2 * sizeof(int) on 64-bit systems, causing confusing out-of-bounds corruption.
  • Fix: Write sizeof(*ptr) or sizeof(int).

5. Integer Overflow in Allocation Size Calculation

  • Error: Calculating n * sizeof(int) when n is large (e.g., 2302^{30} on 32-bit systems) overflows std::size_t, wrapping around to a tiny number of bytes. The allocation succeeds, but writing nn elements overflows the buffer.
  • Fix: Validate that n <= std::numeric_limits<std::size_t>::max() / sizeof(int).

6. Reading Uninitialized Storage

  • Error: Expecting malloc() to zero memory:
    int* p = static_cast<int*>(std::malloc(sizeof(int)));
    std::cout << *p; // Reads indeterminate garbage bits!
  • Fix: Initialize before reading, or use std::calloc().

7. Mismatching malloc() with delete or delete[]

  • Error:
    int* arr = static_cast<int*>(std::malloc(10 * sizeof(int)));
    delete[] arr; // UNDEFINED BEHAVIOR: Crashes or corrupts heap!
  • Fix: Release with std::free(arr).

8. Mismatching new with free()

  • Error: Allocating with new and releasing with std::free(). This skips destructor execution and corrupts allocator state.
  • Fix: Always pair new with delete.

9. Memory Leaks (Forgetting to Free)

  • Error: Allocating memory inside a loop or function without deallocating it before losing the pointer.
  • Fix: Ensure every execution path calls std::free(), or migrate to std::vector / std::unique_ptr.

10. Double Free

  • Error: Calling std::free(ptr) twice on the same memory block.
  • Fix: Set ptr = nullptr; immediately after the first free().

11. Use-After-Free / Dereferencing a Dangling Pointer

  • Error:
    std::free(ptr);
    *ptr = 100; // Undefined Behavior: Memory was already returned to the OS!
  • Fix: Never access memory through a freed pointer.

12. Overwriting the Pointer Returned by realloc() on Failure

  • Error: ptr = realloc(ptr, new_size);. If realloc() fails, it returns nullptr, obliterating the only handle to the original block.
  • Fix: Use a temporary pointer: void* temp = std::realloc(ptr, new_sz);.

13. Using Stale Pointers After a Successful realloc()

  • Error: If you have multiple pointer aliases pointing into an array and realloc() moves the buffer to a new address, all previous aliases become invalid dangling pointers.
  • Fix: Update all references to point into the relocated buffer.

14. Calling malloc(0) and Blindly Dereferencing the Result

  • Error: Assuming malloc(0) returns a usable buffer.
  • Fix: Never dereference the pointer returned by a zero-byte allocation.

15. Using malloc() on Classes with Non-Trivial Constructors

  • Error:
    std::string* s = static_cast<std::string*>(std::malloc(sizeof(std::string)));
    *s = "Hello World"; // CRASH: Internal string buffer pointers were never initialized!
  • Fix: Use new std::string("Hello World") or placement new.

16. Assuming calloc() Constructs C++ Objects

  • Error: Thinking that filling memory with zero bytes satisfies class invariants for complex objects.
  • Fix: Non-trivial C++ objects require their constructors to be called.

17. Ignoring Alignment for Over-Aligned Types

  • Error: Using malloc() for types marked with alignas(64).
  • Fix: Use C++17 std::aligned_alloc or specialized platform APIs.

18. Freeing an Offset Pointer

  • Error:
    int* p = static_cast<int*>(std::malloc(10 * sizeof(int)));
    p++;
    std::free(p); // FATAL CRASH: Not the base address returned by malloc!
  • Fix: Retain the original base pointer to pass to free().

19. Type Casting to the Wrong Pointer Type

  • Error: double* p = static_cast<double*>(std::malloc(sizeof(float)));. Sizing mismatches cause buffer overflows.
  • Fix: Always match the cast type with the sizeof type: sizeof(*p).

20. Treating free() as a Guard for Pointer Aliases

  • Error: If p1 and p2 point to the same allocation, calling free(p1); p1 = nullptr; leaves p2 as a dangerous dangling pointer.
  • Fix: Understand resource ownership; avoid untracked raw pointer aliasing.

Memory Leaks and Undefined Behavior

Memory mismanagement in C++ creates catastrophic vulnerabilities:

+-----------------------+--------------------------------------------------------------+
| Bug Type              | Concrete Danger                                              |
+-----------------------+--------------------------------------------------------------+
| Memory Leak           | Memory usage grows monotonically until the OS kills process  |
| Dangling Pointer      | Reads stale data or corrupts newly allocated unrelated data  |
| Double Free           | Exploitable heap corruption; immediate crash via SIGABRT     |
| Buffer Overflow       | Remote Code Execution (RCE), stack/heap memory corruption    |
| Use-After-Free        | Critical security vulnerability (CWE-416)                    |
| Uninitialized Read    | Non-deterministic behavior, logical bugs, security leaks     |
+-----------------------+--------------------------------------------------------------+

Detection Tools: Sanitizers and Valgrind

Modern compilers provide built-in instrumentation tools that catch these bugs at runtime with negligible overhead:

  1. AddressSanitizer (ASan): Catches out-of-bounds accesses, use-after-free, and double frees. Compile with:
    clang++ -fsanitize=address -g main.cpp -o main
  2. LeakSanitizer (LSan): Included automatically with ASan on Linux and macOS; pinpoints exact line numbers of unreleased memory blocks upon program termination.
  3. UndefinedBehaviorSanitizer (UBSan): Catches signed integer overflows, misaligned pointers, and null pointer dereferences:
    clang++ -fsanitize=undefined -g main.cpp -o main
  4. Valgrind (Memcheck): On Linux platforms, Valgrind runs your compiled binary in an emulated CPU environment to track every single memory read, write, and allocation without recompilation:
    valgrind --leak-check=full --show-leak-kinds=all ./main

Integer Overflow and Safe Allocation Sizes

When allocating an array of nn elements, calculating n * sizeof(T) is vulnerable to arithmetic overflow.

Consider a 32-bit system where std::size_t caps at 232−1≈4.29×1092^{32} - 1 \approx 4.29 \times 10^9. If a program accepts a user-specified n = 1,073,741,825 (230+12^{30} + 1) and multiplies it by sizeof(int) (4 bytes):

n×4=(230+1)×4=232+4≡4(mod232)n \times 4 = (2^{30} + 1) \times 4 = 2^{32} + 4 \equiv 4 \pmod{2^{32}}

The expression evaluates to just 4 bytes! std::malloc(4) allocates space for exactly one integer. When the subsequent loop attempts to write 1,073,741,8251,073,741,825 integers, it writes past the allocated 4 bytes, causing a massive heap buffer overflow.

Safe Sizing Function

#include <iostream>
#include <cstdlib>
#include <limits>
#include <cstddef>

template <typename T>
T* safe_malloc_array(std::size_t count) {
    constexpr std::size_t max_allowed = std::numeric_limits<std::size_t>::max() / sizeof(T);

    // Step 1: Detect multiplication overflow BEFORE it occurs
    if (count > max_allowed) {
        std::cerr << "Requested count exceeds maximum representable size!\n";
        return nullptr;
    }

    // Step 2: Safe multiplication
    std::size_t total_bytes = count * sizeof(T);
    return static_cast<T*>(std::malloc(total_bytes));
}

int main() {
    std::size_t large_count = 1000;
    int* buffer = safe_malloc_array<int>(large_count);

    if (!buffer) {
        std::cerr << "Allocation failed or overflow detected.\n";
        return 1;
    }

    buffer[0] = 123;
    std::cout << "Successfully allocated and wrote: " << buffer[0] << "\n";

    std::free(buffer);
    return 0;
}

Using malloc() with C++ Classes

Can you use malloc() to manage memory for C++ classes?

The answer is yes, but only if you explicitly manage object lifetimes yourself.

Because malloc() only allocates raw bytes, you must use placement new to run the class constructor inside that memory, and you must explicitly invoke the destructor before calling std::free().

Low-Level Pattern: Placement New and Explicit Destruction

#include <iostream>
#include <cstdlib>
#include <string>
#include <new> // Required for placement new

class Player {
public:
    std::string username;
    int level;

    Player(std::string name, int lvl)
        : username(std::move(name)), level(lvl) {
        std::cout << "[Constructor] Player '" << username << "' created at level " << level << ".\n";
    }

    ~Player() {
        std::cout << "[Destructor] Player '" << username << "' destroyed.\n";
    }

    void display() const {
        std::cout << "Player: " << username << " | Level: " << level << "\n";
    }
};

int main() {
    // Step 1: Allocate raw, uninitialized storage on the heap
    void* raw_storage = std::malloc(sizeof(Player));
    if (!raw_storage) {
        std::cerr << "Memory allocation failed!\n";
        return 1;
    }

    // Step 2: Placement new
    // Constructs the Player object in-place at the exact memory address 'raw_storage'
    Player* p = new (raw_storage) Player("Aragorn", 99);

    // Step 3: Use the live object
    p->display();

    // Step 4: Explicitly invoke the destructor
    // Normal 'delete p' would call the wrong deallocation function!
    p->~Player();

    // Step 5: Release the raw heap memory
    std::free(raw_storage);
    p = nullptr;

    return 0;
}

Modern C++20 Lifetimes: std::construct_at and std::destroy_at

In C++20, the standard library introduced cleaner, standardized utilities in <memory> that formalize in-place construction and destruction:

#include <memory>
#include <cstdlib>

void modern_lifecycle_example() {
    void* mem = std::malloc(sizeof(Player));
    if (!mem) return;

    // C++20 std::construct_at replaces placement new syntax:
    Player* p = std::construct_at(static_cast<Player*>(mem), "Legolas", 85);
    p->display();

    // C++20 std::destroy_at replaces explicit p->~Player() call:
    std::destroy_at(p);

    std::free(mem);
}

[!NOTE] Placement new and std::construct_at are specialized, low-level tools primarily used by developers writing custom allocators, memory arenas, or container libraries (such as the internal implementation of std::vector). You should not write this in everyday application code.


Modern C++ Alternatives to malloc()

In modern C++ (C++11 through C++26), manual memory management using raw pointers and malloc/free is considered a code smell. The C++ standard provides powerful, zero-overhead abstractions built on RAII (Resource Acquisition Is Initialization):

+-----------------------+--------------------------------------------------------------+
| Modern C++ Primitive  | Primary Use Case                                             |
+-----------------------+--------------------------------------------------------------+
| std::vector<T>        | Dynamic arrays that automatically resize, manage, and free   |
| std::string           | Dynamic textual strings with automatic null-termination      |
| std::unique_ptr<T>    | Sole, exclusive ownership of a dynamically allocated object  |
| std::shared_ptr<T>    | Shared ownership of an object with reference counting        |
| std::make_unique<T>   | Exception-safe allocation and initialization of unique_ptr   |
+-----------------------+--------------------------------------------------------------+

Replacing a malloc Array with std::vector

Consider the contrast between manual malloc management and std::vector:

The Dangerous malloc() Way:

void legacyApproach(std::size_t n) {
    int* arr = static_cast<int*>(std::malloc(n * sizeof(int)));
    if (!arr) return;

    // If an exception is thrown here, or if someone writes an early return,
    // memory is LEAKED!
    arr[0] = 10;

    std::free(arr); // Must remember to free!
}

The Idiomatic Modern C++ Way:

#include <vector>
#include <iostream>

void modernApproach(std::size_t n) {
    // Automatically allocates, initializes, and guarantees deallocation!
    std::vector<int> vec(n);

    vec[0] = 10;
    vec.push_back(20); // Can resize dynamically on demand

    // No std::free() needed!
    // When 'vec' goes out of scope, its destructor releases heap memory automatically.
}

Replacing Raw Pointers with std::unique_ptr

If you need a single object dynamically allocated on the heap:

#include <memory>
#include <iostream>

struct Config {
    int timeout = 30;
};

void run() {
    // std::make_unique allocates storage AND constructs the object in one step
    auto cfg = std::make_unique<Config>();
    std::cout << "Timeout: " << cfg->timeout << "\n";

    // Automatically destroyed when cfg exits run()
}

Should You Use malloc() in Modern C++?

ContextRecommendation
Everyday Application Code❌ Do not use malloc(). Prefer std::vector, std::string, and smart pointers.
C Library Interoperability✅ Use malloc() / free(). When a C library returns a buffer that must be freed via free(), or accepts an ownership transfer.
Custom Memory Allocators✅ Use malloc() or platform APIs. When authoring game-engine arena allocators, slab allocators, or memory pools.
Embedded / OS Kernels✅ Permitted. When running in freestanding environments without a full C++ standard runtime.

Practical Examples

Here is a progressive suite of standalone, fully compilable examples demonstrating dynamic memory concepts.

Example A: Allocate Storage for One Integer

#include <iostream>
#include <cstdlib>

int main() {
    int* p = static_cast<int*>(std::malloc(sizeof(int)));
    if (!p) {
        std::cerr << "Allocation failure\n";
        return 1;
    }
    *p = 100;
    std::cout << "Integer value: " << *p << "\n";
    std::free(p);
    p = nullptr;
    return 0;
}

Example B: Allocate and Initialize an Integer Array

#include <iostream>
#include <cstdlib>
#include <limits>

int main() {
    std::size_t count = 5;
    if (count > std::numeric_limits<std::size_t>::max() / sizeof(int)) return 1;

    int* numbers = static_cast<int*>(std::malloc(count * sizeof(int)));
    if (!numbers) return 1;

    for (std::size_t i = 0; i < count; ++i) numbers[i] = static_cast<int>((i + 1) * 5);
    for (std::size_t i = 0; i < count; ++i) std::cout << numbers[i] << " ";
    std::cout << "\n";

    std::free(numbers);
    return 0;
}

Example C: Allocate an Array of Characters (C-String)

#include <iostream>
#include <cstdlib>
#include <cstring>

int main() {
    const char greeting[] = "Hello from C++!";
    std::size_t len = std::strlen(greeting);

    // +1 byte for the null terminator '\0'
    char* str = static_cast<char*>(std::malloc((len + 1) * sizeof(char)));
    if (!str) return 1;

    std::memcpy(str, greeting, len + 1);
    std::cout << "Allocated string: " << str << "\n";

    std::free(str);
    return 0;
}

Example D: Use calloc() for Clean Initialized Memory

#include <iostream>
#include <cstdlib>

int main() {
    std::size_t n = 4;
    int* zeroes = static_cast<int*>(std::calloc(n, sizeof(int)));
    if (!zeroes) return 1;

    for (std::size_t i = 0; i < n; ++i) std::cout << zeroes[i] << " ";
    std::cout << "\n";

    std::free(zeroes);
    return 0;
}

Example E: Resize an Allocation Using realloc()

#include <iostream>
#include <cstdlib>

int main() {
    int* list = static_cast<int*>(std::malloc(2 * sizeof(int)));
    if (!list) return 1;
    list[0] = 1; list[1] = 2;

    // Expand to 4 items using a safe temporary pointer
    int* temp = static_cast<int*>(std::realloc(list, 4 * sizeof(int)));
    if (!temp) {
        std::free(list);
        return 1;
    }
    list = temp;
    list[2] = 3; list[3] = 4;

    for (int i = 0; i < 4; ++i) std::cout << list[i] << " ";
    std::cout << "\n";

    std::free(list);
    return 0;
}

Example F: Comparing malloc() and new

#include <iostream>
#include <cstdlib>

int main() {
    // malloc: uninitialized memory
    int* m = static_cast<int*>(std::malloc(sizeof(int)));
    // new: value-initialized to 0
    int* n = new int();

    std::cout << "new int() value: " << *n << "\n";

    std::free(m);
    delete n;
    return 0;
}

Example G: Class Construction with Placement New

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

struct Tracker {
    int id;
    Tracker(int val) : id(val) { std::cout << "Tracker " << id << " created\n"; }
    ~Tracker() { std::cout << "Tracker " << id << " destroyed\n"; }
};

int main() {
    void* buffer = std::malloc(sizeof(Tracker));
    if (!buffer) return 1;

    Tracker* t = new (buffer) Tracker(77);
    t->~Tracker();
    std::free(buffer);
    return 0;
}

Example H: The Modern Alternative with std::vector

#include <iostream>
#include <vector>

int main() {
    // Dynamic array, automatic memory management, zero leaks
    std::vector<int> dynamic_list = {10, 20, 30};
    dynamic_list.push_back(40);

    for (int x : dynamic_list) std::cout << x << " ";
    std::cout << "\n";
    return 0;
}

Performance Considerations

When designing high-performance systems in C++, dynamic memory allocation patterns have a substantial impact on execution speed:

  1. Allocator Overhead: Every call to malloc() or operator new requires looking up free memory chunks, acquiring thread-safety locks in multi-threaded runtimes, and reserving memory for internal chunk headers. Hundreds of thousands of small individual allocations create significant CPU bottlenecks.
  2. Heap Fragmentation: Repeatedly allocating and freeing blocks of varying sizes over hours or days leaves the heap riddled with tiny, non-contiguous free gaps. Eventually, a large contiguous allocation request may fail even though total free memory appears sufficient.
  3. Cache Locality: Modern CPUs rely on multi-level hardware caches (L1, L2, L3). Contiguous arrays (like std::vector or raw buffers) allow the CPU prefetcher to load adjacent elements into cache lines ahead of time. In contrast, scattered dynamic objects linked via pointers incur frequent cache misses.
  4. Is malloc() Faster than new?: Under typical C++ implementations, operator new simply calls malloc() under the hood before running constructor code. For primitive types like int, the performance is virtually identical. Any real-world performance differences stem from construction/initialization logic, not raw allocator speed.
  5. Memory Pools and Arenas: For performance-critical code (game engines, high-frequency trading), developers pre-allocate a large chunk of memory once with malloc() or new, and then carve out sub-allocations in user space using custom linear or arena allocators without OS overhead.

Frequently Asked Questions (FAQ)

What is malloc() in C++?

malloc() is a standard library function inherited from C (declared in <cstdlib>) that allocates a contiguous block of raw, uninitialized memory of a given size in bytes from the heap and returns a void* pointer to the first byte.

Is malloc() available in C++?

Yes. It is fully supported by the ISO C++ standard under <cstdlib> as std::malloc().

Which header is required for malloc()?

Include #include <cstdlib> in modern C++, or #include <stdlib.h> in legacy code.

What does malloc() return?

It returns a generic void* pointer to the allocated memory on success, or a null pointer (nullptr in C++, NULL in C) on allocation failure.

Does malloc() initialize memory to zero?

No. malloc() leaves allocated memory uninitialized. It contains whatever random bit patterns previously occupied that physical address (“garbage values”).

Does malloc() call constructors?

No. malloc() is entirely unaware of C++ types and object lifecycles; it never invokes class constructors.

Can malloc() allocate an array?

Yes. Pass the total number of bytes: n * sizeof(ElementType).

Can malloc() allocate memory for a class?

It can allocate the raw storage required to hold a class object (sizeof(MyClass)), but it will not initialize or construct the object. You must use placement new or std::construct_at to construct an object within that storage.

What is the difference between malloc() and new?

new is a C++ keyword that allocates memory, invokes constructors, ensures type safety, and throws std::bad_alloc on failure. malloc() allocates uninitialized raw bytes, ignores constructors, requires manual casts from void*, and returns nullptr on failure.

What is the difference between malloc() and calloc()?

malloc() accepts one argument (total bytes) and leaves memory uninitialized. calloc() accepts two arguments (number of elements, size per element) and zeros out all allocated bytes.

What is the difference between malloc() and realloc()?

malloc() creates a new allocation. realloc() resizes an existing allocation, preserving its contents and either expanding in place or copying to a new location.

How do I free memory allocated by malloc()?

Call std::free(ptr); passing the exact base pointer returned by malloc().

What happens if malloc() fails?

It returns a null pointer (nullptr). Dereferencing this pointer causes undefined behavior and typically crashes your program.

What happens if I forget to call free()?

The allocated memory remains reserved until the program terminates. This creates a memory leak, which degrades system performance and can cause the operating system to kill your application.

Can I use delete to release malloc() memory?

No! Mixing malloc() with delete or delete[] causes undefined behavior, corrupts allocator bookkeeping, and leads to runtime crashes.

Can I resize a malloc() allocation?

Yes, using std::realloc().

Is malloc() still used in modern C++?

It is rarely used in standard application code, but remains vital for C library interoperability, custom memory allocators, low-level OS drivers, and embedded systems.

Is malloc() faster than new?

Not in any meaningful sense. For primitive types, new typically forwards its allocation request directly to malloc().

Can I use malloc() with structures?

Yes, provided the struct is a C-compatible POD (Plain Old Data) or trivial type that does not rely on non-trivial constructors or destructors.

Should beginners use malloc() in C++?

No. Beginners should learn std::vector, std::string, and smart pointers first, learning malloc() later to understand low-level systems mechanics.

What happens when malloc(0) is called?

Its behavior is implementation-defined: it returns either nullptr or a non-dereferenceable pointer that must still be freed with std::free().

What is the difference between the stack and heap?

The stack is fast, automatically managed, fixed-size memory used for local variables. The heap is large, dynamically managed memory whose lifetime is controlled manually by the programmer or RAII containers.

How can I detect memory leaks in C++?

Use AddressSanitizer (-fsanitize=address -g) with Clang/GCC, or run your program through Valgrind Memcheck on Linux.


Summary and Key Takeaways

               +-------------------------------------------------+
               |             std::malloc(size_in_bytes)          |
               +-------------------------------------------------+
                                        |
                 +----------------------+----------------------+
                 |                                             |
                 v                                             v
        [ Allocation Fails ]                          [ Allocation Succeeds ]
                 |                                             |
                 v                                             v
          Returns nullptr                               Returns void*
                 |                                             |
                 v                                             v
        Handle Out-of-Memory                           Must static_cast<T*>
                                                               |
                                                               v
                                                      Raw, Uninitialized Bytes
                                                               |
                                                               v
                                                    Must Pair with std::free()
  • Core Functionality: std::malloc(size) requests a contiguous block of raw memory from the heap, measured in bytes.
  • Type Safety: It returns void*, requiring an explicit static_cast<T*>() in C++.
  • Failure Checks: Always verify that the returned pointer is not nullptr.
  • Initialization: malloc() does not initialize memory; it contains indeterminate values until written to.
  • Lifetime vs. Storage: malloc() reserves raw storage. It does not call C++ constructors or destructors.
  • Matching Deallocation: Always pair malloc() with std::free(). Never use delete or delete[].
  • Modern C++: Use RAII containers (std::vector, std::string) and smart pointers (std::unique_ptr, std::shared_ptr) for safe, robust, leak-free code.

Now that you understand low-level memory allocation with malloc(), continue your journey through modern C++ memory management:


Sources & Authoritative References

  1. cppreference.com: std::malloc - Formal declaration, parameter semantics, and alignment guarantees.
  2. cppreference.com: std::free - Deallocation rules and null pointer safety guarantees.
  3. cppreference.com: std::calloc & std::realloc - Contiguous allocation and dynamic reallocation.
  4. cppreference.com: New Expression - Allocation functions, initialization rules, and placement new syntax.
  5. ISO C++ Standard (P0593R6): Implicit creation of objects for low-level object manipulation - C++20 object lifetime rules regarding malloc.

Edit page
Share this post:

Related Articles

Continue exploring related topics and foundational concepts.


Previous Post
Namespaces in C++: Scope Resolution, Name Collisions, and Best Practices
Next Post
Rule of Three, Five and Zero in C++: Resource Management