在C ++中确切地销毁对象是什么意思?因为没有垃圾收集器,我是否必须手动销毁它们?异常如何发挥作用?
(注意:这本来是Stack Overflow的C ++ FAQ的条目。如果您想批评以这种形式提供FAQ的想法,那么开始所有这些工作的meta上的发布将是这样做的地方。该问题在C ++聊天室中进行监控,该问题最初是从FAQ想法开始的,所以提出这个想法的人很可能会读懂您的答案。)
在C ++中确切地销毁对象是什么意思?因为没有垃圾收集器,我是否必须手动销毁它们?异常如何发挥作用?
(注意:这本来是Stack Overflow的C ++ FAQ的条目。如果您想批评以这种形式提供FAQ的想法,那么开始所有这些工作的meta上的发布将是这样做的地方。该问题在C ++聊天室中进行监控,该问题最初是从FAQ想法开始的,所以提出这个想法的人很可能会读懂您的答案。)
Answers:
在下文中,我将区分作用域对象和动态对象,这些作用域对象的破坏时间由其封闭范围(函数,块,类,表达式)静态确定,而动态对象的确切破坏时间通常要到运行时才能知道。
虽然类对象的销毁语义由析构函数确定,但是标量对象的销毁始终是无操作的。具体地说,破坏指针变量并没有破坏指针对象。
当控制流离开其定义范围时,自动对象(通常称为“局部变量”)按照其定义的相反顺序被破坏:
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:这是一个极其简化的模型。静态对象的初始化细节实际上要复杂得多。
当控制流离开对象的析构函数主体时,其成员子对象(也称为“数据成员”)将按照其定义的相反顺序进行破坏。之后,其基类子对象将以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
如果在构造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表达式求值,则结果为value,而不是临时对象。但是,如果使用prvalue初始化引用,则将构造一个临时对象:
const int& r = i + j;
在下一节中,destroy 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>
,但这是另一个讨论。
std::vector
代替std::deque
。仅在这里为我自己说话,但我希望自己的记忆是连续的。
resize()
在将元素插入其中之前会记得它:)
当对象的生存期结束并被销毁时,将自动调用该对象的析构函数。通常,您不应该手动调用它。
我们将以该对象为例:
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中为四种)不同的对象类型,而对象的类型定义了对象的寿命。
这些是最简单的并且等同于全局变量。这些对象的寿命通常是应用程序的长度。这些通常是在进入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
成员变量的寿命绑定到拥有它的对象。当所有者的寿命结束时,其所有成员的寿命也将终止。因此,您需要查看遵循相同规则的所有者的生命周期。
注意:成员总是按照相反的创建顺序在所有者之前被销毁。
这些是根据表达式创建的对象,但未分配给变量。临时变量与其他自动变量一样被销毁。只是它们范围的末尾就是创建它们的语句的末尾(通常是“;”)。
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
这些是新语言。它们非常类似于静态存储持续时间对象。但是,与其在与应用程序相关联的执行线程中生存,不如他们与应用程序一样生存。