Умные указатели и перегрузка операторов

Проблема:

Написав класс Array, мы попробовали оградить программиста от ошибок, связанных с неосвобожденной памятью. Но как только мы решили, что память под некоторые объекты лучше выделять в куче, так сразу мы вернулись к изначальной проблеме. Если мы выделяем память, например:
Object* p = new Object();
Мы получаем указатель на объект, но с этим могут быть связаны ошибки, к примеру, надо ровно один раз написать:
delete p;
Логичным решением этой проблемы являются так называемые умные указатели. Их мы разберём 3 штуки. Итак, первый из них:

scoped_ptr

scoped_ptr (от английского scope - область видимости, и pointer - указатель) - самый простой в реализации вариант умных указателей. Идея крайне проста - сделать так, чтобы указатель сам освобождал память когда область его видимости закончилась(т.е. деструктор выполнял команду delete).
Попробуем реализовать такой класс:
Определение класса:
class scoped_ptr {

private:
  Object* myPointer;

public:
  scoped_ptr(Object* ptr);
  ~scoped_ptr();
   Object* ptr();

private:
//надо запретить копирование объектов
  scoped_ptr(const scoped_ptr& p);
  const scoped_ptr& operator=(const scoped_ptr& p);  
}
Определение методов:
scoped_ptr::scoped_ptr(Object* ptr) {
  myPointer = ptr;
}

scoped_ptr::~scoped_ptr() {
  delete myPointer;
}

Object* scoped_ptr::ptr() {
  return myPointer;
}

Примечание: Мы запретили конструктор копирования и оператор копирования(т.е. полностью запретили копирование объектов класса), чтобы избежать очевидной ошибки повторного освобождения памяти, которая возникла бы в противном случае.

Что получилось: Можно написать:
{
  scoped_ptr ptr = new Object; 
  ...
}
И память автоматически освободится в конце области видимости. Кроме того, имеем доступ к указателю с помощью метода ptr.

Что хотелось бы ещё? Хочется писать так, как мы писали бы, если бы наш scoped_ptr был обычным указателем на Object, т.е. пользоваться операторами унарная звёздочка и оператором стрелочка. Это подводит нас к самой интересной части рассказа, к перегрузке. С перегрузкой мы встречались, когда писали несколько конструкторов у классов.
Реализуем 2 метода в нашем классе:
public:  
  Object& operator *() const;
  Object* operator->() const;
 
  
Object& scoped_ptr::operator*() const{
  return *myPointer;
}

Object& scoped_ptr::operator->() const{
  return myPointer;
}
В чём удобство: Реализовав эти 2 метода, мы теперь имеем доступ к указателю на объект и самому объекту с помощью методов operator-> и operator* соответственно. Т.е. можем писать такой код(в примере предполагается, что функция processObject принимает на вход объект класса Object, а doSomething какой-то метод класса Object):
processObject(p.operator*());
//или
p.operator->()->doSomething();
Но основное удобство перегрузки этих операторов заключается в том, что компилятор считает, что следующие 2 строчки значат одно и тоже(вторая из них просто особая запись первой):
processObject(p.operator*());
processObject(*p);
И, кроме того, что следующие 2 строчки идентичны по значению:
p.operator->()->doSomething;
p->doSometging();
Таким образом, мы выполнили поставленную задачу. Но осталось ещё одна ошибка, которую мы не предусмотрели. Следующий код теперь приведёт к ошибке:
{
  scoped_ptr p = 0;
}
Нужно предотвратить освобождение памяти по адресу 0. Для этого вставим проверку того, является ли myPointer нулем в деструктор и реализуем метод isNull, чтобы пользователь мог самостоятельно проверять корректность указателя.
Окончательная версия класса scoped_ptr будет выглядеть так:

Определение класса:
class scoped_ptr {

private:
  Object* myPointer;

public:

  scoped_ptr(Object* ptr);
  ~scoped_ptr();

public:

  Object* ptr();
  Object& operator *() const;
  Object* operator->() const;
  bool isNull() const;

private:
  scoped_ptr(const scoped_ptr& p);
  const scoped_ptr& operator=(const scoped_ptr& p);  
}
Определение методов:
scoped_ptr::scoped_ptr(Object* ptr) {
   myPointer = ptr;
}

scoped_ptr::~scoped_ptr() {
  if (myPointer != 0) {
    delete myPointer;
  }
}

