Уничтожение объектов в C++

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

(Примечание. Предполагается, что это будет вход в FAQ по C++ в Stack Overflow. Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация в meta, с которой все это началось, будет подходящим местом для этого. этот вопрос отслеживается в чате C++, где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)

2 ответа

Решение

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

В то время как семантика уничтожения объектов класса определяется деструкторами, уничтожение скалярного объекта всегда запрещено. В частности, уничтожение указателя на переменную не приводит к уничтожению объекта.

Объекты с областью видимости

автоматические объекты

Автоматические объекты (обычно называемые "локальными переменными") разрушаются в обратном порядке их определения, когда поток управления выходит за рамки их определения:

void some_function()
{
    Foo a;
    Foo b;
    if (some_condition)
    {
        Foo y;
        Foo z;
    }  <--- z and y are destructed here
}  <--- b and a are destructed here

Если во время выполнения функции выдается исключение, все ранее созданные автоматические объекты уничтожаются до того, как исключение передается вызывающей стороне. Этот процесс называется разматыванием стека. Во время разматывания стека никакие дальнейшие исключения не могут оставлять деструкторы вышеупомянутых ранее созданных автоматических объектов. В противном случае функция std::terminate называется.

Это приводит к одному из самых важных рекомендаций в C++:

Деструкторы никогда не должны бросать.

нелокальные статические объекты

Статические объекты, определенные в области пространства имен (обычно называемые "глобальными переменными"), и члены статических данных разрушаются в обратном порядке их определения после выполнения main:

struct X
{
    static Foo x;   // this is only a *declaration*, not a *definition*
};

Foo a;
Foo b;

int main()
{
}  <--- y, x, b and a are destructed here

Foo X::x;           // this is the respective definition
Foo y;

Обратите внимание, что относительный порядок построения (и уничтожения) статических объектов, определенных в разных единицах перевода, не определен.

Если исключение покидает деструктор статического объекта, функция std::terminate называется.

локальные статические объекты

Статические объекты, определенные внутри функций, создаются, когда (и если) поток управления впервые проходит их определение. 1 Они уничтожаются в обратном порядке после исполнения main:

Foo& get_some_Foo()
{
    static Foo x;
    return x;
}

Bar& get_some_Bar()
{
    static Bar y;
    return y;
}

int main()
{
    get_some_Bar().do_something();    // note that get_some_Bar is called *first*
    get_some_Foo().do_something();
}  <--- x and y are destructed here   // hence y is destructed *last*

Если исключение покидает деструктор статического объекта, функция std::terminate называется.

1: Это чрезвычайно упрощенная модель. Детали инициализации статических объектов на самом деле намного сложнее.

подобъекты базового класса и подобъекты-члены

Когда поток управления покидает тело деструктора объекта, его дочерние подобъекты (также известные как "члены данных") разрушаются в обратном порядке их определения. После этого его подобъекты базового класса уничтожаются в порядке, обратном списку базовых спецификаторов:

class Foo : Bar, Baz
{
    Quux x;
    Quux y;

public:

    ~Foo()
    {
    }  <--- y and x are destructed here,
};          followed by the Baz and Bar base class subobjects

Если исключение выдается при строительстве одного из Foo субобъектов, то все его ранее построенные подобъекты будут уничтожены до распространения исключения. Foo деструктор, с другой стороны, не будет выполнен, так как Foo объект никогда не был полностью построен.

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

элементы массива

Элементы массива уничтожаются в порядке убывания. Если исключение выдается во время конструирования n-го элемента, элементы с n-1 по 0 разрушаются до того, как распространится исключение.

временные объекты

Временный объект создается, когда вычисляется предварительное выражение типа класса. Наиболее ярким примером выражения prvalue является вызов функции, которая возвращает объект по значению, например T operator+(const T&, const T&), При нормальных обстоятельствах временный объект разрушается, когда полное выражение, которое лексически содержит значение prvalue, полностью оценено:

__________________________ full-expression
              ___________  subexpression
              _______      subexpression
