首页 文章

C中的POD和继承 . 结构的地址=第一个成员的地址吗?

提问于
浏览
7

(我编辑了这个问题是为了避免干扰 . 在任何其他问题有意义之前,有一个核心问题需要澄清 . 对现在的答案似乎不太重要的任何人道歉 . )

让我们 Build 一个具体的例子:

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 .

问题:这个事实(衍生的是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

我不是在这里询问 newdelete - 更容易考虑 mallocfree . 在这种简单的情况下,我只是对有关派生对象布局的规定感到好奇,并且基类的初始非静态成员位于可预测的位置 .

Derived对象应该等效于:

struct Derived { // no inheritance
    Base b; // it just contains it instead
};

事先没有填充?

4 回答

  • 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 的情况) . 但这会让问题变得非常明显 .

  • 14

    关于上述无关问题,有很多讨论 . 是的,主要针对C兼容性,只要您知道自己在做什么,就可以依赖许多保证 . 然而,这一切与您的主要问题无关 . 主要问题是:是否存在使用指针类型删除对象的情况,该指针类型与对象的动态类型不匹配,并且指向的类型没有虚拟析构函数 . 答案是:不,没有 .

    这个逻辑可以从运行时系统应该做的事情中得到:它获取一个指向对象的指针并被要求删除它 . 它需要存储有关如何调用派生类析构函数的信息,或者存储对象实际需要的内存量(如果要定义) . 然而,这意味着在使用的存储器方面可能相当大的成本 . 例如,如果第一个成员需要非常严格的对齐,例如,要像 double 那样在8字节边界处对齐,添加一个大小会增加至少8个字节的开销来分配内存 . 虽然这可能听起来不太糟糕,但这可能意味着只有一个对象而不是两个或四个适合缓存行,从而大大降低了性能 .

  • 0

    大概你最后一段代码的意思是:

    Base *b = new Derived;
    delete b;  // delete b, not d.
    

    在这种情况下,简短的回答是它仍然是未定义的行为 . 有问题的类或结构是POD,标准布局或平凡可复制的事实并没有真正改变任何东西 .

    是的,你're passing the right address, and yes, you and I know that in this case the dtor is pretty much a nop -- nonetheless, the pointer you'传递给 delete 的静态类型与动态类型不同,静态类型没有虚拟dtor . 标准很清楚,这给出了未定义的行为 .

    从一个实际的角度来看,如果你真的坚持的话,你可能可以逃离UB--至少对于大多数典型的编译器来说,赢得胜利的机会非常好 . [181]但是要注意,即使最好的代码也是非常脆弱的,所以看似微不足道的变化可能会破坏一切 - 甚至切换到具有非常繁重的类型检查的编译器,这样做也可以 .

    就你的论点而言,情况非常简单:它基本上意味着委员会可能会根据需要制定这种定义的行为 . 据我所知,它确实增加了很多,启用了新的编程风格等等 .

  • 2

    你不关心POD-ness,你关心标准布局 . 这是定义,来自标准部分9 [class]

    标准布局类是一个类:没有非标准布局类(或此类类型的数组)或引用的非静态数据成员,没有虚函数(10.3)且没有虚基类(10.1) ),对所有非静态数据成员具有相同的访问控制(第11条),没有非标准布局基类,在最派生类中没有非静态数据成员,并且最多只有一个非基类-static数据成员,或者没有具有非静态数据成员的基类,并且没有与第一个非静态数据成员相同类型的基类 .

    然后保证你想要的 property (第9.2节 [class.mem] ):

    指向标准布局结构对象的指针,使用reinterpret_cast进行适当转换,指向其初始成员(或者如果该成员是位字段,则指向它所在的单位),反之亦然 .

    这实际上比旧的要求更好,因为通过添加非平凡的构造函数和/或析构函数不会丢失 reinterpret_cast 的能力 .


    现在让我们转到你的第二个问题 . 答案不是你所希望的 .

    Base *b = new Derived;
    delete b;
    

    是未定义的行为,除非 Base 具有虚拟析构函数 . 见5.3.5( [expr.delete]

    在第一个备选(删除对象)中,如果要删除的对象的静态类型与其动态类型不同,则静态类型应为要删除的对象的动态类型的基类,静态类型应为有一个虚拟析构函数或行为未定义 .


    您使用 mallocfree 的早期代码段大部分都是正确的 . 这将有效:

    Base *b = new (malloc(sizeof(Derived))) Derived;
    free(b);
    

    因为指针 b 的值与放置new的地址相同,而new又是从 malloc 返回的相同地址 .

相关问题