6.3 Smart Pointers

Smart Pointers
Section titled “Smart Pointers”Smart pointers were introduced in C++11 (with further enhancements in C++14) and are supported by all modern C++ compilers. They provide a structured and safer way to manage memory, addressing the risks associated with manual memory management using raw pointers. This is one of the main reasons for their widespread adoption. Smart pointers are effectively template classes that overload operators to behave like regular pointers, while automatically managing the ownership and lifetime of the allocated memory.
For example, consider the following function call f() that returns a pointer:
T * f(); // a function that returns a raw pointer.This form is poorly defined in terms of ownership: it does not indicate whether this pointer is the sole owner of the object (in which case you are responsible for calling delete), or whether it is one of multiple shared references (in which case manually deleting it could lead to undefined behaviour and serious bugs).
Smart pointers make ownership semantics explicit. They are declared as follows:
std::unique_ptr<T> f(); // Exclusive ownership; cannot be copiedstd::shared_ptr<T> f(); // Shared ownership; multiple shared pointers can existTo use smart pointers you need to include the memory header file, i.e.,
#include <memory>We examine these pointers and another “weak” pointer in the following sections using examples.
The Unique Pointer
Section titled “The Unique Pointer”The unique pointer is a smart pointer that enforces exclusive ownership — it cannot be copied, so only one instance of the pointer can exist at any time. Ownership can, however, be transferred (moved) when necessary.
The following example has a demonstration class Student. Please focus on the main() function and the test() functions below and review the inline comments. See uniquePointerExample.cpp:
// Note that this test function takes a reference to a smart pointer. You// cannot pass by value, as this would try to create a copy of the unique pointer.
void test(unique_ptr<Student> & ptr){ cout << "Function received smart pointer to object " << ptr->getName() << endl;}
int main() { // C++11 allows creation of unique pointer unique_ptr<Student> p(new Student("Joe", 1234)); test(p);
// C++14 introduced the make_unique template function // which also calls the constructor of Student. // here I am using the auto type to set the type automatically for q auto q = make_unique<Student>("Jack",1235); test(q);
// To destroy the object and replace the object you use reset // The object "Joe" will be destroyed at this point p.reset(new Student("Derek", 1236));
// We cannot copy a unique pointer, but we can move // auto r = p; // copy is not permitted for unique pointers cout << "Moving p to a new pointer r" << endl; auto r = move(p); // At this point p is now null -- i.e., not pointing at an object // The pointer r is now pointing at Derek // To destroy the object at q cout << "Destroying the object at pointer q" << endl; q.reset();
// You can use release() to release the object from managed memory // but the destructor is not called -- e.g., q.release(); cout << "End of main() " << endl;}This example program will give the following output:
Constructor called on JoeFunction received smart pointer to object JoeConstructor called on JackFunction received smart pointer to object JackConstructor called on DerekDestructor called on JoeMoving q to a new pointer rDestroying the object at pointer qDestructor called on JackEnd of main()Destructor called on DerekYou will notice that the test function is called on p and q, which are created in different ways. You will also see that the line of code r = q; is not permitted and will cause a compiler error as unique pointers may not be copied. Notice also that the destructor is automatically called on the “Derek” object smart pointer when it goes out of scope.
The Shared Pointer
Section titled “The Shared Pointer”The shared pointer behaves similarly to the unique_ptr, with one key difference: copies are allowed. Multiple shared_ptr instances can share ownership of the same object. The underlying object is automatically deallocated when the last shared_ptr that owns it is destroyed.
In effect, shared_ptr provides basic reference-counted garbage collection. This makes memory management easier and safer in situations where multiple parts of a program need access to the same object. For this reason, shared_ptr is often the most commonly used smart pointer in modern C++. See sharedPointerExample.cpp
#include <iostream>#include <memory> // required for smart pointers#include <string>using namespace std;
class Student {private: string name; int id;public: Student(string name, int id): name(name), id(id) { cout << "Constructor called on " << name << endl; } virtual void display(); string getName() { return name; } int getID() { return id; } ~Student() { cout << "Destructor called on " << name << endl; }};
void Student::display() { cout << "A student with name " << name << " and id " << id << endl;}
void test(shared_ptr<Student> & ptr) { cout << "[ Function received smart pointer to object " << ptr->getName() << endl; // Display the reference count for the object pointed to by ptr cout << "[ There is(are) " << ptr.use_count() << " reference(s) to the object" << endl;}
int main() { // C++11 on allows you to create a shared pointer to an object of the class Student shared_ptr<Student> p(new Student("Joe", 1234)); test(p);
// C++11 introduced the make_shared template function // which also calls the constructor of Student. // here I am using the auto type to set the type automatically for q auto q = make_shared<Student>("Jack",1235); test(q);
// To destroy the object and replace the object you use reset // The object "Joe" will be destroyed at this point p.reset(new Student("Derek", 1236));
// We can copy a shared pointer cout << "Creating a copy of p to a new pointer r" << endl; auto r = p; // copy is permitted for shared pointers test(r);
// To destroy the object at q cout << "Destroying the object at pointer q" << endl; q.reset(); // You can use release() to release the object from managed memory // but the destructor is not called -- e.g., q.release(); cout << "End of main() " << endl;}The output of this code is as follows:
Constructor called on Joe[ Function received smart pointer to object Joe[ There are 1 references to the objectConstructor called on Jack[ Function received smart pointer to object Jack[ There are 1 references to the objectConstructor called on DerekDestructor called on JoeCreating a copy of p to a new pointer r[ Function received smart pointer to object Derek[ There are 2 references to the objectDestroying the object at pointer qDestructor called on JackEnd of main()Destructor called on DerekNotice that the reference count was 2 the when the smart pointer r was passed to the function.
Also notice that the objects were destroyed correctly. There are exactly three constructors being called and exactly three destructors being called automatically, even though at one stage there were two pointers to the same object.
The Weak Pointer
Section titled “The Weak Pointer”The weak pointer is a special type of smart pointer that works alongside shared_ptr but does not contribute to the reference count. This is useful when you need a reference to an object without affecting its lifetime management.
A weak_ptr is always created from an existing shared_ptr. The following is an example that uses the class from the shared pointer example. See weakPointerExample.cpp:
int main() { shared_ptr<Student> p(new Student("Joe", 1234));
// Make a copy of p auto q = p; cout << "The reference count is now " << p.use_count() << endl;
// Make a weak pointer r auto r = weak_ptr<Student>(p); // Note there is no increase in the reference count cout << "The reference count is now " << p.use_count() << endl;
// To use the weak pointer you must obtain a 'lock' and extract the shared // pointer. This has the effect of creating another reference! auto temp = r.lock(); test(temp);
cout << "End of main() " << endl;}This code gives the following output:
Constructor called on JoeThe reference count is now 2The reference count is now 2[ Function received smart pointer to object Joe[ There is(are) 3 reference(s) to the objectEnd of main()Destructor called on JoeWhy would you use weak_ptr? weak_ptr is** typically used to prevent circular references** that can lead to memory leaks when using shared_ptr.
For example, imagine you have two classes: Student and Module. The Student class may hold pointers to Module objects representing the modules a student is enrolled in, while the Module class may hold pointers to the Student objects registered for that module. If both sides use shared_ptr, the reference counts on both objects would never reach zero, preventing the memory from ever being released — even after both objects are no longer needed.
To solve this, you can use weak_ptr for one side of the relationship — for example, have Module store weak_ptr references to its Student objects. This breaks the ownership cycle, allowing memory to be freed correctly when no longer in use.
General advice on choosing smart pointers: \
- Use
shared_ptrby default for shared ownership where multiple parts of your code need access to the same object. - Use
unique_ptrwhen you require strict one-to-one ownership of an object. - Use
weak_ptronly when you need to reference ashared_ptr-managed object without affecting its lifetime, typically to break circular references.
Custom Deleters
Section titled “Custom Deleters”Custom deleters allow you to control how the object managed by a smart pointer is destroyed. This can be very useful when objects require special clean-up operations, or when managing resources other than memory (e.g., file handles, sockets, or external resources).
In the following example, we define a custom deleter function that takes a pointer to a Student object. Inside the deleter, we have full access to the object’s methods before it is destroyed (See weakDeleter.cpp):
#include <iostream>#include <memory> // required for smart pointers#include <string>using namespace std;
class Student {private: string name; int id;public: Student(string name, int id): name(name), id(id) {} string getName() const { return name; } int getID() const { return id; } virtual ~Student() { cout << "Student destructor called " << endl; }};
void deleter(const Student *s) { cout << "[ Custom deleter for the Student class" << endl; cout << "[ Doing something for " << s->getName() << " object " << endl; cout << "[ For example calc grades, print transcript " << endl; delete s; cout << "[ The destructor should have been called by now " << endl;}
int main() { shared_ptr<Student> p(new Student("Joe", 1234), deleter); p.reset(); cout << "End of main() " << endl;}Important Notes on Custom Deleters \
- The custom deleter must be specified when the smart pointer is created.
- You cannot use
std::make_sharedorstd::make_uniquewhen a custom deleter is required, because these functions do not allow you to specify a deleter. - Instead, you must construct the smart pointer directly, passing both the raw pointer and the custom deleter to the constructor.
[ Custom deleter for the Student class[ Doing something for Joe object[ For example calc grades, print transcriptStudent destructor called[ The destructor should have been called by nowEnd of main()Important Reminder on Custom Deleters: When using a custom deleter, you are fully responsible for correctly releasing the object’s memory. For example:
void studentDeleter(Student* s) { if (s) { s->close(); // Custom cleanup delete s; // Deallocate memory }}The call to delete s; explicitly destroys the object and releases its allocated memory. If you omit this line, the Student destructor will not be called, resulting in a memory leak and any clean-up code inside the destructor will not execute.
Unlike the default deleter used by smart pointers (which automatically calls delete), a custom deleter completely replaces that behaviour. You must therefore ensure that the object is properly destroyed within the custom deleter function.
In summary, a destructor is a class’s built-in cleanup function that runs automatically when an object’s lifetime ends, releasing its internal resources. A deleter, by contrast, is an external callable (such as a function or lambda) used mainly with smart pointers to specify how an object should be destroyed and its memory deallocated. In essence, the destructor cleans up within the object, while the deleter controls how and when that destruction happens.
© 2026 Derek Molloy, Dublin City University. All rights reserved.