some_function(a + " " + b);
                          ^ both temporary objects are destructed here

Вышеупомянутый вызов функции some_function(a + " " + b) является полным выражением, потому что оно не является частью большего выражения (вместо этого оно является частью выражения-выражения). Следовательно, все временные объекты, которые создаются во время вычисления подвыражений, будут уничтожены в точке с запятой. Существует два таких временных объекта: первый создается во время первого добавления, а второй создается во время второго добавления. Второй временный объект будет разрушен раньше, чем первый.

Если исключение выдается во время второго добавления, первый временный объект будет уничтожен должным образом перед распространением исключения.

Если локальная ссылка инициализируется выражением prvalue, время жизни временного объекта расширяется до области локальной ссылки, поэтому вы не получите висячую ссылку:

{
    const Foo& r = a + " " + b;
                              ^ first temporary (a + " ") is destructed here
    // ...
}  <--- second temporary (a + " " + b) is destructed not until here

Если вычисляется выражение prvalue не-классового типа, результатом является значение, а не временный объект. Тем не менее, временный объект будет создан, если prvalue используется для инициализации ссылки:

const int& r = i + j;

Динамические объекты и массивы

В следующем разделе " уничтожить Х" означает "сначала уничтожить Х, а затем освободить основную память". Аналогично, создание X означает "сначала выделите достаточно памяти, а затем создайте X там".

динамические объекты

Динамический объект, созданный с помощью p = new Foo уничтожен через delete p, Если вы забудете delete p, у вас есть утечка ресурсов. Никогда не пытайтесь выполнить одно из следующих действий, поскольку все они ведут к неопределенному поведению:

  • уничтожить динамический объект с помощью delete[] (обратите внимание на квадратные скобки), free или любые другие средства
  • уничтожить динамический объект несколько раз
  • получить доступ к динамическому объекту после его уничтожения

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

динамические массивы

Динамический массив, созданный с помощью p = new Foo[n] уничтожен через delete[] p (обратите внимание на квадратные скобки). Если вы забудете delete[] p, у вас есть утечка ресурсов. Никогда не пытайтесь выполнить одно из следующих действий, поскольку все они ведут к неопределенному поведению:

  • уничтожить динамический массив с помощью delete, free или любые другие средства
  • уничтожить динамический массив несколько раз
  • получить доступ к динамическому массиву после его уничтожения

Если исключение выдается во время конструирования n-го элемента, элементы с n-1 по 0 уничтожаются в порядке убывания, основная память освобождается, и исключение распространяется.

(Как правило, вы должны std::vector<Foo> над Foo* для динамических массивов. Это делает написание правильного и надежного кода намного проще.)

умные указатели подсчета ссылок

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

(Как правило, вы должны std::shared_ptr<Foo> над Foo* для общих объектов. Это делает написание правильного и надежного кода намного проще.)

Деструктор объекта вызывается автоматически, когда срок службы объекта заканчивается и он уничтожается. Вы не должны обычно вызывать это вручную.

Мы будем использовать этот объект в качестве примера:

class Test
{
    public:
        Test()                           { std::cout << "Created    " << this << "\n";}
        ~Test()                          { std::cout << "Destroyed  " << this << "\n";}
        Test(Test const& rhs)            { std::cout << "Copied     " << this << "\n";}
        Test& operator=(Test const& rhs) { std::cout << "Assigned   " << this << "\n";}
};

Существует три (четыре в C++11) различных типа объектов в C++, и тип объекта определяет продолжительность жизни объектов.

  • Статическая длительность хранения объектов
  • Автоматическое хранение объектов продолжительности
  • Динамическое хранение объектов длительности
  • (В C++11) Объекты длительности хранения потоков

Статическая длительность хранения объектов

Это самые простые и равные глобальным переменным. Продолжительность жизни этих объектов (обычно) - длина приложения. Они (обычно) создаются до входа в main и уничтожаются (в обратном порядке создания) после выхода из main.

