問題描述
C++ 中的對象究竟何時被銷毀,這意味著什么?由于沒有垃圾收集器,我是否必須手動銷毀它們?異常如何發揮作用?
(注意:這是Stack Overflow 的 C++ 常見問題解答的條目.如果您想批評以這種形式提供常見問題解答,然后 開始在元數據上發帖所有這些都可以做到這一點.C++ 聊天室中監控該問題的答案a>,FAQ 的想法最初是從這里開始的,因此您的答案很可能會被提出該想法的人閱讀.)
在下面的文本中,我將區分作用域對象,它們的銷毀時間由它們的封閉作用域(函數、塊、類、表達式)和動態對象,它們的確切銷毀時間通常要到運行時才能知道.
雖然類對象的銷毀語義由析構函數決定,但標量對象的銷毀始終是空操作.具體來說,銷毀指針變量不會銷毀指針對象.
作用域對象
自動對象
當控制流離開其定義的范圍時,自動對象(通常稱為局部變量")將按照其定義的相反順序進行銷毀:
void some_function(){福一;福 b;如果(some_condition){傅y;福茲;} <--- z 和 y 在這里被破壞} <--- b 和 a 在這里被破壞
如果在函數執行過程中拋出異常,則在異常傳播到調用者之前,所有先前構造的自動對象都會被銷毀.這個過程稱為堆棧展開.在堆棧展開期間,前面提到的自動對象的析構函數不會再有任何異常.否則,函數 std::terminate
被調用.
這引出了 C++ 中最重要的準則之一:
<塊引用>析構函數不應該拋出.
非本地靜態對象
在命名空間范圍內定義的靜態對象(通常稱為全局變量")和靜態數據成員在執行main
后按照其定義的相反順序被銷毀:
struct X{靜態 Foo x;//這只是一個*聲明*,而不是*定義*};福一;福 b;int main(){} <--- y, x, b 和 a 在這里被破壞福 X::x;//這是各自的定義傅y;
請注意,在不同翻譯單元中定義的靜態對象的構造(和銷毀)的相對順序是未定義的.
如果異常離開靜態對象的析構函數,則調用函數std::terminate
.
本地靜態對象
在函數內部定義的靜態對象是在(如果)控制流第一次通過它們的定義時構造的.1main
執行后,它們以相反的順序銷毀:
Foo&get_some_Foo(){靜態 Foo x;返回 x;}酒吧&get_some_Bar(){靜態酒吧 y;返回 y;}int main(){get_some_Bar().do_something();//注意 get_some_Bar 被稱為 *first*get_some_Foo().do_something();} <--- x 和 y 在這里被破壞//因此 y 被破壞 *last*
如果異常離開靜態對象的析構函數,則調用函數std::terminate
.
1:這是一個極其簡化的模型.靜態對象的初始化細節其實要復雜得多.
基類子對象和成員子對象
當控制流離開對象的析構函數體時,其成員子對象(也稱為其數據成員")按照其定義的相反順序進行析構.之后,它的基類子對象按照基類說明符列表的相反順序銷毀:
class Foo : Bar, Baz{庫克斯 x;曲y;上市:~Foo(){} <--- y 和 x 在這里被破壞,};其次是 Baz 和 Bar 基類子對象
如果在 Foo
的子對象之一的構造過程中拋出異常,那么在傳播異常之前,其先前構造的所有子對象都將被銷毀.另一方面,Foo
析構函數將不會被執行,因為 Foo
對象從未被完全構造過.
請注意,析構函數體不負責析構數據成員本身.如果數據成員是對象銷毀時需要釋放的資源的句柄(例如文件、套接字、數據庫連接、互斥鎖或堆內存),則只需編寫析構函數.>
數組元素
數組元素按降序銷毀.如果在第 n 個元素的構造過程中拋出異常,則在傳播異常之前先銷毀第 n-1 到 0 個元素.
臨時對象
當評估類類型的純右值表達式時,會構造一個臨時對象.prvalue 表達式最突出的例子是調用按值返回對象的函數,例如 T operator+(const T&, const T&)
.一般情況下,當詞法上包含純右值的完整表達式被完全求值時,臨時對象就會被破壞:
__________________________ 完整表達式___________子表達式_______ 子表達式some_function(a + " " + b);^ 兩個臨時對象都在這里被破壞
上面的函數調用 some_function(a + " " + b)
是一個完整的表達式,因為它不是更大表達式的一部分(相反,它是表達式語句的一部分).因此,在評估子表達式期間構造的所有臨時對象都將在分號處銷毀.有兩個這樣的臨時對象:第一個是在第一次加法時構造的,第二個是在第二次加法時構造的.第二個臨時對象將在第一個之前銷毀.
如果在第二次添加過程中拋出異常,第一個臨時對象將在傳播異常之前正確銷毀.
如果局部引用是用純右值表達式初始化的,臨時對象的生命周期會擴展到局部引用的范圍,所以你不會得到一個懸空引用:
<代碼>{const Foo&r = a + " " + b;^ 第一個臨時 (a + " ") 在這里被破壞//...} <--- 第二個臨時 (a + " " + b) 直到這里才被破壞
如果計算非類類型的純右值表達式,結果是一個值,而不是一個臨時對象.但是,如果使用純右值來初始化引用,則將構造一個臨時對象:
const int&r = i + j;
動態對象和數組
在下一節中,destroy X 的意思是先銷毀 X,然后釋放底層內存".類似地,create 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*
.它使編寫正確和健壯的代碼變得更加容易.)
引用計數智能指針
一個由多個std::shared_ptr
對象管理的動態對象在最后一個參與共享的std::shared_ptr
對象銷毀過程中被銷毀那個動態對象.
(對于共享對象,您通常應該更喜歡 std::shared_ptr<Foo>
而不是 Foo*
.它使編寫正確和健壯的代碼變得更加容易.)
When exactly are objects destroyed in C++, and what does that mean? Do I have to destroy them manually, since there is no Garbage Collector? How do exceptions come into play?
(Note: This is meant to be an entry to Stack Overflow's 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 would be the place to do that. Answers to that question are monitored in the C++ chatroom, 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.)
In the following text, I will distinguish between scoped objects, whose time of destruction is statically determined by their enclosing scope (functions, blocks, classes, expressions), and dynamic objects, whose exact time of destruction is generally not known until runtime.
While the destruction semantics of class objects are determined by destructors, the destruction of a scalar object is always a no-op. Specifically, destructing a pointer variable does not destroy the pointee.
Scoped objects
automatic objects
Automatic objects (commonly referred to as "local variables") are destructed, in reverse order of their definition, when control flow leaves the scope of their definition:
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
If an exception is thrown during the execution of a function, all previously constructed automatic objects are destructed before the exception is propagated to the caller. This process is called stack unwinding. During stack unwinding, no further exceptions may leave the destructors of the aforementioned previously constructed automatic objects. Otherwise, the function std::terminate
is called.
This leads to one of the most important guidelines in C++:
Destructors should never throw.
non-local static objects
Static objects defined at namespace scope (commonly referred to as "global variables") and static data members are destructed, in reverse order of their definition, after the execution of 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;
Note that the relative order of construction (and destruction) of static objects defined in different translation units is undefined.
If an exception leaves the destructor of a static object, the function std::terminate
is called.
local static objects
Static objects defined inside functions are constructed when (and if) control flow passes through their definition for the first time.1
They are destructed in reverse order after the execution of 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*
If an exception leaves the destructor of a static object, the function std::terminate
is called.
1: This is an extremely simplified model. The initialization details of static objects are actually much more complicated.
base class subobjects and member subobjects
When control flow leaves the destructor body of an object, its member subobjects (also known as its "data members") are destructed in reverse order of their definition. After that, its base class subobjects are destructed in reverse order of the base-specifier-list:
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
If an exception is thrown during the construction of one of Foo
's subobjects, then all its previously constructed subobjects will be destructed before the exception is propagated. The Foo
destructor, on the other hand, will not be executed, since the Foo
object was never fully constructed.
Note that the destructor body is not responsible for destructing the data members themselves. You only need to write a destructor if a data member is a handle to a resource that needs to be released when the object is destructed (such as a file, a socket, a database connection, a mutex, or heap memory).
array elements
Array elements are destructed in descending order. If an exception is thrown during the construction of the n-th element, the elements n-1 to 0 are destructed before the exception is propagated.
temporary objects
A temporary object is constructed when a prvalue expression of class type is evaluated. The most prominent example of a prvalue expression is the call of a function that returns an object by value, such as T operator+(const T&, const T&)
. Under normal circumstances, the temporary object is destructed when the full-expression that lexically contains the prvalue is completely evaluated:
__________________________ full-expression
___________ subexpression
_______ subexpression
some_function(a + " " + b);
^ both temporary objects are destructed here
The above function call some_function(a + " " + b)
is a full-expression because it is not part of a larger expression (instead, it is part of an expression-statement). Hence, all temporary objects that are constructed during the evaluation of the subexpressions will be destructed at the semicolon. There are two such temporary objects: the first is constructed during the first addition, and the second is constructed during the second addition. The second temporary object will be destructed before the first.
If an exception is thrown during the second addition, the first temporary object will be destructed properly before propagating the exception.
If a local reference is initialized with a prvalue expression, the lifetime of the temporary object is extended to the scope of the local reference, so you won't get a dangling reference:
{
const Foo& r = a + " " + b;
^ first temporary (a + " ") is destructed here
// ...
} <--- second temporary (a + " " + b) is destructed not until here
If a prvalue expression of non-class type is evaluated, the result is a value, not a temporary object. However, a temporary object will be constructed if the prvalue is used to initialize a reference:
const int& r = i + j;
Dynamic objects and arrays
In the following section, destroy X means "first destruct X and then release the underlying memory". Similarly, create X means "first allocate enough memory and then construct X there".
dynamic objects
A dynamic object created via p = new Foo
is destroyed via delete p
. If you forget to delete p
, you have a resource leak. You should never attempt to do one of the following, since they all lead to undefined behavior:
- destroy a dynamic object via
delete[]
(note the square brackets),free
or any other means - destroy a dynamic object multiple times
- access a dynamic object after it has been destroyed
If an exception is thrown during the construction of a dynamic object, the underlying memory is released before the exception is propagated. (The destructor will not be executed prior to memory release, because the object was never fully constructed.)
dynamic arrays
A dynamic array created via p = new Foo[n]
is destroyed via delete[] p
(note the square brackets). If you forget to delete[] p
, you have a resource leak. You should never attempt to do one of the following, since they all lead to undefined behavior:
- destroy a dynamic array via
delete
,free
or any other means - destroy a dynamic array multiple times
- access a dynamic array after it has been destroyed
If an exception is thrown during the construction of the n-th element, the elements n-1 to 0 are destructed in descending order, the underlying memory is released, and the exception is propagated.
(You should generally prefer std::vector<Foo>
over Foo*
for dynamic arrays. It makes writing correct and robust code much easier.)
reference-counting smart pointers
A dynamic object managed by several std::shared_ptr<Foo>
objects is destroyed during the destruction of the last std::shared_ptr<Foo>
object involved in sharing that dynamic object.
(You should generally prefer std::shared_ptr<Foo>
over Foo*
for shared objects. It makes writing correct and robust code much easier.)
這篇關于C++中的對象銷毀的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!