这个问题基于一些移植到C++的现有C代码.我只是对它是否"安全"感兴趣.我已经知道我不会这样写的.我知道这里的代码基本上是C而不是C++,但它是用C++编译器编译的,我知道标准有时略有不同.
我有一个分配一些内存的功能.我把返回的东西void*投入int*并开始使用它.
后来我把返回void*到了a 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);
Run Code Online (Sandbox Code Playgroud)
我从来没有读过通过与存储类型不同的类型使用的变量,但担心编译器可能会看到它data_i并且data_d是不同的类型,因此不能在法律上相互别名并决定重新排序我的代码,例如将存储放在data_d第一个之前printf.哪会破坏一切.
然而,这是一直使用的模式.如果你在两次访问之间插入一个free,malloc我不相信它会改变任何东西,因为它不会触及受影响的内存本身,并且可以重用相同的数据.
我的代码被破坏了还是"正确"?
Nia*_*all 52
它是"OK",它就像你编写它一样工作(假设原语和普通旧数据类型(POD)).这很安全.它实际上是一个自定义内存管理器.
一些说明:
如果在分配的内存的位置创建具有非平凡析构函数的对象,请确保调用它
obj->~obj();
Run Code Online (Sandbox Code Playgroud)如果创建对象,请考虑在纯转换上放置新语法(也适用于POD)
Object* obj = new (data) Object();
Run Code Online (Sandbox Code Playgroud)检查是否nullptr(或NULL),如果malloc失败,NULL则返回
鉴于您使用的是C++编译器,除非您希望保持代码的"C"性质,否则您也可以查看全局operator new().
和往常一样,一旦完成不要忘记free()(或delete如果使用new)
你提到你还没有转换任何代码; 但是如果或者当你考虑它时,C++中有一些惯用的功能你可能希望malloc在全局甚至全局上使用::operator new.
您应该查看智能指针std::unique_ptr<>或std::shared_ptr<>允许它们处理内存管理问题.
Dev*_*lar 25
根据定义Data,您的代码可能会被破坏.这是糟糕的代码,无论哪种方式.
如果Data是普通的旧数据类型(POD,即基本类型的typedef,POD类型的结构等),并且分配的内存与类型(*)正确对齐,那么您的代码是明确定义的,意味着它将"起作用"(只要你在使用它之前初始化每个成员*data_d),但这不是好习惯.(见下文.)
如果Data是非POD类型,则会遇到麻烦:例如,指针赋值不会调用任何构造函数.data_d,它是"指向Data" 的类型,实际上会撒谎,因为它指向某个东西,但是某些东西不是类型的,Data因为没有创建/构造/初始化这种类型.那时未定义的行为就不远了.
在给定内存位置正确构造对象的解决方案称为placement new:
Data * data_d = new (data) Data();
Run Code Online (Sandbox Code Playgroud)
这指示编译器在该位置构造Data对象.这适用于POD和非POD类型.您还需要调用析构函数()以确保它在内存之前运行.datadata_d->~Data()delete
请注意不要混淆分配/释放功能.无论你
malloc()需要free()什么,分配什么new需求delete,如果new []你需要delete [].任何其他组合是UB.
在任何情况下,在C++中都不鼓励使用"裸"指针来获取内存所有权.你也应该
把new在一个构造函数和相应delete的一类的析构函数,使得对象的存储器的所有者(包括当对象超出范围适当释放,例如,在出现异常的情况下); 要么
使用智能指针,有效地为您做上述事情.
(*):已知实现定义"扩展"类型,malloc()不考虑其对齐要求.实际上,我不确定语言律师是否会称他们为"POD".例如,MSVC 在malloc()上进行8字节对齐,但将SSE扩展类型定义__m128为具有16字节对齐要求.
Mat*_* M. 16
围绕严格别名的规则可能非常棘手.
严格别名的一个例子是:
int a = 0;
float* f = reinterpret_cast<float*>(&a);
f = 0.3;
printf("%d", a);
Run Code Online (Sandbox Code Playgroud)
这是严格的别名违规,因为:
如果您没有同时执行这两项操作,那么您的代码不会违反严格的别名.
在C++中,对象的生命周期在构造函数结束时开始,在析构函数启动时停止.
在内置类型(无析构函数)或POD(普通析构函数)的情况下,规则是,只要内存被覆盖或释放,它们的生命周期就会结束.
注意:这专门用于支持编写内存管理器; 毕竟malloc用operator newC语言编写并用C++编写,并且明确允许它们汇集内存.
我特意使用镜头而不是类型,因为规则有点困难.
C++通常使用名义输入:如果两种类型具有不同的名称,则它们是不同的.如果您访问动态类型的值,T就好像它是a U,那么您违反了别名.
此规则有许多例外情况:
最复杂的规则与unionC++转向结构类型的位置有关:如果只访问这段内存开头的部分,你可以通过两种不同的类型访问一块内存,其中两种类型共享一个共同的初始序列.
§9.2/ 18如果标准布局联合包含两个或多个共享公共初始序列的标准布局结构,并且标准布局联合对象当前包含这些标准布局结构中的一个,则允许检查公共其中任何一个的初始部分.如果相应的成员具有布局兼容类型并且两个成员都不是位字段,或者两者都是具有相同宽度的位字段(对于一个或多个初始成员的序列),则两个标准布局结构共享共同的初始序列.
鉴于:
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; };可以访问x.b.a,x.b.c并且x.c.a,x.c.c在同一时间; 但是,如果当前存储的类型不是(分别不是),则x.b.d(分别x.c.z)访问是违反别名的.BC
注意:非正式地,结构类型就像将类型映射到其字段的元组(展平它们).
注意:char*特别是免除此规则,您可以通过查看任何一块内存char*.
在你的情况下,没有Data我的定义,我不能说是否可以违反"镜头"规则,但是因为你是:
Data在访问之前覆盖内存Data*int*之后那么你就符合生命周期规则,因此就语言而言,没有出现锯齿现象.
只要内存一次只用于一件事就是安全的.你基本上使用分配的数据作为union.
如果你想将内存用于类的实例而不仅仅是简单的C风格结构或数据类型,你必须记住做新的放置来"分配"对象,因为这实际上会调用对象的构造函数.当你完成对象时你必须明确调用的析构函数,你不能delete.
只要您只处理"C"类型,这就没问题.但是一旦你使用C++类,你就会遇到正确的初始化问题.如果我们假设那Data将是std::string例如,代码将是非常错误的.
编译器无法在调用中真正移动存储printf,因为这是一个可见的副作用.结果必须好像副作用是按照程序规定的顺序产生的.
实际上,在这种情况下,您已经在malloc/ free重用块之上实现了自己的分配器.那是非常安全的.只要块足够大并且来自保证足够对齐(并且malloc确实)的源,分配器包装器当然可以重用块.