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

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

<суб > (Примечание: это означает запись в Часто задаваемые вопросы о переполнении стека С++. Если вы хотите критиковать идею предоставления FAQ в этой форме, тогда публикация на мета, которая начала все это, была бы местом для этого. Ответы на этот вопрос отслеживаются в С++ чате, где идея FAQ начиналась в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.) Суб >

Ответ 1

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

В то время как семантика разрушения объектов класса определяется деструкторами, уничтожение скалярного объекта всегда является no-op. В частности, уничтожение указательной переменной не разрушает pointee.

Объекты с областью

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

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

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.

Это приводит к одному из наиболее важных принципов в С++:

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

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

Статические объекты, определенные в области пространства имен (обычно называемые "глобальными переменными" ) и статические члены данных, разрушаются в обратном порядке их определения после выполнения 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;

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

Если исключение оставляет деструктор статического объекта, вызывается функция 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 уничтожаются до того, как будет распространено исключение.

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

Временной объект создается, когда оценивается выражение класса praleue типа класса. Наиболее ярким примером выражения 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 типа non-class, результатом является значение, а не временный объект. Тем не менее, временный объект будет создан, если для инициализации ссылки используется prvalue:

const int& r = i + j;

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

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

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

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

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

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

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

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

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

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

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

ссылки на интеллектуальные указатели

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

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

Ответ 2

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

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

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";}
};

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

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

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

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

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

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

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

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

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

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

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

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

  • Порядок построения внутри единицы компиляции хорошо определен и совпадает с объявлением.
  • Порядок построения между единицами компиляции undefined.
  • Порядок разрушения является точной инверсией порядка построения.

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

Это наиболее распространенный тип объектов и то, что вы должны использовать в 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 и уничтожены в обратном порядке 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

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

Самое близкое к большинству других собранных 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

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

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