Understanding Smart Pointers in C++

Abhilekh Gautam
The Zerone
Published in
5 min readJan 7, 2023

In this article we will start with raw pointers, talk about their cons and then learn about smart pointers that resolves the issues with raw pointers.

Lets begin by looking at a snippet that demonstrates the use of raw pointers.

#include <iostream>
class Student{
std::string sid;
std::string name;
};
int main(){
// allocating memory to the heap.
Student *myStudent = new Student();

// do something with student...

// if something is allocated to heap
// you must manually delete it.
// don't forget to delete it
delete myStudent;
return 0;
} // no memory leak

We start by defining our class Student, it has two fields sid and name both of type std::string.

Let us look at some details here,

Student *myStudent = new Student();

The new operator allocates memory in the heap and returns the address to that allocated memory. Here, myStudent is a pointer (raw) stored in stack that points to contents in the Heap as shown in the diagram below.

Demonstration of Dynamic Memory Allocation

Stack vs Heap

Anything stored in stack gets deleted as soon as it goes out of scope, but that’s not the case with heap . We must manually delete them. So if myStudent goes out of scope here and you haven’t called the delete operator on it already you will encounter a memory leak. Memory is a valuable resource, we need to avoid such conditions.

Can we not delete the heap allocated memory as soon as myStudent goes out of scope here? This is kinda how Smart Pointers work.

Let me tell it here, raw pointers are evil, they are not safe at all.

Problems with Raw Pointers

You can face these problems when dealing with raw pointers

  • Memory Leak: Forgetting to free allocated memory after use.
  • Double Free: Freeing already freed memory.
  • Use after Free: Trying to access already freed memory.

While dealing with raw pointers, it’s your job to take care of those parts. And let me remind you that these are very common source of bug in C++ codebase.

Moving away from Raw Pointers

To solve those problems, C++ introduced Smart Pointers. C++ supports 3 different smart pointers:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

All these smart pointers are defined in std namespace under the <memory> header. We will be only talking about unique_ptr and shared_ptr in this article.

What is a Smart Pointer?

Smart pointer is similar to raw pointer with some additional features in it. Additional feature includes automatic-memory-management. C++ was always criticized for its lack of managing memory automatically. With the introduction of smart pointers we no longer need to manage memory manually, Smart pointer will be there for us.

The unique_ptr:

#include <iostream>
#include <memory>
class Student{
std::string sid;
std::string name;
public:
Student(std::string id, std::string sname){
sid = id;
name = sname;
}
void printName(){
std::cout << name << '\n';
}
void printSID(){
std::cout << sid << '\n';
}
};
int main(){
auto myStudent = std::unique_ptr<Student>(new Student("4","Abhilekh Gautam"));
// accessing member function is similar to regular(raw) pointers.
myStudent->printName();
} // no delete required at all

Let us look at the changes from the previous snippets,

  • We included the <memory> header
  • Added a constructor and two helper functions for our class.
  • Created a unique_ptr , myStudent.

Let’s get into the unique_ptr thing now.

Calling a constructor of unique_ptr

We just pass in a raw pointer to the unique_ptr constructor and that’s it from our side, we can use the methods associated with the Student class using the -> operator.

After the smart pointer is initialized, it owns the raw pointer i.e. the smart pointer is now responsible for deleting the memory pointed by the pointer we passed to the constructor. The destructor for smart pointer contains all the code to delete the allocated memory. Since we declared the smart pointer in stack, it’s destructor gets called when it goes out of scope and hence all the clean up is performed.

From C++ 14 onward you can use the following construct to create a new unique_ptr instance.

auto myptr = std::make_unique<Student>(Student("4","Abhilekh Gautam"));

Things to keep in mind about unique_ptr:

We must remember that, unique_ptr prevents copying the raw pointer it contains.

auto myStudent = std::unique_ptr<Student> (new Student("1","Ram"));
auto anotherStudent = myStudent; // compile time error.

You cannot copy a unique_ptr. But the good thing is you can move.

auto myStudent = std::unique_ptr<Student> (new Student("1","Ram"));
auto anotherStudent = std::move(myStudent); // move ownership

Move here means transferring the ownership to anotherStudent, since we can have only one owner myStudent is set to a nullptr.

The shared_ptr:

Having just a single owner for a raw pointer can be troublesome some times, so to overcome that issue we use shared_ptr.

A shared_ptr is also a wrapper around a raw pointer like unique_ptr. But shared_ptr maintains a reference count for the number of owners, and the memory is cleaned up only when the reference count is zero.

Let’s see a code snippet then

#include <iostream>
#include <memory>

class Student{
std::string sid;
std::string name;
public:
Student(std::string id, std::string sname){
sid = id;
name = sname;
}
void printName(){
std::cout << name << '\n';
}
void printSID(){
std::cout << sid << '\n';
}
};
int main(){
auto myStudent = std::shared_ptr<Student>(new Student("4","Abhilekh Gautam"));
// simply increments the reference count
auto myAnotherStudent = myStudent; // absolutely fine
// we can use `use_count()` method to get the reference count
std::cout << "Reference Count: " << myStudent.use_count() << '\n';
{
// increments the reference count again
auto myYetAnotherStudent = myAnotherStudent;

} // myYetAnotherStudent goes out of scope here
// going out of scope means a decrease in reference count.

std::cout << "Reference Count: " << myStudent.use_count() << '\n';
std::cout << "Reference Count: " << myAnotherStudent.use_count() << '\n';
}

We created a shared_ptr by passing a raw pointer to its constructor. After its creation the reference count is set to One, we can then again copy it to another shared_ptr, which increments the reference count again. This means that the initial raw pointer now has two owners, and the heap contents will be cleaned up only when the reference count is zero.

Diagrammatically this can be viewed as,

Demonstration of shared_ptr

And when one of the shared_ptr goes out of scope,

Demonstration of reference count mechanism in shared_ptr

After all of them go out of scope, the reference count becomes zero and the allocated memory is deleted.

From C++ 14 onward you can use the following construct to create a new shared_ptr instance.

auto myptr = std::make_shared<Student>(Student("4","Abhilekh Gautam"));

To summarize this for you:

  • Raw pointers are evil prefer smart pointers over them.
  • std::unique_ptr cannot be copied.
  • std::shared_ptr allows having multiple owners of the same data using reference counting mechanism.

By now, you should have a good understanding of C++ smart pointers and how they can benefit your programming projects.

Happy Coding!

--

--

Abhilekh Gautam
The Zerone

Computer Engineering Student with interest in System Programming