Smart Pointers in C++

Bernakonduk
5 min readMay 3, 2024

I want to begin my writing with a mention of pointers that exist in C and C++ languages and provide direct memory access.

What is Pointer?

Pointers hold memory addresses of variables. We can access to value of a variable indirectly and change it in this way. We can make the same process without pointers but we have to make processes like copying data that lead to an increase in runtime of the application. We are making the process faster without keeping the CPU busy by directly accessing to memory address with pointers. It is very useful while we develop real-time applications that make time-critical operations. There are two basic types of pointers: raw and smart pointers.

Raw Pointers

A raw pointer is a standard pointer as we know. But it can lead to some problems. If I talk about it briefly, memory allocate and deallocation is made dynamically by using new and delete keywords in raw pointers. If isn’t deallocated to memory allocated, it leads to memory leakage. Memory leakage is allocated memory even no more need. It can cause out-of-memory, low performance, or crashing. We have to make dynamic memory management in raw pointers using new and delete keywords.

Let’s create an example for more understanding. There is a class that includes constructer and deconstructer for observing the pointer lifetime.

#include <iostream>

using namespace std;

class Source
{
public:
Source() { cout << "Source Constructed." << endl; }
~Source() { cout << "Source Deconstructed." << endl; }
};

int main()
{
Source* p = new Source;
delete p;

return 0;
}

Its output will be:

Source Constructed.
Source Deconstructed.

If we didn’t add a delete keyword output would be just “Source Constructed” and it cause memory leakage. Because there isn’t automatic deallocate memory in raw pointers contrary to smart pointers.

Smart Pointers

Smart pointers can be used prevent to memory leakage problems. There are 3 types of smart pointers.

1. unique_ptr

unique_ptr came with C++11 instead of auto_ptr which was removed because of causing problems. It is used management of arrays and objects. It allows only one unique_ptr’s ownership to object at the same time. It helps to dangling pointers (pointer that points to invalid data) and memory leakage problems and also removes to need for dynamic memory management. When the pointer gets out of scope, the pointer that the owner of the object removes (the big difference from raw pointers is this). Ownership of an object can be transferred to another pointer but it can’t be shared or copied. Move semantics must used for content transfer. Also, there is a new function the name of make_unique came with C++14 and it can be used to create unique_ptr without using a new keyword.

If we change the pointer with unique_ptr in the main code, the output will be the same as before even though we haven’t used the delete keyword.

#include <memory> // unique_ptr is under the memory header file

int main()
{
// unique_ptr<Source> ptr1(new Source);
unique_ptr<Source> ptr1 = make_unique<Source>(); //That's an option that advised instead of use new keyword
// ptr2 = ptr1; // Copy assignment is disabled in unique_ptr
unique_ptr<Source> ptr2 = move(ptr1); // Ownership is transferred from ptr1 to ptr2

cout << "Ptr1 is " << (ptr1 ? "not null.\n" : "null.\n");
cout << "Ptr2 is " << (ptr2 ? "not null.\n" : "null.\n");

return 0;
}

Its output:

Source Constructed.
Ptr1 is null.
Ptr2 is not null.
Source Deconstructed.

2. shared_ptr

The big difference from unique_ptr is that shared_ptr allows shared object ownership. The object is destroyed only when deallocate the last pointer that points to yourself. When it is created every pointer that points to an object increases the counter of reference, when the pointer goes out of scope decreases. Only when the counter of reference 0 memory is clear. If you need more than one pointer, you should use a copy of the first pointer. There is a function in shared_ptr with the name of make_shared similar to make_unique in unique_ptr.

int main()
{
//shared_ptr<Source> ptr1(new Source()); // Should be used make_shared instead of the new keyword
shared_ptr<Source> ptr1 = make_shared<Source>();
{
shared_ptr<Source> ptr2 = ptr1;
}
cout << "Out of scope." << endl;

return 0;
}

Its output:

Source Constructed.
Out of scope.
Source Deconstructed.

As in the given example, the shared pointer is only deallocated after the last pointer has gone out of scope not before. But this rule can cause problems as known “cyclical ownership”. Let’s make a few changes to the codes.

class Source
{
public:
shared_ptr<Source> src_ptr{};
Source() { cout << "Source Constructed." << endl; }
~Source() { cout << "Source Deconstructed." << endl; }
};

int main()
{
shared_ptr<Source> ptr = make_shared<Source>();
ptr->src_ptr = ptr;
//ptr->src_ptr = nullptr; // For prevent memory leakage

return 0;
}

Its output will be only “Source Constructed.” and the pointer won’t deallocate. Because ptr and src_ptr shares object and src_ptr isn’t going out of scope even if ptr goes so ptr continues to stay in memory until we give a new value.

3. weak_ptr

It is designed to solve the problem of “cycling ownership”. It can access to an object that owner who shared_ptr but it doesn’t increase the counter of reference. So that we don’t have to wait until all pointers will be out of scope for memory deallocate.

int main()
{
weak_ptr<Source> weak;
{
shared_ptr<Source> shared = make_shared<Source>();
weak = shared;
}

cout << "Out of scope." << endl;

return 0;
}

Its output:

Source Constructed.
Source Deconstructed.
Out of scope.

In this example object was destroyed before out of the second scope, unlike shared_ptr. But this situation can lead to a problem with the name of “dangling pointer” which there is in raw pointers too. Because weak_ptr still might point to the object even if shared_ptr had deleted the object. In this point, weak_ptr has two functions that we can control there is an object that isn’t in raw pointers to prevent this problem. We can check if the object still exists with expired and lock functions. Also, it can be used use_count for check there are how many users of the object before the destroyed object.

int main()
{
weak_ptr<Source> weak;

{
shared_ptr<Source> shared = make_shared<Source>();
weak = shared;

cout << "Number of users: " << weak.use_count() << '\n';

cout << "Object " << (weak.lock() ? "still exist.\n" : "isn't exist.\n");
}

cout << "Object " << (weak.expired() ? "isn't exist.\n" : "still exist.\n");

return 0;
}

Its output:

Source Constructed.
Number of users: 1
Object still exist.
Source Deconstructed.
Object isn't exist.

The object has been destroyed after being out of scope because there is only one user (shared_ptr) of the object. Because weak_ptr is accepted only observer.

References:

https://www.learncpp.com/cpp-tutorial

https://roadmap.sh/cpp

--

--

Bernakonduk
0 Followers

I am a learner who believes that information should be shared with others that's why I am here. This is my learning journey. You can join me.