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

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

#include <iostream>
class Student{
  std::string sid;
  std::string name;
};
int main(){
  // allocating memory to the heap.
  Student *myStudent = new Student();
  
  // do something with student...
  
  // if something is allocated to heap
  // you must manually delete it.
  // don't forget to delete it
  delete myStudent;
  return 0;
} // no memory leak

Мы начнем с определения нашего класса Student, он имеет два поля sid и имя типа std::string.

Давайте посмотрим на некоторые детали здесь,

Student *myStudent = new Student();

Новый оператор выделяет память в куче и возвращает адрес этой выделенной памяти. Здесь myStudent — это указатель (необработанный), хранящийся в стеке, который указывает на содержимое в куче, как показано на диаграмме ниже.

Стек против кучи

Все, что хранится в стеке, удаляется, как только оно выходит за пределы области видимости, но это не относится к куче. Мы должны вручную удалить их. Поэтому, если myStudent выходит за пределы области видимости и вы еще не вызвали для него оператор удаления, вы столкнетесь с утечкой памяти. Память — ценный ресурс, нам нужно избегать таких условий.

Можем ли мы не удалить память, выделенную в куче, как только myStudent выйдет из области видимости? Примерно так работают умные указатели.

Позвольте мне сказать здесь, необработанные указатели — это зло, они совсем не безопасны.

Проблемы с необработанными указателями

Вы можете столкнуться с этими проблемами при работе с необработанными указателями.

  • Утечка памяти: забывание освободить выделенную память после использования.
  • Двойное освобождение: освобождение уже освобожденной памяти.
  • Использовать после освобождения: попытка доступа к уже освобожденной памяти.

Работая с необработанными указателями, вы должны позаботиться об этих частях. И позвольте мне напомнить вам, что это очень распространенный источник ошибок в кодовой базе C++.

Отказ от необработанных указателей

Чтобы решить эти проблемы, C++ представил интеллектуальные указатели. C++ поддерживает 3 разных интеллектуальных указателя:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

Все эти интеллектуальные указатели определены в пространстве имен std под заголовком ‹memory›. В этой статье мы будем говорить только об unique_ptr и shared_ptr.

Что такое умный указатель?

Умный указатель похож на необработанный указатель с некоторыми дополнительными функциями. Дополнительная функция включает в себя автоматическое управление памятью. C++ всегда критиковали за отсутствие автоматического управления памятью. С введением интеллектуальных указателей нам больше не нужно управлять памятью вручную, интеллектуальные указатели будут для нас.

Уникальный_указатель:

#include <iostream>
#include <memory>
class Student{
   std::string sid;
   std::string name;
   public:
     Student(std::string id, std::string sname){
          sid = id;
          name = sname;
     }
     void printName(){
        std::cout << name << '\n';
     }
     void printSID(){
        std::cout << sid << '\n';
     }
};
int main(){
  auto myStudent = std::unique_ptr<Student>(new Student("4","Abhilekh Gautam"));
  // accessing member function is similar to regular(raw) pointers.
  myStudent->printName();
} // no delete required at all

Давайте посмотрим на изменения по сравнению с предыдущими фрагментами,

  • Мы включили заголовок ‹memory›
  • Добавлен конструктор и две вспомогательные функции для нашего класса.
  • Создал unique_ptr , myStudent.

Теперь давайте перейдем к уникальному_ptr.

Мы просто передаем необработанный указатель конструктору unique_ptr, и с нашей стороны все, мы можем использовать методы, связанные с классом Student, используя оператор -›.

После того, как интеллектуальный указатель инициализирован, он владеет необработанным указателем, т. е. интеллектуальный указатель теперь отвечает за удаление памяти, на которую указывает указатель, который мы передали конструктору. Деструктор интеллектуального указателя содержит весь код для удаления выделенной памяти. Поскольку мы объявили смарт-указатель в стеке, его деструктор вызывается, когда он выходит за пределы области видимости, и, следовательно, выполняется вся очистка.

Начиная с C++ 14, вы можете использовать следующую конструкцию для создания нового экземпляра unique_ptr.

auto myptr = std::make_unique<Student>(Student("4","Abhilekh Gautam"));

Что нужно помнить об unique_ptr:

Мы должны помнить, что unique_ptr предотвращает копирование содержащегося в нем необработанного указателя.

auto myStudent = std::unique_ptr<Student> (new Student("1","Ram"));
auto anotherStudent = myStudent; // compile time error.

Вы не можете скопировать файл unique_ptr. Но хорошо, что ты можешь двигаться.

auto myStudent = std::unique_ptr<Student> (new Student("1","Ram"));
auto anotherStudent = std::move(myStudent); // move ownership

Переместить сюда означает передать право собственности anotherStudent, поскольку у нас может быть только один владелец, для которого myStudent задано значение nullptr.

Shared_ptr:

Наличие только одного владельца для необработанного указателя иногда может быть проблематичным, поэтому для решения этой проблемы мы используем shared_ptr.

Shared_ptr также является оболочкой для необработанного указателя, такого как unique_ptr. Но shared_ptr поддерживает счетчик ссылок для количества владельцев, и память очищается только тогда, когда счетчик ссылок равен нулю.

Тогда давайте посмотрим фрагмент кода

#include <iostream>
#include <memory>

class Student{
   std::string sid;
   std::string name;
   public:
     Student(std::string id, std::string sname){
          sid = id;
          name = sname;
     }
     void printName(){
        std::cout << name << '\n';
     }
     void printSID(){
        std::cout << sid << '\n';
     }
};
int main(){
    auto myStudent = std::shared_ptr<Student>(new Student("4","Abhilekh Gautam"));
    // simply increments the reference count
    auto myAnotherStudent = myStudent; // absolutely fine
    // we can use `use_count()` method to get the reference count
    std::cout << "Reference Count: " << myStudent.use_count() << '\n';
    {
      // increments the reference count again
      auto myYetAnotherStudent = myAnotherStudent;
    
    } // myYetAnotherStudent goes out of scope here
      // going out of scope means a decrease in reference count.
    
    std::cout << "Reference Count: " << myStudent.use_count() << '\n';
    std::cout << "Reference Count: " << myAnotherStudent.use_count() << '\n';
}

Мы создали shared_ptr, передав необработанный указатель его конструктору. После его создания счетчик ссылок устанавливается равным One, затем мы можем снова скопировать его в другой shared_ptr, который снова увеличивает счетчик ссылок. Это означает, что исходный необработанный указатель теперь имеет двух владельцев, и содержимое кучи будет очищено только тогда, когда счетчик ссылок будет равен нулю.

Схематически это можно представить как

И когда один из shared_ptr выходит за рамки,

После того, как все они выходят из области видимости, счетчик ссылок становится равным нулю, а выделенная память удаляется.

Начиная с C++ 14, вы можете использовать следующую конструкцию для создания нового экземпляра shared_ptr.

auto myptr = std::make_shared<Student>(Student("4","Abhilekh Gautam"));

Подводя итог для вас:

  • Необработанные указатели - зло, они предпочитают умные указатели.
  • std::unique_ptr не может быть скопирован.
  • std::shared_ptr позволяет иметь нескольких владельцев одних и тех же данных, используя механизм подсчета ссылок.

К настоящему моменту вы уже должны иметь хорошее представление об умных указателях C++ и о том, какую пользу они могут принести вашим проектам программирования.

Удачного кодирования!