Memory management is crucial to designing reliable and efficient software. Mismanaging memory can lead to buffer overflows, segmentation faults, and memory leaks. Sometimes, mismanaging memory can be gravely detrimental, leading to exploitable vulnerabilities that malicious actors can leverage as attack surfaces.
Raw Pointers
Pointers are variables that are used for tracking the memory address of some other object such as an array or a structure. The pointer type that most programmers are familiar with is known as the raw pointer which is very prevalent in C and is sometimes used in C++. Below is an example of the typical use of a raw pointer in C.
<stdio.h>int main() { // Declare an integer int x = 5; // Create raw pointer to 'x' int* ptr = &x; // Print the value of 'ptr' which is the // memory address of 'x' printf("%p", ptr); return 0;}
Building and running the above code produces this output:
~ gcc pointers.c -o pointers~ ./pointers0x7ffda8c9049c
The text printed to the terminal is a hexadecimal string that represents the memory address of the data assigned to x. This is a pretty rudimentary use case for raw pointers; let’s look at something more complex.
Using malloc in C, we can manually allocate a slice of memory and capture a pointer to the starting address of that memory block.
<stdio.h> <stdlib.h>int main() { int* ptr = (int*)malloc(sizeof(int)); printf("Allocated memory for an integer\n"); // Memory allocated with 'malloc' must be manually freed // to prevent memory leaks free(ptr); printf("Freed the allocated memory"); // Pointers to deleted memory must be assigned the 'NULL' // value to be properly cleaned up ptr = NULL; return 0;}
In C, the responsibility of managing memory lies with the developer. If a block of memory is manually allocated with malloc(), calloc(), or realloc(), the developer must call free() to deallocate the memory. If the memory is not explicitly deallocated, a memory leak occurs. The same principle applies to memory allocated with the new keyword, except the memory must be deallocated with the delete keyword to prevent a leak.
Smart Pointers
Having to manually deallocate memory causes a lot of issues and headaches for developers. It can be easy to accidentally forget to deallocate a block of memory, especially in a code base where many individual memory blocks are allocated at a time. This is where the concept of smart pointers comes in. The C++11 standard introduced smart pointers to the standard template library (STL) in the <memory> header.1 Let’s investigate the three types of smart pointers that the C++ STL provides and discuss when and how to implement them.
Unique Pointers
A unique pointer is a template class that wraps a raw pointer and owns the memory of the object that is referenced by that pointer. Unique pointers are responsible for freeing the memory that it owns whenever it falls out of scope. Once either the unique pointer falls out of scope or is assigned another raw pointer with either the assignment operator or the reset() method, the unique pointer’s deleter is invoked. The developer is able to specify a custom deleter if desired, but by default, delete is invoked to destroy the object and deallocate the memory.2
<cstdint> <iostream> <memory>struct CustomDeleter { void operator()(uint64_t* i) { std::cout << "Custom deleter called; pointer fell out of scope" << std::endl; delete i; }};int main() { // Standard use of unique ptr auto ptr = std::make_unique<uint64_t>(10); std::cout << "Value at " << ptr.get() << ": " << *ptr << std::endl; // Demonstration for destruction after falling out of scope { using custom_ptr_t = std::unique_ptr<uint64_t, CustomDeleter>; custom_ptr_t ptr_with_deleter(new uint64_t(10)); } std::cout << "Terminating"; return 0;}
The above code demonstrates the standard use of a unique pointer as well as a showcase of how the unique pointer cleans up its owned memory whenever it falls out of scope. A custom deleter functor called CustomDeleter is declared and passed as a template parameter to the unique pointer type. A variable named ptr_with_deleter is then instantiated inside a local scope, and then it immediately falls out of scope. The custom deleter is invoked, and we get the following output when building and running:
~ g++ unique.cpp -o unique~ ./uniqueValue at 0x5fa1229862b0: 10Custom deleter called; pointer fell out of scopeTerminating
Shared Pointers
Shared pointers are similar to unique pointers in that they also wrap a raw pointer and own the memory of that object; however, the difference is that multiple instances of a shared pointer can own a single instance of memory. Another crucial difference is that shared pointers have a reference counter. The reference counter represents how many instances of a shared pointer claim ownership of the same memory location. For memory owned by a shared pointer to be deallocated when the shared pointer falls out of scope, the reference counter must also be equal to 0. For example, if two shared pointers claim ownership of the same memory, and one falls out of scope, the memory is not deallocated because there still exists a shared pointer that claims ownership. Only when that shared pointer falls out of scope (which decrements the reference count to 0) will the memory be deallocated. Let’s look at an example implementation:
<cstdint> <iostream> <memory>int main() { std::shared_ptr<uint64_t> ptr(new uint64_t(10)); std::shared_ptr<uint64_t> ptr2 = ptr; std::cout << "Value at first pointer (" << ptr.get() << "): " << *ptr << std::endl; std::cout << "Value at second pointer (" << ptr2.get() << "): " << *ptr2 << std::endl; std::cout << "Reference count: " << ptr.use_count() << std::endl; { std::shared_ptr<uint64_t> ptr3 = ptr; std::cout << "Reference count: " << ptr.use_count() << std::endl; } std::cout << "Reference count: " << ptr.use_count() << std::endl; return 0;}
The above code demonstrates that multiple instances of a shared pointer can “own” the same memory, and the output proves this. Compiling and running the code produces the following:
~ g++ shared.cpp -o shared~ ./sharedValue at first pointer (0x5816b72602b0): 10Value at second pointer (0x5816b72602b0): 10Reference count: 2Reference count: 3Reference count: 2
Notice that ptr and ptr2 refer to the same memory address. They both maintain ownership of the memory that was originally initialized by ptr. Also notice that the reference count is 2 before ptr3 is initialized in its local scope; after it is initialized, the reference count increases to 3. Once ptr3 falls out of scope, the reference count drops back to 2.
Weak Pointers
The average C++ developer will be very familiar with unique and shared pointers, but the weak pointer may seem foreign and esoteric. In fact, doing a simple code search on GitHub for “std::weak_ptr” shows at the time of writing only 175k results, whereas “std::shared_ptr” and “std::unique_ptr” both yield over 4 million hits. It would seem that weak pointers are not very commonly used; what, then, is their purpose?
Weak pointers are used for creating a variable that holds a “weak” reference to an object that is managed by a shared pointer.3 A “weak” reference means that the variable holding that reference does not have ownership of the object to which it is referring. To use the reference held by a weak pointer, it must first be promoted to a shared pointer through a copy. Let’s look at an example:
<cstdint> <iostream> <memory>std::weak_ptr<uint64_t> weak;void CheckPointer() { std::cout << "Reference count: " << weak.use_count() << std::endl; // Promote to shared pointer if weak pointer is valid if (std::shared_ptr<uint64_t> shared_copy = weak.lock()) { std::cout << "Value of weak pointer is " << *shared_copy << std::endl; } else { std::cout << "weak pointer is expired" << std::endl; }}int main() { { auto shared = std::make_shared<uint64_t>(10); weak = shared; CheckPointer(); } CheckPointer(); return 0;}
In the CheckPointer function, notice the condition within the if statement: a temporary shared pointer is created if the weak pointer is valid. If the new shared pointer refers to valid memory, then the reference can be used. If the new shared pointer is invalid, we consider the weak pointer to have expired, so we cannot use the reference. A weak pointer is invalidated if the shared pointer from which the weak pointer was derived has been deallocated due to falling out of scope with a reference count of 0.
Building and running the above code produces the following output:
~ g++ weak.cpp -o weak~ ./weakReference count: 1Value of weak pointer is 10Reference count: 0weak pointer is expired
Notice that, even with the weak pointer being derived from the shared pointer, the weak pointer indicates a reference count of 1. This is because the weak pointer does not have ownership of the reference it received from the shared pointer as mentioned previously. Once the original shared pointer falls out of scope, the weak pointer is checked again, and this time, it is found to have expired.
So, what happens if we try to dereference a weak pointer without promoting it? Well, this is a compiler error. Let’s try to build the following code:
<cstdint> <memory>int main() { auto shared = std::make_shared<uint64_t>(10); std::weak_ptr<uint64_t> weak = shared; auto val = *weak; // Will not compile return 0;}
~ g++ deref_weak.cpp -o deref_weakderef_weak.cpp: In function ‘int main()’:deref_weak.cpp:9:14: error: no match for ‘operator*’ (operand type is ‘std::weak_ptr<long unsigned int>’) 9 | auto val = *weak; // Will not compile | ^~~~~
It simply is not possible. Weak pointers cannot be dereferenced on their own. They must be promoted to a shared pointer before dereferencing can occur.
This naturally brings about the question: Why use weak pointers at all if they have to be promoted to a shared pointer to be useful? Weak pointers actually solve an interesting problem that can occur with shared pointers. Sometimes, a developer might implement their software design in such a way that two or more shared pointers end up referring to each other, forming a reference cycle. For instance:
struct B;struct A { std::shared_ptr<B> b_ptr;};struct B { std::shared_ptr<A> a_ptr;};
A and B each have a shared pointer referring to one another, meaning that their reference count is now circular. Even when they fall out of scope, the reference count will not hit 0, meaning the memory is not deallocated, leading to a memory leak. This is where the weak pointer comes in.
struct B;struct A { std::shared_ptr<B> b_ptr;};struct B { std::weak_ptr<A> a_ptr;};
The weak pointer does not increase the reference count of A, meaning that once both objects go out of scope, the memory will be safely and correctly deallocated.
The other use case for weak pointers is for checking whether an object exists without extending its lifetime. This is similar to what we implemented in the original weak pointer example. By using the lock() method to promote a weak pointer to a temporary shared pointer, we are able to determine if the object still exists; if it exists, our shared pointer will be valid.
Conclusion
Smart pointers are a major improvement upon raw pointers, implementing a quasi garbage collection system in C++. Where possible, smart pointers should be preferred over raw pointers. Understanding when, where, and how to use smart pointers is crucial to being a C++ developer so that you can produce efficient code with heightened memory safety.
The code referenced in this post is available on my GitHub, and instructions for building and running are provided. If you enjoy these types of posts and want to stay in the loop for future articles, be sure to subscribe!
References
- ISO 14882:2011 20.7.1 ↩︎
- std::unique_ptr – cppreference.com. (2025). Cppreference.com. https://en.cppreference.com/w/cpp/memory/unique_ptr.html ↩︎
- std::weak_ptr – cppreference.com. (2025). Cppreference.com. https://en.cppreference.com/w/cpp/memory/weak_ptr.html ↩︎