Object& scoped_ptr::operator *() const {
  return *myPointer;
}

Object& scoped_ptr::operator ->() const {
  return myPointer;
}

bool scoped_ptr::isNull() const {
  return myPointer == 0;
}

Очевидно, что у scoped_ptr все ещё много недостатков. К примеру, мы не сможем присвоить ему значение, возвращаемое какой либо функцией(которая, к примеру, создаёт объект и возвращает на него указатель). Кроме того, иногда хотелось бы копировать указатели. Некоторые из этих проблем решает следующий вариант умных указателей:

auto_ptr

auto_ptr ужасно похож на scoped_ptr. Отличие состоит в том, что его можно копировать. Идея состоит в следующем:
Пусть у нас есть указатель p0 указывающий на какой-то объект класса Object.
auto_ptr p0 = new Object;
Заведём указатель p1 и присвоим ему значение p0;
auto_ptr p1 = p0;
Понятно, что если здесь просто скопировать значение поля myPointer, то это приведёт к повторному освобождению памяти деструктором. Поэтому сделаем так, что после этой строчки p1 у нас указывает туда куда указывал p0, а тот в свою просто сделаем равным нулю. Т.е. после копирования (оператором присвоения или конструктором копирования), только один указатель продолжает указывать на наш объект, а второй приравнивается 0.

Приведём реализацию класса auto_ptr:
Определение класса:
class auto_ptr {

private:
  Object* myPointer; 

public:

  auto_ptr(Object* ptr);
  ~scoped_ptr();
  auto_ptr(auto_ptr& p);

  auto_ptr& operator=(auto_ptr& p);  
  Object* ptr();
  Object& operator *() const;
  Object* operator->() const;
  bool isNull() const;
}
Определение методов:
auto_ptr::auto_ptr(Object* ptr) {
   myPointer = ptr;
}

auto_ptr::~auto_ptr() {
  if (myPointer != 0) {
    delete myPointer;
  }
}

auto_ptr::(auto_ptr& p) {
  myPointer = p.myPointer;
  p.myPointer = 0; 
}

Object* auto_ptr::ptr() {
  return myPointer;
}

Object& auto_ptr::operator *() const {
  return *myPointer;
}

auto_ptr& auto_ptr::operator=(auto_ptr& p) {
  if (this != &p) {
    if (myPointer != 0) {
      delete myPointer;
    }
    myPointer = p.myPointer;
    p.myPointer = 0;    
  }
  return *this;
}  

Object& auto_ptr::operator ->() const {
  return myPointer;
}

bool auto_ptr::isNull() const {
  return myPointer == 0;
}


shared_ptr

Последний вариант умных указатель, рассматриваемый в данной лекции это shared_ptr, разделяемые или считающие указатели.
Для shared_ptr хочется реализовать следующие возможности:
1. Всю ту же функциональность, что и в предыдущих вариантах.
2. Возможность копировать указатели, причём так, чтобы как только закончилась область видимости последнего указателя, указывающего на данный объект, вызывался деструктор для него.
Идея реализации:
Создаётся специальный вспомогательный объект Storage, который считает кол-во ссылок на данный объект типа Object (т.е. сколько указателей указывает на него в данный момент). Когда счётчик становится нулём, освобождаем память из-под объекта. Попробуем проиллюстрировать пример кода:
1. Пусть мы создали shared_ptr, по указателю на Object. Создаём вспомогательный объект Storage, в котором есть счётчик ссылок(и кроме того указатель на Object), устанавливаем его на 1.
shared_ptr p1 = new Object;

2. Пусть теперь создали другой указатель и присвоили ему значение 1ого, тогда увеличиваем счётчик на 1.
shared_ptr p2 = p1;

3. Теперь область видимости заканчивается и вызывается деструктор p2. Уменьшаем счётчик на 1, т.к. значение счётчика не 0, то больше ничего не делаем.

4. Вызывается деструктор p1, уменьшаем счётчик на 1, т.к. он стал равен нулю, вызываем деструктор для Object, а затем для Storage.

Реализовать класс shared_ptr предлагается в качестве домашнего задания.

Стандартная библиотека языка Си++

