Когда именно уничтожаются объекты в C++ и что это значит? Нужно ли их уничтожать вручную, так как сборщика мусора нет? Как исключения вступают в игру?
_(Note: This is meant to be an entry to [Stack Overflow's C++ FAQ](https://stackoverflow.com/questions/tagged/c++-faq). If you want to critique the idea of providing an FAQ in this form, then [the posting on meta that started all this](https://meta.stackexchange.com/questions/68647/setting-up-a-faq-for-the-c-tag) would be the place to do that. Answers to that question are monitored in the [C++ chatroom](https://chat.stackoverflow.com/rooms/10/c-lounge), where the FAQ idea started out in the first place, so your answer is very likely to get read by those who came up with the idea.)_Разрушение объекта в C++
Ответы (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 типа класса. Наиболее ярким примером выражения 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, а затем освободить базовую память». Точно так же создать 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*
. Это значительно упрощает написание правильного и надежного кода.)
std::vector<Foo>
, а не Foo*
. - На самом деле, в большинстве случаев std::deque<Foo>
лучше, чем std::vector<Foo>
, но это другой разговор.
- person Mihai Todor; 17.07.2013
std::vector
вместо std::deque
. Здесь я говорю только за себя, но мне нравится, когда моя память непрерывна.
- person fredoverflow; 17.07.2013
resize()
его соответствующим образом, прежде чем вставлять в него элементы :)
- person Mihai Todor; 17.07.2013
std::vector::push_back
амортизировал постоянное время.
- person fredoverflow; 17.07.2013
Деструктор объекта вызывается автоматически, когда срок жизни объекта заканчивается и он уничтожается. Обычно вы не должны вызывать его вручную.
Мы будем использовать этот объект в качестве примера:
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++ существует три (четыре в C++11) различных типа объекта, и тип объекта определяет продолжительность жизни объекта.
- Статические объекты продолжительности хранения
- Объекты продолжительности автоматического хранения
- Объекты длительности динамического хранения
- (В С++ 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
и уничтожаются в обратном порядке 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
Объекты продолжительности хранения потоков
Это новые для языка. Они очень похожи на статические объекты длительности хранения. Но вместо того, чтобы жить той же жизнью, что и приложение, они живут до тех пор, пока поток выполнения, с которым они связаны.