Константа нулевого указателя nullptr

До введения стандарта C++11 для обозначения неинициализированного указателя использовалась числовая константа 0 или макрос NULL, имевший значение ((void*)0). Такой подход вызывает проблемы, связанные с перегрузкой функции, так как числовая константа 0 может интерпретироваться человеком и как число, и как неинициализированный указатель. Однако, при использовании механизма перегрузки функций компилятор выберет функцию с целочисленным параметром.

// Пример 8.23 - проблема при использовании нуля в качестве

// неинициализированного указателя

void f1(int i) { std::cout << "void f1(int i)" << std::endl; }

void f1(int* i) { std::cout << "void f1(int* i)" << std::endl; }

int main() {

f1(0);

}

Результат работы программы следующий:

void f1(int i)

Аналогичный код в С++11 не приведет к проблемам, так как для обозначения нулевого указателя будет использоваться константа nullptr.

// Пример 8.24 - перегрузка функций и nullptr.

void f1(int i) { std::cout << "void f1(int i)" << std::endl; }

void f1(int* i) { std::cout << "void f1(int* i)" << std::endl; }

int main() {

f1(0);

f1(nullptr);

}

Результат работы программы следующий:

void f1(int i)

void f1(int* i)

Константа nullptr имеет тип std::nullptr_t и может быть неявно преобразована к нулевому указателю или к значению false логического типа. Неявное преобразование nullptr к целочисленному типу приведет к ошибке.

// Пример 8.25 - неявное приведение nullptr

int *pi = nullptr;

int *pi2 = 0;

if(pi == pi2)

std::cout << "nullptr == 0" <<std::endl;

bool b = nullptr;

if(!b)

std::cout << "b == false" << std::endl;

Результат работы программы следующий:

nullptr == 0

b == false

8.7 "Умные" указатели

Основной проблемой при работе с обычными указателями являются утечки памяти, которых сложно избежать в больших проектах. Утечки памяти могут возникать по разным причинам. Например, программист может просто забыть вызвать оператор delete. Во-вторых, утечка может произойти при возникновении исключения. Хотя в процессе раскрутки стека уничтожится локальная переменная-указатель, но динамическая память останется не освобождённой. В третьих, в больших программах может иметься несколько указателей на один и тот же ресурс. При этом бывает достаточно сложно понять, в какой момент времени на ресурс больше никто не указывает, и его пора освободить.

"Умные" указатели (smart pointers) предназначены для устранения данных проблем. Умный указатель представляет собой объект, с которым можно работать как с обычным указателем. При этом он обеспечивает автоматическое освобождение памяти и может также предоставлять некоторую дополнительную функциональность.

В C++ имеется несколько видов умных указателей: std::auto_ptr, std::unique_ptr, std::shared_ptr, std::weak_ptr. Все они объявлены в заголовочном файле memory.

Указатель std::auto_ptr появился в языке C++ довольно давно. Он имеет некоторые известные проблемы, и в настоящее время объявлен устаревшим. Вместо него рекомендуется использовать std::unique_ptr.

Указатель std::unique_ptr, появившийся в C++11, реализует концепцию единоличного владения объектом: на один объект указывает только один экземпляр unique_ptr. При уничтожении экземпляра unique_ptr в его деструкторе происходит и уничтожение управляемого объекта.

В отличие от auto_ptr, указатель unique_ptr не может быть скопирован или изменён через операцию присвоения. Например, следующий код вызовет ошибку компиляции:

std::unique_ptr<int> p(new int(1));

std::unique_ptr<int> q;

q = p; // ошибка компиляции

Заметим, что если бы вместо unique_ptr использовался auto_ptr, то этот код бы скомпилировался. Однако, после присваивания указатель p стал бы недействителен, и попытка обращения к нему вызвала бы аварийное завершение программы.

Если мы для каких-то целей хотим передать владение объектом другому указателю, то следует использовать функцию std::move:

q = std::move(p);

Ещё одним отличием unique_ptr от auto_ptr является способность работать с массивами. Однако, польза от этого довольно сомнительна − в большинстве случаев проще воспользоваться классом std::vector.

Некоторые полезные методы класса std::unique_ptr:

• get − возвращает сохранённый указатель на объект

• release − возвращает сохранённый указатель на объект и заменяет его на nullptr (заметим, что объект при этом не уничтожается)

• reset − уничтожает текущий объект и вступает во владение другим объектом

