【问题标题】:Is it allowed to write an instance of Derived over an instance of Base?是否允许在 Base 实例上编写 Derived 实例?
【发布时间】:2012-02-25 09:42:14
【问题描述】:

说,代码

    class Derived: public Base {....}

    Base* b_ptr = new( malloc(sizeof(Derived)) ) Base(1);
    b_ptr->f(2);
    Derived* d_ptr = new(b_ptr) Derived(3);
    b_ptr->g(4);
    d_ptr->f(5);

似乎合理,LSP满意。

我怀疑当 Base 和 Derived 是 POD 时,此代码是标准允许的,否则不允许(因为 vtbl ptr 被覆盖)。我的问题的第一部分是:请指出这种覆盖的精确前提。

可能存在其他标准允许的重写方式。

我的问题的第二部分是:还有其他方法吗?它们的确切前提是什么?

更新:我不想写这样的代码;我对这种代码的理论可能性(或不可能)感兴趣。所以,这是“标准的纳粹”问题,而不是“我怎么能……”的问题。 (我的问题要转移到其他 stackoverflow 网站吗?)

UPDATE2&4:析构函数呢?此代码的假定语义是“基本实例(破坏性地)由派生实例的切片更新”。为简单起见,让我们假设 Base 类有一个简单的析构函数。

UPDATE3:对我来说最有趣的是通过b_ptr->g(4)访问的有效性

【问题讨论】:

  • 您实际上想要完成什么?在不知情的情况下很难提出替代方案。
  • 亲爱的主,这代码太可怕了......
  • b_ptr->g(4) 是UB,因为在第二个new 之后,原始对象*b_ptr 不再存在。要获得一个新的基指针,你必须说类似static_cast<Base*>(d_ptr)
  • 是的,你至少应该使用 new 和 delete 运算符,而不是 malloc 和 free 函数。
  • @Serge:他正在使用new 运算符。如果你想说他应该使用void* operator new(size_t) 分配函数,那与new 运算符完全不同。

标签: c++ memory overwrite language-lawyer


【解决方案1】:

你真的需要在 Derived 的placement-new 之后执行b_ptr = d_ptr,以防Base 子对象不是Derived 布局中的第一个。正如所写,b_ptr->g(4) 会引发未定义的行为。

规则(3.8basic.life):

如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向该对象的指针原始对象、引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可以 用于操作新对象,如果

  • 新对象的存储恰好覆盖了原始对象占用的存储位置, 和
  • 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且
  • 原始对象的类型不是 const 限定的,并且,如果是类类型,则不包含任何非静态 类型为 const 限定或引用类型的数据成员,并且
  • 原始对象是 T 类型的最衍生对象 (1.8),而新对象是 T 类型的最衍生对象(也就是说,它们不是基类子对象 )。

您还应该在重用其内存之前销毁旧对象,但标准并没有强制要求这样做。但是,如果不这样做会泄漏旧对象拥有的任何资源。完整规则在标准的第 3.8 节 (basic.life) 中给出:

程序可以通过重用对象占用的存储空间或通过显式调用具有非平凡析构函数的类类型对象的析构函数来结束任何对象的生命周期。对于具有非平凡析构函数的类类型的对象,在重用或释放对象占用的存储空间之前,程序不需要显式调用析构函数;但是,如果没有明确调用 析构函数或如果不使用删除表达式 (5.3.5) 来释放存储,则不应隐式调用析构函数,并且 任何依赖于析构函数产生的副作用的程序都有未定义的行为.

【讨论】:

  • @DeadMG:我在那里得到了标准报价,另有说明。这是有条件的未定义行为。
  • 这不是强制性的——文本就在那里:“程序不需要调用析构函数”。它所说的是 UB 假设无论如何都会以某种方式神奇地调用析构函数。
  • @user1123502:不,这是不合法的。当新旧类型具有共同的基类但不是同一类型时,在创建新对象后使用指针是明确禁止的。请参阅上面的粗体文本。
【解决方案2】:

您正在对同一块内存进行两次初始化。结局不会很好。

例如,假设Base 构造函数分配了一些内存并将其存储在一个指针中。第二次通过构造函数,第一个指针会被覆盖,内存泄露。

【讨论】:

  • 当然,rewrite 的意思是“我们不会调用析构函数”。我更正了我的问题。
【解决方案3】:

我认为允许覆盖。

如果是我,我可能会在重用存储之前调用Base::~Base,这样原始对象的生命周期就会干净利落地结束。但该标准明确允许您在不调用析构函数的情况下重用存储。

我不相信您通过 b_ptr 的访问是有效的。 Base 对象的生命周期已结束。

(有关生命周期的规则,请参见任一标准中的 3.8/4。)

而且我也不完全相信 b_ptr 必须提供与 malloc() 调用最初返回的地址相同的地址。

【讨论】:

  • 在第一个位置 new 中,b_ptr 的地址与从 malloc 返回的地址相同(它不是数组,因此禁止填充)。
  • @Ben 我同意不允许使用填充。我不完全确定将 Base * 转换为 void * 一定会给你一个指向 Base 对象第一个字节的指针,但它当然在这里。
【解决方案4】:

如果你把这段代码写得更干净,更容易看出哪里出了问题:

void * addr = std::malloc(LARGE_NUMBER);

Base * b = new (addr) Base;
b->foo();                    // no problem

Derived * d = new (addr) Derived;
d->bar();                    // also fine  (#1)

b->foo();                    // Error! b no longer points to a Base!

static_cast<Base*>(d)->foo(); // OK
b = d; b->foo();              // also OK

问题在于,在标记为 (#1) 的行中,bd 指向完全独立的、不相关的事物,并且由于您覆盖了以前的对象 *b 的内存,因此 b 在事实不再有效。

您可能对Base*Derived* 是可转换指针类型有一些误导的想法,但这与目前的情况无关,为了这个例子,这两种类型可能完全不相关.当我们执行实际转换时,我们使用Derived* 可转换为Base* 这一事实只是最后两行之一。但请注意,这种转换是真正的 转换,并且d static_cast&lt;Base*&gt;(d) 不是同一个指针(至少就语言而言)。

最后,让我们收拾一下这个烂摊子:

d->~Derived();
std::free(addr);

破坏原始*b的机会已经过去,所以我们可能已经泄露了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-05
    • 1970-01-01
    • 2018-08-23
    • 1970-01-01
    • 2020-08-08
    • 2017-07-01
    相关资源
    最近更新 更多