Smart pointers, as one of the most important features of C++11, were originally recorded in C++11 Syntactic Sugar, but this part is so important that I have recently been relatively free (逃) and decided to summarize it in detail separately.
In C, dynamic memory allocation and deallocation are achieved through malloc
and free
:
1 | void* malloc( size_t size ); |
We can allocate a contiguous block of memory for our use:
1 | int *p1 = malloc(4*sizeof(int)); |
In C++, dynamic memory management is done through another pair of operators: new
allocates space for an object in dynamic memory and returns a pointer to that object, allowing us to optionally initialize the object; delete
takes a pointer to a dynamic object, destroys that object, and frees the associated memory.
The following code demonstrates this:
1 | // Use new to create a string object and assign it to a string pointer |
As seen in the output, the data stored in the location pointed to by testNew before and after the delete operation (on Windows):
The value stored at the memory address pointed to after releasing with delete (Dangling Pointer: a pointer that points to a block of memory that once held a data object but is now invalid) is undefined.
On Linux, compiling and running this would result in Segmentation fault (core dumped).
Therefore, when we use delete to release a dynamic object, it is best to immediately set it to nullptr (null pointer).
As you can see, when using new/delete to manually manage memory, it is essential to remember that new/delete must appear in pairs to avoid creating a dynamic object with new without subsequently deleting it, resulting in memory leaks.
You can also check out my two blog posts discussing issues related to new/delete:
- Memory leaks caused by deleting void* pointers
- Memory leaks caused by STL releasing pointer elements
Using dynamic memory is very prone to issues, as ensuring memory is released at the correct time is not easy. Sometimes we forget to release memory, causing memory leaks; at other times, we release it while still having pointers referencing that memory, leading to illegal memory references (dangling pointers).
Fortunately, to use dynamic memory more safely, C++11 introduced a new feature—smart pointers.
C++11 provides two types of smart pointers (shared_ptr
and unique_ptr
) to manage dynamic objects. Smart pointers behave similarly to conventional pointers (raw pointers), with the most important difference being that smart pointers can automatically release the object they point to.
The two types of smart pointers provided by C++11 differ in how they manage underlying pointers:
shared_ptr allows multiple pointers to point to the same object.
unique_ptr, on the other hand, exclusively owns the pointed-to object.
The standard library also defines a companion class—weak_ptr
, which is a weak reference pointing to the object managed by shared_ptr.
Note: These three types are all defined in memory.
shared_ptr Class
Similar to vector
/ string
/ list
/ deque
that we have come across earlier, smart pointers are also templates. Therefore, when we create a smart pointer, we must provide additional definitions—the type the pointer can point to. Like with vectors, we specify the type inside angle brackets, followed by the name of the defined smart pointer.
1 | // shared_ptr can point to string |
The default initialized smart pointer holds a null pointer (nullptr)
The usage of smart pointers is similar to that of ordinary pointers. Dereferencing a smart pointer returns the object it points to. If a smart pointer is used in a conditional check, it effectively checks whether it is null.
1 | shared_ptr<string> strp=make_shared<string>(); |
Operations supported by both shared_ptr and unique_ptr.
Operation | Meaning |
---|---|
shared_ptr<T> sp;unique_ptr<T> up; | An empty smart pointer that can point to an object of type T |
p | Uses p as a condition check; if p points to an object, this evaluates to true |
*p | Dereference p to obtain the object it points to |
p->mem | Equivalent to (*p).mem |
p.get() | Returns the pointer stored in p; use with caution—if the smart pointer releases its object, the returned pointer will also disappear (becoming a dangling pointer) |
swap(p,q)p.swap(q) | Swaps the pointers in p and q |
Operations unique to shared_ptr
Operation | Meaning |
---|---|
make_shared<T> (args) | Returns a shared_ptr pointing to a dynamically allocated object of type T. The object is initialized using args. |
shared_ptr<T> p(q) | p is a copy of shared_ptr q; this operation increments the counter in q. The pointer in q must be convertible to T* (for more details, refer to A Detailed Analysis of Pointer Conversion in C++) |
p=q | Both p and q are shared_ptr; your pointers must be convertible to each other. This operation decrements p’s reference count and increments q’s; if p’s reference count becomes 0, the original memory it managed will be released. |
p.unique() | Returns true if p.use_count() is 1; otherwise returns false |
p.use_count() | Returns the number of smart pointers that share the object with p; it can be slow and is mainly used for debugging |
We can also implement a simple class similar to shared_ptr
1 | template<typename T> |
The above implements only simple reference counting and provides the smartPointer::count()
member function to get the number of references to the pointed object.
Here’s a simple test code:
1 | struct test{ |
make_shared Function
The safest way to allocate and use dynamic memory is to call a standard library function called make_shared
.
This function allocates an object in dynamic memory, initializes it, and returns a shared_ptr
pointing to this object. Like smart pointers, make_shared
is also defined in the memory
header.
When using make_shared
, you must specify the type of the object to create. The definition is similar to templates, following the function name with angle brackets, in which you provide the type.
1 | // Points to an int initialized with a value of 0 |
Similar to the emplace
member of sequence containers
(which directly calls the constructor of the element type to construct elements in the container, for detailed information refer to C++11 Syntactic Sugar: emplace Operation), make_shared
uses its parameters to construct an object of the given type.
For example:
- The parameters passed when calling
make_shared<string>
must match one of the constructors for string. - The parameters passed when calling
make_shared<int>
must be able to initialize an int, and so on. - If we do not pass any parameters, the object will be value-initialized.
We can also use auto to define an object that holds the result of make_shared
(i.e., the smart pointer object), which is a simpler method compared to the previous ones:
1 | // strp points to a dynamically allocated empty vector<string> |
Copying and Assigning shared_ptr
When a copy or assignment operation is performed, each shared_ptr records how many other shared_ptrs point to the same object.
As mentioned in the previous table, we can call the use_count
member function of shared_ptr
to get the number of smart pointers sharing the object.
1 | // The object pointed to by p has only one reference |
Each shared_ptr
can be thought of as having an associated counter, commonly referred to as the reference count. Whenever we copy a shared_ptr, the counter gets incremented.
For example:
When one shared_ptr is initialized with another shared_ptr.
When a shared_ptr is passed as a parameter to a function.
When it is the return value of a function.
The associated counter will increment.
When we
- Assign a new value to
shared_ptr
- The
shared_ptr
is destroyed (e.g., a localshared_ptr
leaves its scope)
the counter will decrement.
Once a shared_ptr’s counter becomes 0, it will automatically release the object it manages.
1 | struct test{ |
Note: Whether to use a counter or another data structure to record how many pointers share the object is entirely up to the specific implementation of the standard library. The key is that smart pointers can record how many shared_ptrs point to the same object and can release them at the appropriate time.
Automatic Destruction of Objects Managed by shared_ptr
Like all defined local variables, local objects are destroyed when they leave their scope.
1 | void func(void){ |
Smart pointers behave similarly, but when they leave their scope or are assigned a new value, they decrement the object’s internal reference count, and only when the count reaches 0 does the object pointed to by the smart pointer get destroyed.
We can see the order of decrementing the reference count and destroying objects with the following code:
1 | smartPoint<test> funa(void){ |
As seen, we call funa in funb, where a smart pointer is created in funa that points to an instance of the test object, and we return this smart pointer to the invocation site of funb. Upon returning, the reference count of the smart pointer created in funa increments (the return is actually a copy constructor creating a new object as a value pass. If you’re interested, you can try returning from funa without receiving (using) that return value at the invocation site to see when the dynamic object created in funa gets destroyed) and then decrements (when leaving the funa scope), so the object pointed to by the smart pointer will not be destroyed when leaving the funa scope. But subsequently, when leaving the funb scope, the smart pointer is destroyed along with the dynamic memory object it points to (because the reference count decrements to 0 when leaving the funb scope, thus calling the destructor of the object pointed to by the smart pointer to release that object).
When the last shared_ptr
pointing to an object is destroyed, the shared_ptr class will automatically destroy this object. It does this through a special member function—the destructor. Similar to constructors, every class has a destructor. As constructors control initialization, destructors control what actions to take when an object of this type is destroyed.
The destructor of shared_ptr will decrement the reference count of the object it points to. If the reference count drops to 0, the shared_ptr’s destructor will destroy the object and free the memory it occupied.
When dynamic objects are no longer needed, shared_ptr
will automatically release them, making dynamic memory usage very easy, as we don’t always have to remember when we created (new) a dynamic object and have yet to release (delete) it.
The destructor of shared_ptr will decrement the reference count of the object it points to. If the reference count drops to 0, the shared_ptr’s destructor will destroy the object and free the memory it occupied.
If you store a
shared_ptr
in a container and later no longer need all of the elements while only using some, remember to useerase
to delete those elements you no longer need.
Classes Using Dynamic Memory for Resource Generation
Programs use dynamic memory for one of the following reasons:
- The program does not know how many objects it needs.
- The program does not know the exact type of the required objects.
- The program needs to share data among multiple objects.
Container classes
are a typical example of using dynamic memory for the first reason.
In the example below, we will define a class that uses dynamic memory to allow multiple objects to share the same underlying data.
The containers we often use (vector
/ string
/ map
/ set
/ deque
/ list
) allocate resources that have the same lifetime as the object.
For instance, each vector
object contains its own elements. When we copy a vector
, the elements of the original vector and the new copy are separated.
1 | vector<string> v1; |
Elements allocated from a vector only exist while the vector is present. When a vector is destroyed, the elements within that vector also get destroyed.
However, certain classes allocate resources that have a lifetime independent of their original objects. Generally, if two objects share underlying data, we cannot unilaterally destroy the underlying data when one of the objects is destroyed:
1 | Blob<string> b1; |