Skip to content
Home
C++ Memory Management: Pointers, Smart Pointers, and RAII

C++ Memory Management: Pointers, Smart Pointers, and RAII

C/C++ C/C++ 8 min read 1539 words Beginner ExcellentWiki Editorial Team

Manual memory management is one of C++’s defining features and its greatest source of bugs. Unlike garbage-collected languages, C++ gives you full control over when objects are created and destroyed. This power comes with responsibility — every new must be paired with a delete, every resource acquired must be released. Modern C++ makes this safer through RAII and smart pointers, but understanding what happens under the hood is essential for writing correct, efficient code.

Stack vs Heap

Stack Allocation

Local variables live on the stack. They are automatically destroyed when they go out of scope.

void foo() {
    int x = 42;           // Stack-allocated
    std::string s = "hello"; // s itself on stack, data on heap
--- // x and s destroyed here

Stack allocation is fast — just a pointer bump. The compiler knows the lifetime of every stack variable at compile time. The stack has limited size (typically 1–8 MB), so large objects or deep recursion can cause stack overflow.

Heap Allocation

Heap allocation is managed explicitly with new/delete or through smart pointers.

void foo() {
    int* p = new int(42);    // Allocate on heap
    // use p
    delete p;                // Must free manually
---

Heap allocation is slower than stack because it requires searching for a free block in the heap. Heap memory persists until explicitly freed, which gives you control over object lifetimes but opens the door to leaks, dangling pointers, and double frees.

Raw Pointers

Raw pointers hold the address of an object. They do not own the object — they simply point to it.

int x = 10;
int* ptr = &x;      // Points to x on the stack
*ptr = 20;          // Modifies x

int* heap_ptr = new int(30);
delete heap_ptr;     // Must delete
heap_ptr = nullptr;  // Good practice: null after delete

Common Pointer Pitfalls

Dangling pointer — accessing memory that has been freed:

int* p = new int(42);
delete p;
*p = 10;  // Undefined behavior — dangling pointer

Double delete — freeing already freed memory:

int* p = new int(42);
delete p;
delete p;  // Undefined behavior

Memory leak — never freeing allocated memory:

void leak() {
    int* p = new int(42);
    // No delete — memory lost until program exits
---

RAII (Resource Acquisition Is Initialization)

RAII is the most important idiom in C++. The idea is simple: acquire a resource in a constructor and release it in the destructor. The destructor runs automatically when the object goes out of scope, guaranteeing cleanup even if an exception is thrown.

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t n) : size(n), data(new int[n]) {}
    ~Buffer() { delete[] data; }

    // Copy constructor and assignment operator (Rule of Three)
    Buffer(const Buffer& other)
        : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }
---;

void use_buffer() {
    Buffer buf(1000);   // Acquires resource
    // use buf ...
---  // Automatically freed — no leak

Without RAII, every new requires a corresponding delete on every code path, including error paths. With RAII, cleanup is automatic and exception-safe.

The Rule of Three / Five / Zero

If a class manages a resource (has a destructor), it probably needs:

  • Destructor — release the resource
  • Copy constructor — create a new copy of the resource
  • Copy assignment operator — copy the resource
  • Move constructor (C++11) — transfer ownership
  • Move assignment operator (C++11) — transfer ownership

Smart Pointers

Smart pointers wrap raw pointers and use RAII to ensure automatic cleanup. They are defined in <memory>.

std::unique_ptr

Exclusive ownership — only one unique_ptr can point to a given resource. Cannot be copied, only moved.

#include <memory>

std::unique_ptr<int> p = std::make_unique<int>(42);
*p = 10;

// Cannot copy — compile error
// auto q = p;  // Error

// Must move to transfer ownership
std::unique_ptr<int> q = std::move(p);
// Now p is nullptr

// Custom deleter
auto deleter = [](FILE* f) { fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file(fopen("test.txt", "r"), deleter);

std::shared_ptr

Shared ownership — multiple shared_ptr instances can point to the same object. Uses reference counting. The object is destroyed when the last shared_ptr is destroyed.

std::shared_ptr<int> p1 = std::make_shared<int>(42);
{
    std::shared_ptr<int> p2 = p1;  // Reference count: 2
    // both p1 and p2 point to the same int
---  // p2 destroyed, ref count: 1
// p1 destroyed, ref count: 0, object freed

make_shared allocates the object and control block in a single allocation, which is more efficient than shared_ptr<int>(new int(42)).

std::weak_ptr

A non-owning observer that works with shared_ptr. Does not affect the reference count. Used to break circular references.

class Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Weak to avoid circular reference
---;

std::shared_ptr<Node> a = std::make_shared<Node>();
std::shared_ptr<Node> b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // No cycle because prev is weak

// To access a weak_ptr, lock() it
if (auto ptr = b->prev.lock()) {
    // ptr is a shared_ptr, guaranteed valid
---

Avoiding Memory Leaks

Use make_unique and make_shared

Always prefer std::make_unique<T>(args) and std::make_shared<T>(args) over new. They are safer (exception-safe) and more efficient.

// Bad
std::unique_ptr<int> p(new int(42));

// Good
auto p = std::make_unique<int>(42);

Never Use Raw new/delete in Application Code

Wrap resource management in RAII classes. If you find yourself writing delete, you are probably doing something wrong. Use smart pointers or standard containers instead.

Use Containers for Dynamic Arrays

// Bad
int* arr = new int[100];
delete[] arr;

// Good
std::vector<int> arr(100);

Detecting and Debugging Leaks

AddressSanitizer (ASan)

Compile with -fsanitize=address to detect memory errors at runtime:

g++ -fsanitize=address -g main.cpp && ./a.out

Valgrind

valgrind --leak-check=full ./myprogram

Static Analysis

Tools like Clang-Tidy and Cppcheck can detect many memory issues at compile time.

Conclusion

C++ memory management is powerful but demands discipline. Use stack allocation by default, RAII for resource management, and smart pointers for heap allocation. Avoid raw new/delete in application code. Follow the Rule of Zero when possible — let standard library types manage resources for you.

Related: See our C++ STL guide and C++ move semantics.

Smart Pointer Ownership Models

C++11 introduced three smart pointer types that encode ownership semantics directly in the type system, eliminating the need for manual new/delete in well-written modern C++.

std::unique_ptr — Exclusive Ownership

std::unique_ptr represents exclusive ownership of a dynamically allocated object. It cannot be copied — only moved. The owned object is destroyed when the unique_ptr goes out of scope:

std::unique_ptr<Database> db = std::make_unique<Database>("prod.db");
// db cannot be copied — ownership is unique
auto other = std::move(db);  // Transfer ownership
// db is now nullptr

Use std::make_unique instead of new — it is exception-safe and avoids a potential memory leak if an exception occurs between allocation and smart pointer construction.

std::shared_ptr — Shared Ownership

std::shared_ptr uses reference counting for shared ownership. The object is destroyed when the last shared_ptr pointing to it is destroyed or reset:

std::shared_ptr<Config> config = std::make_shared<Config>();
auto c2 = config;  // Reference count becomes 2
config.reset();    // Count becomes 1
// Object destroyed when c2 goes out of scope

Use std::make_shared for efficiency — it allocates the control block and the managed object in a single allocation. Be aware of circular references: if two objects hold shared_ptr to each other, neither will ever be freed. Use std::weak_ptr to break cycles.

std::weak_ptr — Non-owning Observation

std::weak_ptr holds a non-owning reference to an object managed by shared_ptr. It must be converted to a shared_ptr via lock() before use:

std::weak_ptr<Cache> weak_cache = cache;
if (auto shared = weak_cache.lock()) {
    shared->invalidate();  // Object still alive
--- else {
    // Object was already destroyed
---

This pattern is essential for caching systems, observer patterns, and breaking reference cycles in shared ownership graphs.

Custom Deleters

Smart pointers support custom deleters for non-standard cleanup:

auto file_deleter = [](FILE *f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(file_deleter)> file(fopen("data.txt", "r"), file_deleter);

This brings RAII to any resource that requires cleanup — file handles, database connections, socket descriptors, and operating system handles.

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.

Section: C/C++ 1539 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top