To understand the need for smart pointers, it’s important to first understand the shortcomings of raw pointers.
Raw pointers are used (among other things) to access heap memory that has been allocated using the new
operator and deallocated using the delete
operator.
However, if the memory is not properly deallocated, it can lead to memory leaks. This is where smart pointers come in. The purpose of smart pointers is to manage dynamically allocated memory automatically so that programmers don’t have to worry about memory leaks in large code bases or complex data structure implementations.
To better understand the difficulty of working with raw pointers, let’s take a look at the following example. The code dynamically allocates three integers on the heap (a
, b
, and c
), and then sets c
to the sum of a
and b
.
int* heapSum(){int* a = new int{1};if(a == nullptr){return nullptr;}int* b = new int{2};if(b == nullptr){//Allocation for b failed, free adelete a;return nullptr;}int* c = new int{3};if(c == nullptr){//Allocation for c failed, free a and bdelete a;delete b;return nullptr;}*c = *a + *b;//Free a and b as they are no longer neededdelete a;delete b;return c;}
This example is significant because it shows that even in a simple function like adding three numbers, it can become complex to handle allocation failures properly. For instance, if c
allocation fails, we must free both a
and b
, which were successfully allocated earlier (lines 21 and 22).
Using local variables allocated on the stack results in simpler code that doesn’t risk memory leaks. There’s no need to manually deallocate stack variables as their lifetime is automatically managed by the compiler. See below:
int stackSum()
{
int a = 1;
int b = 2;
int c = a + b;
return c;
}
We will leverage this automatic lifetime management of stack variables together with OOP concepts such as constructors and destructors to implement smart pointers.
The smart pointer paradigm is based on RAII (Resource Acquisition Is Initialization) and uses the above idea. We wrap the heap memory allocation in an object allocated on the stack. The constructor allocates memory, and the destructor deallocates it when the object is no longer needed, ensuring the memory is freed.
#include <iostream>class SmartIntPointer{private:int* rawPointer = nullptr;public:SmartIntPointer(int value){std::cout << "SmartIntPointer constructor, allocating memory" << std::endl;rawPointer = new int{value};}~SmartIntPointer(){std::cout << "SmartIntPointer destructor, freeing memory" << std::endl;delete rawPointer;}int& operator*() const{return *rawPointer;}};int main(){SmartIntPointer sp(5);std::cout << *sp << std::endl;*sp = 6;std::cout << *sp << std::endl;return 0;}
The above code demonstrates how smart pointers work:
*
operator to provide access to the raw pointer. This operator returns a reference so we can read and write to the smart pointer as if it were a raw pointer.The code output shows that the constructor runs when the object is created, allocating memory. We can access the underlying raw pointer using the *
operator, and when the program ends, the destructor runs and frees the memory.
C++ provides built-in smart pointer implementations, such as std::unique_ptr
, std::shared_ptr
, and std::weak_ptr
, which work with any data type, including arrays. The above example provides a simplified version of how smart pointers work, and there are other considerations to be aware of when working with them, which we can see with the built-in smart pointers.
To learn how to write good pointer-intensive code with raw pointers, visit this course for further information and practical exercises.
Free Resources