Как упомяналось ранее, язык состоит из двух частей: собственно синтаксиса языка и его стандартной библиотеки. Есть большое множество средств, которые требуются программистам крайне часто, но не являются частью синтаксиса языка, например, всевозможные структуры данных, как список или дерево, и многое другое. Всё это содержится в стандартной библиотеке языка Си++. Но прежде чем начать разговор про саму библиотеку необходимо затронуть ещё одну небольшую, но важную тему:

Примечание: Чем новее(младше) язык, тем больше его стандартная библиотека. К примеру, библиотека языка Java в разы превосходит библиотеку языка Cи++ по размеру и функциональности.

Namespace


Проблема: При использовании и написании библиотек возникает проблема нехватки имён, ведь если программист использует какую-то библиотеку, то он не может назвать свой класс именем класса, реализованного в библиотеке. При этом общеупотребимых слов английского языка, которые в основном используются для названия классов, просто напросто не хватает.

Решение: При написании программ принято помещать свои классы в отдельный namespace(от английского - пространство имён). Имена для namespace'ов выбирают по текущей решаемой задаче или по имени и фамилии программиста.

Пример:
namespace myNamespace {
  class List {
  ...
  ...
  };
}
Реализациюметодов класса List обычно помещают в тот же namespace, но можно приписать название namespace'a и два двоеточия перед именем метода.

Вся стандартная библиотека лежит в namespace'e std. Есть 2 способа воспользоваться классами из стандартной библиотеки:

1)Первый способ заключается в том, что перед именем класса из стандартной библиотеке приписывается имя namespace'a через двойное двоеточие. Например, чтобы воспользоваться классом "список из int'ов":
std::list<int>
2)Второй способ написать в начале программы:
using namespace std;
Примечание: Николай Михайлович предлагает навсегда забыть про второй способ.

Классы стандартной библиотеки

Наконец-то, мы можем перейти к краткому описанию того, что собственно содержится в стандартной библиотеке:

std::string
Класс строк, очень удобных для использования. Их можно инициализировать также, как мы делали это с массивом char'ов, складывать друг с другом, приравнивать друг другу и ещё много разных крайне полезных в программировании вещей. Реализованны они с помошью считающих ссылок (похожих на те, что мы использовали в shared_ptr). Примеры:
std::string str1 = "Hello";
str1 += ", World";
std::string str2 = str1 + "!";

std::list
Двусвязный список. В него можно добавлять элементы (в начало, в конец), удалять элементы, пробегать по списку т.д.

std::vector
Автоматически расширяющийся массив. В нём можно обращаться к элементу по индексу, удалять, добавлять элементы и т.д.

std::set
Множество. Реализовано через красно-чёрные деревья.

std::map
Множество пар элементов(ключ и значение, которые могут быть разных типов). Также реализовано через красно-черные деревья.

Примеры: Чтобы завести список, массив или мн-во нужно указать, что будет в нём хранится. Делается это с помощью угловых скобочек(знаков "больше" или "меньше"):
//список int'ов
std::list<int> list1;
//массив указателей на char*
std::vector<char*> list2;
//мн-во пар, где первый элемент это списки, содержащие элементы класса Object, а второй это множества содержащие элементы класса Object
std::map<std::list<Object>, std::set<Object> > list3;
Примечание 1: В последнем примере закрывающие угловые скобки должны быть обязательно разделены пробелом.

Примечание 2: Такая техника передачи типа в угловые скобки называется шаблоном(template).

Подключение заголовочных файлов: Чтобы воспользоваться всеми прелестями функций стандартной библиотеки нужно подключить заголовочные файлы, в которых хранятся определения классов стандартной библиотеки. Определения, описанных выше классов, все хранятся в разных заголовочных файлах. Эти файлы одноимённы с классами, что хранятся в них. Т.е. класс list хранится в header'e list. Чтобы подключить его:
#include <list>
Стоит отметить, что не надо приписывать расширение ".h" как это принято в языке Си. Чтобы "модно" подключить header'ы библиотеки языка Си можно вместо расширения приписать букву 'c' перед именем заголовочного файла. Например:
#include <cstdio>
//вместо
#include <stdio.h>
Данные способы приводят к практически идентичному результату.

Примечание: Ни в коем случае не надо путать header'ы <string> библиотеки языка Си++ и <string.h> библиотеки языка Си.

Мораль всей истории:

Не стоит изобретать велосипед и писать структуры данных, которые уже реализованы в стандартной библиотеке. В подавляющем большинстве случаев они будут работать достаточно эффективно, чтобы можно было не тратить время на ненужную работу.