查看以下示例。 C ++标准是否保证object.x
的值最终会等于1
?如果我不打电话给析构函数object.~Class();
怎么办?
#include <new>
class Class
{
public:
Class() {}
~Class() {}
int x;
};
int main()
{
Class object;
object.x = 1;
object.~Class();
new (&object) Class();
object.x == ?
Class object_2;
object_2.x = 1;
new (&object_2) Class();
object_2.x == ?
return 0;
}
答案 0 :(得分:12)
否强>
x
等于1
的对象已被销毁。
你知道,因为你是摧毁它的人。
您的新x
未初始化且具有未指定的值,由于内存重复使用,实际上可能为1
。这与任何其他未初始化的价值没有什么不同。
由于似乎有很多人抛出断言,这里有一些事实,直接来自标准。
没有关于此案例的直接陈述,因为它遵循管理对象的一般规则以及对象生命周期的含义。
通常,一旦你知道C ++是一个抽象而不是字节的直接映射,你就可以理解这里发生了什么,以及为什么没有这样的作为OP寻求的保证。
首先,关于对象生命周期和破坏的一些背景知识:
[C++14: 12.4/5]:
如果析构函数不是用户提供的,那么析构函数是微不足道的:
- 析构函数不是
virtual
,- 其类的所有直接基类都有简单的析构函数,
- 对于类的所有类型(或其数组)的非静态数据成员,每个这样的类都有一个简单的析构函数。
否则,析构函数非平凡。
[C++14: 3.8]
: [..] 类型为T
的对象的生命周期在以下时间结束:
- 如果
T
是具有非平凡析构函数(12.4)的类类型,则析构函数调用将启动,或者- 对象占用的存储空间被重用或释放。
[C++14: 3.8/3]
:本国际标准中归属于对象的属性仅适用于给定对象的生命周期。 [..]
[C++14: 3.8/4]
:程序可以通过重用对象占用的存储或通过使用非平凡的析构函数显式调用类类型的对象的析构函数来结束任何对象的生命周期。 / strong> 对于具有非平凡析构函数的类类型的对象,程序不需要在重用或释放对象占用的存储之前显式调用析构函数;但是,如果没有显式调用析构函数或者如果没有使用delete-expression(5.3.5)来释放存储,则不应该隐式调用析构函数,并且任何程序都依赖于析构函数产生的副作用有未定义的行为。
(这段话的大部分内容与你的第一种情况无关,因为它 有一个显式的析构函数调用:你的第二种情况违反了本段中的规则,因此具有未定义的行为;不应讨论进一步。)
[C++14: 12.4/2]
:析构函数用于销毁其类类型的对象。 [..]
[C++14: 12.4/11]
: [..] 也可以明确调用析构函数。
[C++14: 12.4/15]
:一旦为对象调用析构函数,该对象就不再存在。 [..]
现在,一些具体细节。如果我们在展示位置之前检查object.x
该怎么办?
哇,好的。
[C++14: 12.7/1]
: [..] 对于具有非平凡析构函数的对象,在析构函数完成后引用对象的任何非静态成员或基类执行导致未定义的行为。
如果我们在安置后检查它怎么办?即新对象中x
的值是多少?正如问题所要求的那样,它是否会保证1
?请注意Class
的构造函数不包含x
的初始化程序:
[C++14: 5.3.4/17]:
创建类型为T
的对象的 new-expression 按如下方式初始化该对象:
- 如果省略 new-initializer ,则默认初始化对象(8.5);如果没有执行初始化,则该对象具有不确定的值。
- 否则, new-initializer 将根据8.5的初始化规则进行解释,以进行直接初始化。
[C++14: 8.5/16]:
表单中出现的初始化T x(a); T x{a};
以及
new
表达式(5.3.4),static_cast
表达式(5.2.9),功能表示法类型转换(5.2.3)以及基本和成员初始值设定项(12.6.2) )被称为直接初始化。
[C++14: 8.5/17]:
初始值设定项的语义如下。目标类型是要初始化的对象或引用的类型,源类型是初始化表达式的类型。如果初始化程序不是单个(可能带括号的)表达式,则不定义源类型。 [..]
- 如果初始值设定项为
()
,则对象将进行值初始化。- [..]
[C++14: 8.5/8]:
对T
类型的对象进行值初始化意味着:
- 如果T是一个(可能是cv限定的)类类型(第9条),没有默认构造函数(12.1)或者是用户提供或删除的默认构造函数,那么该对象是默认初始化的。
- [..]
[C++14: 8.5/7]:
默认初始化T
类型的对象意味着:
- 如果
T
是(可能是cv限定的)类类型(第9条),T
的默认构造函数(12.1)被称为(并且初始化为如果T
没有默认构造函数或重载决策(13.3)导致一个错误的形式 歧义或在初始化上下文中删除或无法访问的函数中;;- 如果
T
是数组类型,则每个元素都是默认初始化的; - 否则,不执行初始化。如果程序要求对const限定类型
T
的对象进行默认初始化,则T
应为具有用户提供的默认构造函数的类类型。
[C++14: 12.6.2/8]:
在非委托构造函数中,如果给定的非静态数据成员或基类未由mem-initializer-id指定(包括没有mem-initializer-list的情况,因为构造函数没有 ctor-initializer)并且实体不是抽象类的虚拟基类(10.4),然后是:
- 如果实体是具有大括号或等于初始化程序的非静态数据成员,则按照8.5中的规定初始化该实体;
- 否则,如果实体是匿名联合或变体成员(9.5),则不执行初始化;
- 否则,该实体已默认初始化(8.5)。
[C++14: 8.5/12]:
如果没有为对象指定初始值设定项,则默认初始化该对象; 如果未执行初始化,则具有自动或动态存储持续时间的对象具有不确定的值。
那么,标准是否保证您的替换对象的x
具有值1
?不,它没有。
在实践中,为什么不是?好吧,有很多原因。 Class
的析构函数是非平凡的,因此,根据3.8,第一个对象的生命周期在你调用它的析构函数后立即结束。
从技术上讲,编译器可以自由地在该位置放置一个对象,只要它在placement-new生效时被销毁。这里没有理由这样做,但没有什么可以禁止它;更重要的是,析构函数调用和placement-new之间的简单{ int x = 5; x = 42; }
将更有权重用该内存。在那时它不被用来代表任何对象!
更现实地,有些实现(例如Microsoft Visual Studio),对于调试模式程序,将可识别的位模式写入未使用的堆栈内存以帮助诊断程序错误。没有理由认为这样的实现不会挂钩到析构函数中,并且这样的实现会覆盖您的1
值。标准中没有任何内容禁止这一点。
的确,如果我用?
语句替换代码中的std::cout
行,以便我们实际检查值,我会收到有关未初始化变量的警告在使用析构函数的情况下值0
:
main.cpp: In function 'int main()':
main.cpp:19:23: warning: 'object.Class::x' is used uninitialized in this function [-Wuninitialized]
std::cout << object.x << '\n';
^
0
1
我不确定您需要多少证据才能证明此标准不能保证1
值。