5.7. Free store pointers

So far, all of our variables have been created on the stack. Another way to say this is our variables have automatic storage duration. Variables exist only as long as the scope in which they were created. Sometimes, we need to create objects with dynamic storage duration, that is, objects whose lifetime is not limited by the scope in which they were created.

One way to do this is to use the operator new to create objects on the free store. The free store is a system-provided memory pool for variables whose lifetime is directly managed by the programmer. Compare this to our experiences so far where variables were automatically managed by the system, not by the programmer.

The new operator takes a type and (optionally) a set of initializers for that type as its arguments. It returns a pointer to an (optionally) initialized object of its type:

struct Point {
     double x = 0;  // member values is a C++11 feature
     double y = 0;
};

int main() {
  int* p1 = new int;     // allocate 1 uninitialized int
  int* p2 = new int[3];  // allocate 3 uninitialized ints
  int* p3 = new int(5);  // allocate 1 int initialized to 5
  int* p4 = new int{5};  // allocate 1 int initialized to 5, C++11 or later
  int* p5 = new int();   // allocate 1 int initialized to 0

  Point* pt1 = new Point;          // allocate a default constructed Point
  Point* pt2 = new Point();        // allocate a default constructed Point
  Point* pt3 = new Point[3];       // allocate 3 default constructed Points
}

Step through example 1 here.

In all of the above cases, since the new operator returns a pointer to an object of its type, the initialization could use auto.

auto pt1 = new Point;

The operator new allocates memory. When finished with the free-store memory, we return it to the pool of available memory using the operator delete:

struct Point {
     double x = 0;  // member values is a C++11 feature
     double y = 0;
};

int main() {
  int* p1 = new int;
  int* p2 = new int[5];

  Point* pt_x = new Point;
  Point* points = new Point[3];

  delete   p1;  // free memory allocated for a single object
  delete[] p2;  // free array memory

  delete   pt_x;  // same syntax is used for user defined types also
  delete[] points;
}

There should always be exactly 1 delete for every pointer returned by new.

Note

There are two forms of delete:

  • delete p frees the memory for a single object allocated using new

  • delete[] p frees the memory for an array of objects allocated using new

Mistakes over which version of delete to use is a common source of error.

Other mistakes related to delete include deleting the same pointer twice, or not deleting the pointer at all.

Deleting the same pointer twice is a problem because it leads to undefined or unpredictable behavior. The problem rarely arises in very small or short programs. However, in larger programs, strange or unpredictable events may happen long after the statements that perform the double delete are executed. Programs that free memory twice have created real-world security vulnerabilities.

Simply choosing to never delete a pointer on the theory that “well, at least my program won’t crash” is not a good idea either. All computers have a finite amount of memory. Depending on how long your program needs to run, never returning unused memory back to the memory pool is referred to as a memory leak. Also, remember that computers are fast. Depending on what your program does, even a short program can run out of memory before accomplishing all of its goals.

5.7.1. STL memory management

When memory is allocated using operator new, eventually it must be recovered using operator delete. When only a few lines of code are requesting memory, this is not a major problem. However, real world programs often request hundreds or thousands of blocks of memory. Keeping track of all this memory and when it should be freed can be labor intensive. Moreover, the consequences of an error are high: program crashes or corrupted data.

Many languages, such as Java, Python, Ruby, and JavaScript take this problem completely out of the hands of programmers. In these languages, memory is never explicitly deleted by the program. Rather it is managed by a garbage collector, which is responsible for cleaning up after the program (removing its garbage).

C++ does not provide a garbage collection mechanism by default. Given that memory management is such a problem, does the STL provide any resources to help solve it?

Yes.

The C++ Standard Template Library provides a family of classes to help solve these problems. They are all contained in the header <memory> and are defined as templates so that they can point to objects of any type.

Smart pointers are classes that behave like raw pointers but also manage objects created with new, so that you don’t have to worry about when and whether to delete them. Smart pointers are declared on the stack and automatically delete the encapsulated object when the smart pointer goes out of scope. The smart pointer is defined in such a way that it can be used syntactically almost exactly like a raw pointer.

One of the earliest so-called ‘smart pointers’ was auto_ptr. Much online documentation and many text books still refer to and recommend auto_ptr. The auto_ptr function was officially deprecated in C++11 and removed in C++17. Generally, where old texts refer to auto_ptr, use unique_ptr instead.

5.7.1.1. Class std::unique_ptr

A std::unique_ptr is a so-called ‘smart pointer’ that owns and manages another object through a pointer and disposes of that object when the unique_ptr goes out of scope. A unique_ptr is a very lightweight wrapper around a pointer. The basic syntax is:

// older C++11 syntax
// clunky and repetitive
std::unique_ptr<int> p1 = std::unique_ptr<int>(new int);

std::unique_ptr<int> p2 = std::make_unique<int>();       // C++14 adds make_unique

In each example, both p1 and p2 are unique pointers that ‘own’ an int*. Our earlier examples can be changed to:

#include <memory>
struct Point {
  double x = 0;
  double y = 0;
};

int main() {
  std::unique_ptr<int> p2 = std::make_unique<int>();
  auto                 p3 = std::make_unique<int>();       // less redundant

  // array examples
  // unique pointers to arrays of 5 elements
  std::unique_ptr<int[]> p4 = std::unique_ptr<int[]>(new int[5]);
  auto                   p5 = std::make_unique<int[]>(5);

  // user define types are no different
  auto pt_x   = std::make_unique<Point>();    // one Point*
  auto points = std::make_unique<Point[]>(3); // array of 3 Point*
}

Once declared, a unique pointer can be manipulated using the same syntax as a raw pointer.

auto p = std::make_unique<Point>();
// modify Point coordinates and print
p->x = 8;
p->y = 13;
std::cout << p->x << ' ' << p->y << '\n';

// this is an error
// std::cout << p.x << ' ' << p.y << '\n';

What makes a unique_ptr unique?

An object stored within a unique pointer uniquely owns its pointer. In other words, an object is ‘owned’ by exactly one unique_ptr. Unlike raw pointers, a unique pointer cannot be copied or assigned to another variable, even another unique pointer.

No two unique pointers can ever contain the same raw pointer value. This solves the ‘double delete’ problem if both go out of scope. The result is that some operations you can perform on raw pointers are not allowed on unique_ptr:

auto x = std::make_unique<Point>();
std::unique_ptr<Point> y = {x};     // error - copy construction not allowed

std::unique_ptr<Point> z;           // new empty (nullptr)
if(!z) {                            // check if z != nullptr
  z = x;                            // error - copy assignment not allowed
}

Although copying unique pointers is not allowed, you can unique_ptr::release() the pointer and assign it to a raw pointer, or transfer ownership to a different unique_ptr.


You have attempted of activities on this page