Test  global;
int main()
{
    std::cout << "Main\n";
}

> ./a.out
Created    0x10fbb80b0
Main
Destroyed  0x10fbb80b0

Примечание 1: Есть два других типа объекта длительности статического хранения.

статические переменные-члены класса.

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

статические переменные внутри функции.

Это лениво созданные объекты длительности статического хранения. Они создаются при первом использовании (в поточно-безопасном поместье для C++11). Как и другие объекты длительности статического хранения, они уничтожаются при завершении приложения.

Порядок строительства / уничтожения

  • Порядок построения в модуле компиляции четко определен и аналогичен объявлению.
  • Порядок построения между модулями компиляции не определен.
  • Порядок уничтожения является точным обратным порядку построения.

Автоматическое хранение объектов продолжительности

Это наиболее распространенный тип объектов и то, что вы должны использовать в 99% случаев.

Это три основных типа автоматических переменных:

  • локальные переменные внутри функции / блока
  • переменные-члены внутри класса / массива.
  • временные переменные.

Локальные переменные

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

int main()
{
     std::cout << "Main() START\n";
     Test   scope1;
     Test   scope2;
     std::cout << "Main Variables Created\n";


     {
           std::cout << "\nblock 1 Entered\n";
           Test blockScope;
           std::cout << "block 1 about to leave\n";
     } // blockScope is destrpyed here

     {
           std::cout << "\nblock 2 Entered\n";
           Test blockScope;
           std::cout << "block 2 about to leave\n";
     } // blockScope is destrpyed here

     std::cout << "\nMain() END\n";
}// All variables from main destroyed here.

> ./a.out
Main() START
Created    0x7fff6488d938
Created    0x7fff6488d930
Main Variables Created

block 1 Entered
Created    0x7fff6488d928
block 1 about to leave
Destroyed  0x7fff6488d928

block 2 Entered
Created    0x7fff6488d918
block 2 about to leave
Destroyed  0x7fff6488d918

Main() END
Destroyed  0x7fff6488d930
Destroyed  0x7fff6488d938

переменные-члены

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

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

  • Таким образом, для членов класса они создаются в порядке объявления
    и уничтожены в обратном порядке декларации
  • Таким образом, для членов массива они создаются в порядке 0->top
    и уничтожить в обратном порядке сверху ->0

временные переменные

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

std::string   data("Text.");

std::cout << (data + 1); // Here we create a temporary object.
                         // Which is a std::string with '1' added to "Text."
                         // This object is streamed to the output
                         // Once the statement has finished it is destroyed.
                         // So the temporary no longer exists after the ';'

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

Динамическое хранение объектов длительности

Эти объекты имеют динамический срок службы и создаются с new и уничтожен с призывом к delete,

int main()
{
    std::cout << "Main()\n";
    Test*  ptr = new Test();
    delete ptr;
    std::cout << "Main Done\n";
}

> ./a.out
Main()
Created    0x1083008e0
Destroyed  0x1083008e0
Main Done

Для разработчиков, которые поступают из языков, собираемых мусором, это может показаться странным (управление продолжительностью жизни вашего объекта). Но проблема не так страшна, как кажется. В C++ необычно непосредственное использование динамически размещаемых объектов. У нас есть объекты управления для контроля их продолжительности жизни.

Самая близкая вещь к большинству других собранных языков GC - std::shared_ptr, Это будет отслеживать количество пользователей динамически созданного объекта, и когда все они уйдут, вызову delete автоматически (я считаю это лучшей версией обычного объекта Java).

int main()
{
    std::cout << "Main Start\n";
    std::shared_ptr<Test>  smartPtr(new Test());
    std::cout << "Main End\n";
} // smartPtr goes out of scope here.
  // As there are no other copies it will automatically call delete on the object
  // it is holding.

> ./a.out
Main Start
Created    0x1083008e0
Main Ended
Destroyed  0x1083008e0

Объекты длительности хранения потоков

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

Другие вопросы по тегам