(我编辑了这个问题是为了避免干扰。在任何其他问题有意义之前,有一个核心问题需要澄清。对现在的答案似乎不太重要的任何人道歉。)
让我们设置一个具体的例子:
struct Base {
int i;
};
没有虚方法,也没有继承,通常是一个非常愚蠢和简单的对象。因此它是Plain Old Data (POD)并且它依赖于可预测的布局。特别是:
Base b;
&b == reinterpret_cast<B*>&(b.i);
这是根据Wikipedia(它本身声称参考C ++ 03标准):
指向POD结构对象的指针,使用重新解释转换适当转换,指向其初始成员,反之亦然,这意味着在POD结构的开头没有填充。[8]
现在让我们考虑一下继承:
struct Derived : public Base {
};
同样,没有虚方法,没有虚继承,也没有多重继承。因此这也是POD。
问题:这个事实(Derived是C ++ 11中的POD)是否允许我们这样说:
Derived d;
&d == reinterpret_cast<D*>&(d.i); // true on g++-4.6
如果这是真的,那么下面的定义很明确:
Base *b = reinterpret_cast<Base*>(malloc(sizeof(Derived)));
free(b); // It will be freeing the same address, so this is OK
我不是在这里询问new
和delete
- 更容易考虑malloc
和free
。在这样的简单情况下,我只是对有关派生对象布局的规定感到好奇,并且基类的初始非静态成员位于可预测的位置。
派生对象应该等同于:
struct Derived { // no inheritance
Base b; // it just contains it instead
};
事先没有填充?
答案 0 :(得分:14)
你不关心POD-ness,你关心标准布局。这是定义,来自标准部分9 [class]
:
标准布局类是一个类:
- 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
- 没有虚函数(10.3),没有虚基类(10.1),
- 对所有非静态数据成员具有相同的访问控制(第11条),
- 没有非标准布局基类
- 在大多数派生类中没有非静态数据成员,并且最多只有一个具有非静态数据成员的基类,或者没有包含非静态数据成员的基类,并且
- 没有与第一个非静态数据成员相同类型的基类。
然后保证您想要的属性(第9.2节[class.mem]
):
指向标准布局结构对象的指针(适当地使用
reinterpret_cast
转换)指向其初始成员(或者如果该成员是位字段,则指向它所在的单元)和副指针反之亦然。
这实际上比旧的要求更好,因为添加非平凡的构造函数和/或析构函数不会丢失reinterpret_cast
的能力。
现在让我们转到第二个问题。答案不是你所希望的。
Base *b = new Derived;
delete b;
除非Base
具有虚拟析构函数,否则是未定义的行为。见5.3.5([expr.delete]
)
在第一个备选(删除对象)中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类和静态类型type应具有虚拟析构函数或行为未定义。
使用malloc
和free
的早期代码段大部分都是正确的。这将有效:
Base *b = new (malloc(sizeof(Derived))) Derived;
free(b);
因为指针b
的值与从展示位置new返回的地址相同,而该地址又是从malloc
返回的相同地址。
答案 1 :(得分:2)
大概你的最后一段代码是:
Base *b = new Derived;
delete b; // delete b, not d.
在这种情况下,简短的回答是它仍然是未定义的行为。有问题的类或结构是POD,标准布局或平凡可复制的事实并没有真正改变任何东西。
是的,你传递了正确的地址,是的,你和我知道在这种情况下,dtor几乎是一个小鸟 - 尽管如此,你指向的传感器{{1}具有与动态类型不同的静态类型,静态类型没有虚拟dtor。标准很清楚,这给出了未定义的行为。
从一个实际的角度来看,如果你真的坚持的话,你可能会侥幸逃脱UB--很有可能因为你所做的事情不会产生任何有害的副作用,至少最典型的编译器。但是要注意,即使最好的代码也非常脆弱,所以看似微不足道的变化可能会破坏一切 - 甚至切换到具有非常繁重的类型检查的编译器,而也可以这样做。
就你的观点而言,情况非常简单:它基本上意味着委员会可能可能如果他们愿意那样做出这种定义的行为。然而,据我所知,它从来没有被提出过,即使它有可能是一个非常低优先级的项目 - 它并没有真正增加太多,启用新的编程风格等
答案 2 :(得分:2)
这是Ben Voigt's answer'的补充,而不是替代。
您可能认为这只是技术性问题。称之为'undefined'的标准只是一些语义琐事,除了允许编译器编写者无缘无故地做愚蠢的事情之外没有任何实际效果。但事实并非如此。
我可以看到理想的实现:
Base *b = new Derived;
delete b;
导致这种行为非常奇怪。这是因为当编译器静态地知道你分配的内存块的大小是有点傻。例如:
struct Base {
};
struct Derived {
int an_int;
};
在这种情况下,当调用delete Base
时,编译器完全有理由(因为您在问题开头引用的规则)认为指向的数据大小为1而不是4例如,如果它实现了operator new
的一个版本,它有一个单独的数组,其中1个字节的实体都是密集的,而另一个数组中4个字节的实体都是密集的,它最终会假设Base *
指向1字节实体数组中的某处,实际上它指向4字节实体数组中的某处,并因此而产生各种有趣的错误。
我真的希望operator delete
被定义为也采用一个大小,并且如果在具有非虚拟析构函数的对象上调用operator delete
,或者编译器传入静态已知大小,或者如果由于virtual
析构函数而被调用的实际对象的已知大小。虽然这可能会产生其他不良影响,也许并不是一个好主意(例如,如果在没有调用析构函数的情况下调用operator delete
的情况)。但这会让问题变得非常明显。
答案 3 :(得分:0)
上面对无关的问题进行了大量讨论。是的,主要针对C兼容性,只要您知道自己在做什么,就可以依赖许多保证。然而,这一切与您的主要问题无关。主要问题是:是否存在使用指针类型删除对象的情况,该指针类型与对象的动态类型不匹配,并且指向的类型没有虚拟析构函数。答案是:不,没有。
这个逻辑可以从运行时系统应该做的事情中得到:它获取一个指向对象的指针并被要求删除它。它需要存储有关如何调用派生类析构函数的信息,或者存储对象实际需要的内存量(如果要定义)。然而,就使用的存储器而言,这意味着可能相当大的成本。例如,如果第一个成员需要非常严格的对齐,例如,要像double
那样在8字节边界处对齐,添加大小会增加至少8个字节的开销来分配内存。虽然这可能听起来不太糟糕,但这可能意味着只有一个对象而不是两个或四个对象进入缓存行,从而大大降低了性能。