重用内存位置安全吗?
这个问题是基于一些现有的C代码移植到C ++。 我只是对它是否“安全”感兴趣。 我已经知道我不会这样写了。 我知道这里的代码基本上是C而不是C ++,但它是用C ++编译器编译的,而且我知道这些标准有时会略有不同。
我有一个分配一些内存的函数。 我将返回的void*
转换为int*
并开始使用它。
后来我将返回的void*
转换为Data*
并开始使用它。
这在C ++中是安全的吗?
例如: –
void* data = malloc(10000); int* data_i = (int*)data; *data_i = 123; printf("%d\n", *data_i); Data* data_d = (Data*)data; data_d->value = 456; printf("%d\n", data_d->value);
我从不读取通过与存储types不同的types使用的variables,但担心编译器可能会看到data_i
和data_d
是不同的types,因此不能合法地互相data_i
,并决定重新sorting我的代码,例如将商店放到data_d
第一个printf
。 哪个会打破一切。
然而,这是一直在使用的模式。 如果在两次访问之间插入一个free
和malloc
,我不相信它会改变任何事情,因为它不会触及受影响的内存本身,并可以重复使用相同的数据。
我的代码是否被破坏或“正确”?
它是“OK”,它按照你写的那样工作(假设原始数据和简单的旧数据types(POD))。 这是安全的。 它实际上是一个自定义内存pipe理器。
一些说明:
-
如果在分配的内存的位置创build具有非平凡析构函数的对象,请确保调用它
obj->~obj();
-
如果要创build对象,请考虑放置一个普通投射的新语法 (同样适用于POD)
Object* obj = new (data) Object();
-
检查
nullptr
(或NULL
),如果malloc
失败,则返回NULL
- alignment不应该是一个问题,但在创build内存pipe理器时始终要注意,确保alignment是合适的
鉴于您使用的是C ++编译器,除非要保持代码的“C”性质,否则您还可以查看全局operator new()
。
和往常一样,一旦完成,不要忘记free()
(或delete
如果使用new
)
你提到你不会转换任何代码, 但是如果或者当你真的考虑它的时候,在C ++中你可能希望使用malloc
甚至global ::operator new
。
你应该看看智能指针std::unique_ptr<>
或std::shared_ptr<>
并允许他们照顾内存pipe理问题。
根据Data
的定义,您的代码可能会被破坏。 这是不好的代码,无论如何。
如果Data
是普通的旧数据types(POD,即基本types的typedef,PODtypes的结构等), 并且分配的内存针对types (*) 正确alignment ,那么您的代码是明确定义的 ,这意味着它将“工作”(只要你在使用它之前初始化 *data_d
每个成员),但这不是好习惯。 (见下文。)
如果Data
是一个非PODtypes的,那么你就会遇到麻烦:例如,指针分配不会调用任何构造函数。 data_d
是“指向Data
指针”types,因为它指向某个东西,所以会有效地说谎 ,但是某些东西不是Data
types的,因为没有创build/构造/初始化这样的types。 未定义的行为在这一点上并不遥远。
在给定的内存位置正确构build对象的解决scheme称为新增贴图 :
Data * data_d = new (data) Data();
这指示编译器在位置data
上构造一个Data
对象。 这将适用于POD和非PODtypes。 你还需要调用析构函数( data_d->~Data()
)来确保它在delete
内存之前运行。
注意不要混用分配/释放function。 无论你的
malloc()
需要什么free()
d,什么是分配new
需求delete
,如果你new []
你必须delete []
。 任何其他组合是UB。
在任何情况下,在C ++中都不鼓励使用内存所有权的“裸”指针。 你也应该
-
在构造函数中添加
new
,并在类的析构函数中delete
相应的delete
,使对象成为内存的所有者(包括在对象超出范围时正确解除分配,例如在例外的情况下)。 要么 -
使用一个智能指针 ,有效地为您做上述。
(*):已知实现定义“扩展”types,malloc()不考虑其alignment要求。 实际上,我不确定语言律师是否仍然称他们为“POD”。 例如,MSVC在malloc()上执行8字节alignment ,但将SSE扩展types__m128
定义为具有16字节的alignment要求。
围绕严格别名的规则可能相当棘手。
严格别名的一个例子是:
int a = 0; float* f = reinterpret_cast<float*>(&a); f = 0.3; printf("%d", a);
这是一个严格的锯齿违规,因为:
- variables的生命周期(及其使用)重叠
- 他们正在通过两个不同的“镜头”来解读相同的记忆片段
如果你不是同时做两个 ,那么你的代码不会违反严格的别名。
在C ++中,对象的生命周期在构造函数结束时开始,在析构函数启动时停止。
在内置types(无析构函数)或POD(简单析构函数)的情况下,规则是当内存被覆盖或释放时,它们的生存期结束。
注意:这是专门用来支持写内存pipe理器的; 毕竟malloc
是用C编写的, operator new
是用C ++编写的,而且明确允许它们共享内存。
我特别使用镜头而不是types,因为规则有点困难。
C ++通常使用名义types :如果两种types有不同的名称,它们是不同的。 如果你访问一个dynamictypesT
的值就好像它是一个U
,那么你违反了别名。
这个规则有一些例外:
- 由基类访问
- 在POD中,作为指针访问第一个属性
而最复杂的规则与C ++转向结构types的 union
相关:如果只访问这段内存的开始部分,而这两个types共享一个公共初始值,则可以通过两种不同的types访问一部分内存序列。
§9.2/ 18如果一个标准布局联合包含两个或更多共享一个公共初始序列的标准布局结构,并且如果标准布局联合对象当前包含这些标准布局结构中的一个,则允许检查公共其中任何一个的初始部分。 如果相应的成员具有布局兼容的types,并且两个标准布局结构都共享一个公共的初始序列,并且这两个成员都不是位域,或者都是一个或多个初始成员的序列具有相同宽度的位域。
鉴于:
-
struct A { int a; };
-
struct B: A { char c; double d; };
-
struct C { int a; char c; char* z; };
在union X { B b; C c; };
union X { B b; C c; };
你可以同时访问xba
, xbc
和xca
, xcc
; 但是,如果当前存储的types不是B
(分别不是C
),则访问xbd
(分别是xcz
)会违反锯齿。
注意:非正式地说,结构化types就像将types映射到其字段的元组(将其扁平化)。
注意: char*
是专门免除这个规则的,你可以通过char*
来查看任何一块内存。
在你的情况下,没有Data
的定义,我不能说是否可以违反“镜头”规则,但是因为你是:
- 用
Data
重写内存之前,通过Data*
- 之后不通过
int*
访问它
那么你就遵守了终生规则,因此就语言而言不会发生任何混淆。
只要内存一次只用于一件事情,这是安全的。 你基本上使用分配的数据作为一个union
。
如果你想使用内存作为类的实例,而不仅仅是简单的C风格的结构或数据types,你必须记住做“分配”对象的位置,因为这实际上会调用对象的构造函数。 当您完成对象时,必须显式调用析构函数,但不能delete
其delete
。
只要你只处理“C”型,这就好了。 但是,只要你使用C ++类,你将会遇到麻烦,正确的初始化。 如果我们假设Data
将是std::string
例如,代码将是非常错误的。
编译器不能真正将存储移动到printf
的调用中,因为这是一个可见的副作用。 其结果必须是按照程序规定的顺序产生副作用。
实际上,您已经在malloc
/ free
之上实现了自己的分配器,在这种情况下重用了一个块。 这是完全安全的。 分配器包装器可以肯定地重用块,只要块足够大并且来自保证足够alignment的源(和malloc
)。
只要Data
仍然是POD,这应该没问题。 否则,你将不得不切换到新的位置。
然而,我会把一个静态断言,以便在以后的重构过程中不会改变
我没有发现任何重复使用内存空间的错误。 只有我所关心的是悬而未决的参考。 像你说的那样重复使用内存空间,我认为这对程序没有任何影响。
你可以继续你的编程。 但是最好free()
空间然后分配给另一个variables。