How to Understand Smart Pointers in C++

Mohd Mohtashim Nawaz Feb 02, 2024
  1. Implement a Smart Pointer in C++
  2. Use Smart Pointer Destructor to Prevent Memory Leaks in C++
  3. Types of Smart Pointers in C++
  4. When to Use Smart Pointers in C++
How to Understand Smart Pointers in C++

This article will discuss the smart pointers in C++, how they prevent memory leaks, the types of smart pointers, and the cases when we should use them.

Implement a Smart Pointer in C++

We can define the smart pointer in C++ as a class template that we can use to maintain the original raw pointer. Our smart pointer class contains a pointer variable to hold our raw pointer, destructor, and operator overriding methods.

However, we are not restricted to including only these fields and methods. We can add our methods as we need.

Instead of directly handling the raw pointer, we use this class to handle the pointer. And instead of defining a custom class for smart pointers in C++, we can also use the standard C++ library.

Let us define a custom class for C++ smart pointers implementation.

#include <iostream>

using namespace std;

template <class T>
class CustomSmartPointer {
  T *data;

 public:
  explicit CustomSmartPointer(T *ptr = NULL)  // Constructor to assign pointer
  {
    data = ptr;
  }
  ~CustomSmartPointer()  // Destructor that deallocated memory
  {
    delete data;
  }
  // We overload * and -> operators
  T *operator->() { return data; }
  T &operator*() { return *data; }
};

int main() {
  CustomSmartPointer<int> myPtr(new int());
  *myPtr = 100;
  cout << *myPtr << endl;
  // After executing above statement, memory allocated to myPtr is deallocated.
  return 0;
}

Output:

stark@stark:~/eclipse-workspace/smart_pointer$ g++ custom_smart_prt.cc
stark@stark:~/eclipse-workspace/smart_pointer$ ./a.out
100

Use Smart Pointer Destructor to Prevent Memory Leaks in C++

The presence of a destructor makes our smart pointer class special and distinguishes it from raw pointers. We use the C++ smart pointer destructor to deallocate the memory assigned to our pointer.

A destructor is automatically called and deallocates the memory assigned when a class object goes out of scope. If we have automated the memory deallocation process, we need not worry about resource leaks caused by the pointers we might forget to deallocate.

The smart pointer destructor acts like an automatic garbage collector in C++, similar to Java and C#.

Let us see an example of a resource leak caused by raw pointers.

#include <iostream>

using namespace std;

class student {
 private:
  string name;
  int marks;

 public:
  void setMarks(int m) { marks = m; }
  void setName(string n) { name = n; }
  int getMarks() { return marks; }
  string getName() { return name; }
};

int main() {
  while (true) {
    student* p = new student;
    *p->name = "Stark";
    *p->marks = 100;
  }
  return 0;
}

As we can see, we are initializing the pointer of student type in an infinite loop. We have not deallocated the pointer memory, so this would keep allocating the resources until all memory is occupied.

We should not execute this program for long. Otherwise, our system might hang due to memory overflow.

However, if we used the smart pointer, the memory would have been automatically deallocated after each loop. Thus at a time, only one pointer memory would have been occupied.

Types of Smart Pointers in C++

We have three different types of smart pointers in C++. These are the types implemented in the C++ library for smart pointers.

unique_ptr

This smart pointer type lets us assign only a single user for the underlying raw pointer. That means we can not assign the same underlying pointer to two or more objects.

We should use this type of pointer by default until we can share the memory.

shared_ptr

As the name suggests, we can assign multiple owners to a raw pointer using this smart pointer. This uses a reference counter to keep track of the number of assignees.

weak_ptr

This smart pointer type is very similar to the shared_ptr, except it does not participate in the reference counter. It does not mean that it does not have a reference counter, but its reference is not counted.

Instead, we use its reference counter to count other shared references. We can use this pointer when we do not need tight control on the ownership.

It provides access to one or more shared_ptr objects. However, it has an advantage as it relieves the possibility of deadlock.

Let us see a C++ smart pointer implementation.

#include <iostream>
#include <memory>

using namespace std;

class student {
 private:
  string name;
  int marks;

 public:
  void setMarks(int m) { marks = m; }
  void setName(string n) { name = n; }
  int getMarks() { return marks; }
  string getName() { return name; }
};

int main() {
  unique_ptr<student> ptr1(new student);
  ptr1->setName("Stark");
  ptr1->setMarks(100);
  cout << "unique_ptr output >>\n";
  cout << ptr1->getName() << " : " << ptr1->getMarks() << "\n";

  cout << "shared_ptr output >> \n";
  shared_ptr<student> ptr2(new student);
  ptr2->setName("Tony");
  ptr2->setMarks(99);
  cout << ptr2->getName() << " : " << ptr2->getMarks() << "\n";

  shared_ptr<student> ptr22;
  ptr22 = ptr2;
  cout << ptr22->getName() << " : " << ptr22->getMarks() << "\n";
  cout << "Reference count of shared_ptr: " << ptr2.use_count() << endl;

  auto ptr = make_shared<student>();
  ptr->setName("Jarvis");
  ptr->setMarks(98);

  cout << "Weak pointer output >>" << endl;
  weak_ptr<student> ptr3;
  ptr3 = ptr;
  cout << "Reference count of weak_ptr: " << ptr3.use_count() << endl;
  shared_ptr<student> ref = ptr3.lock();
  cout << ref->getName() << " : " << ref->getMarks() << "\n";

  return 0;
}

Output:

stark@stark:~/eclipse-workspace/smart_pointer$ g++ types_smart.cc
stark@stark:~/eclipse-workspace/smart_pointer$ ./a.out
unique_ptr output >>
Stark : 100
shared_ptr output >>
Tony : 99
Tony : 99
Reference count of shared_ptr: 2
Weak pointer output >>
Reference count of weak_ptr: 1
Jarvis : 98

We should note that the reference count of the weak pointer is 1 because it does not participate in the reference count.

When to Use Smart Pointers in C++

We should not use smart pointers for everything. Since we use a class template for smart pointers, they generally have degraded performance and add to the program’s complexity.

We should use raw pointers for performance-critical applications and small programs as a best practice. However, we should always deallocate the pointer resources after their use is over.

If our code is large and it is hard to deallocate memory for each pointer manually, we should use the smart pointers. We should prefer using smart pointers if the application is not performance-critical.

If we need to handle exceptions, reference counts, deallocation time executions, we should use the smart pointers.

Related Article - C++ Pointer