Наиболее востребованным типом умного указателя является указатель std::shared_ptr. Он реализует концепцию множественного владения, когда на один и тем же объект могут ссылаться несколько указателей. Для этого в shared_ptr реализован подсчёт ссылок на объект. Объект уничтожается тогда, когда счётчик ссылок на него станет равным нулю.

Следующий пример кода демонстрирует использование shared_ptr. Для создания объектов в нём используется специальная шаблонная функция std::make_shared. Чтобы видеть, в каком порядке происходит создание и уничтожение объектов, создана структура c именем Demo, в конструкторе и деструкторе которой на консоль выводятся диагностические сообщения.

// Пример 8.26 - использование указателей shared_ptr

#include <iostream>

#include <memory>

struct Demo {

int x;

Demo(int x) : x(x) {

std::cout << "In constructor, x = " << x << "\n";

}

~Demo() {

std::cout << "In destructor, x = " << x << "\n";

}

};

int main() {

std::shared_ptr<Demo> p = std::make_shared<Demo>(1);

std::shared_ptr<Demo> q = std::make_shared<Demo>(2);

q = p;

p.reset();

q = std::make_shared<Demo>(3);

}

Результат работы этой программы:

In constructor, x = 1

In constructor, x = 2

In destructor, x = 2

In constructor, x = 3

In destructor, x = 1

In destructor, x = 3

Указатель std::weak_ptr содержит "слабую" ссылку на объект, управляемый указателями shared_ptr. Типичный пример применения − реализация некоторой стратегии кэширования. Допустим, в кэше хранятся указатели на объекты. Мы хотим дать возможность где-то из другого места программы удалять некоторые из этих объектов для освобождения памяти. Однако, если в кэше будут использоваться "сильные" указатели типа shared_ptr, то они не позволят выполнять удаление объектов. Как раз для этой цели удобно использовать weak_ptr.

Некоторые полезные методы класса std::weak_ptr:

• expired − возвращает true, если объект, на который ссылается указатель, уничтожен

• lock − возвращает указатель shared_ptr на данный объект (weak_ptr не позволяет работать с объектом напрямую)

• use_count − возвращает количество указателей shared_ptr, которые ссылаются на данный объект

Проиллюстрируем на простом примере работу с weak_ptr:

// Пример 8.27 - использование указателей weak_ptr

// создадим новый объект типа int

std::shared_ptr<int> p = std::make_shared<int>(5);

// получим "слабый" указатель на объект

std::weak_ptr<int> w = p;

// изменим значение объекта через временный "сильный" указатель

std::shared_ptr<int> tmp = w.lock();

*tmp = 8;

tmp.reset();

p.reset(); // уничтожим объект

if (w.expired())

std::cout << "Объект был уничтожен\n";

Ещё одним возможным применением weak_ptr является решение проблемы циклических ссылок. Представим себе два каких-то объекта. Пусть поле типа shared_ptr внутри первого объекта ссылается на второй объект, а аналогичное поле внутри второго объекта − на первый. Поскольку счётчик ссылок для обоих объектов равен хотя бы единице, то их деструкторы не будут вызваны никогда. Для решения этой проблемы в одном из объектов можно заменить shared_ptr на weak_ptr.

В завершение данного подраздела приведём пример реализации шаблонного класса Stack − упрощенного аналога стандартного класса std::stack. Для хранения данных будем использовать структуру данных "связный список" с использованием "умных" указателей. Класс будет иметь следующие методы: push − вставка элемента, pop − удаление элемента, top − возвращение значения с вершины стека, empty − проверка стека на пустоту.

// Пример 8.28 - реализация шаблонного класса Stack

// с помощью умных указателей

#include <iostream>

#include <memory>

template <class T>

class Stack {

struct Elem {

T value;

std::shared_ptr<Elem> next;

Elem(const T &value, std::shared_ptr<Elem> next = nullptr)

: value(value), next(next) {}

};

std::shared_ptr<Elem> _top;

public:

bool empty() const { return _top == nullptr; }

void push(const T &value) {

_top = std::make_shared<Elem>(value, _top);

}

void pop() { _top = _top->next; }

T& top() { return _top->value; }

};

int main() {

Stack<int> s;

for (int i = 1; i <= 5; i++)

s.push(i);

while (!s.empty()) {

std::cout << s.top() << " ";

s.pop();

}

}

В данном разделе были рассмотрены некоторые нововведения стандарта С++11. Данный перечень не является полным, для детального изучения последних изменений следует обратиться к стандарту либо специализированной литературе.

Наши рекомендации