【问题标题】:Base pointer to array of derived objects指向派生对象数组的基指针
【发布时间】:2011-11-04 02:09:21
【问题描述】:

根据今天早些时候在这里提出的question 和许多类似主题的问题,我在这里从 stadard 的角度询问这个问题。

struct Base
{
  int member;
};

struct Derived : Base
{
  int another_member;
};

int main()
{
  Base* p = new Derived[10]; // (1)
  p[1].member = 42; // (2)
  delete[] p; // (3)
}

按照标准(1)是良构的,因为Dervied*(这是new-expression的结果)可以隐式转换为Base*(C++11草案, §4.10/3):

类型为“pointer to cv D”的纯右值,其中 D 是类类型,可以是 转换为“指向 cv B 的指针”类型的纯右值,其中 B 是基数 D 的类(第 10 条)。如果 B 是不可访问的(第 11 条)或 D 的模棱两可(10.2)基类,一个需要这个的程序 转换是不正确的。转换的结果是一个指向 派生类对象的基类子对象。空指针 value 被转换为目标类型的空指针值。

(3) 由于 §5.3.5/3 导致未定义的行为:

在第一种选择(delete object)中,如果 要删除的对象与其动态类型不同,静态 type 应该是对象的动态类型的基类 已删除且静态类型应具有虚拟析构函数或 行为未定义。在第二种选择(删除数组)中,如果 要删除的对象的动态类型与其静态类型不同, 行为未定义。

(2) 根据标准是否合法,还是会导致程序格式错误或未定义行为?

编辑:更好的措辞

【问题讨论】:

  • 为什么我们假设 (2) 格式不正确?
  • (2) 的格式非常错误,因为它使用 sizeof(Base) 来计算 p[0] 和 p[1] 之间的距离。
  • 它不是格式错误的,它只是 UB 因为 p 不指向数组对象的元素(指针运算工作的条件),它指向的基类子对象数组元素,因此数组访问无效。
  • @Kerrek SB:也许最后一个问题的措辞应该稍有不同,但由于主要实现(用 gcc、clang 和 MSVC 测试)没有正确,我假设 (2) 格式不正确。我花了两个小时搜索类似 Bo Persson 所说的内容,即 (p + n) 使用 p 的静态类型来计算偏移量,但我觉得关于 operator+ 的段落并不暗示这一点。
  • @Charles Bailey:哦,这确实有道理。请务必将其发布为答案。

标签: c++ standards


【解决方案1】:

如果您查看表达式 p[1]pBase*Base 是完全定义的类型)而 1int,因此根据 ISO/IEC 14882: 2003 5.2.1 [expr.sub] 此表达式有效且与*((p)+(1)) 相同。

从5.7 [expr.add] / 5开始,当一个整数被加到一个指针上时,只有当指针指向一个数组对象的一个​​元素并且指针运算的结果也指向一个该数组对象的元素或数组末尾的元素。但是,p 不指向数组对象的元素,它指向Derived 对象的基类子对象。数组成员是 Derived 对象,而不是 Base 子对象。

请注意,在 5.7 / 4 下,对于加法运算符,Base 子对象可以被视为大小为 1 的数组,因此从技术上讲,您可以形成地址 p + 1,但作为“越过最后一个元素”指针,它不指向Base 对象,尝试读取或写入它会导致未定义的行为

【讨论】:

  • 我想知道您是否愿意对a similar question I asked 进行权衡,其中DerivedBase 已经拥有的之上没有添加任何数据成员。它得到了一些答案,但我需要帮助判断什么是正确的。
  • @RobKennedy:我重新阅读了我的答案,我不明白为什么你认为它只适用于sizeof(Derived) != sizeof(Base)
【解决方案2】:

(3) 会导致未定义的行为,但严格来说它不是格式错误的。 格式错误表示C++程序不是按照语法规则、可诊断语义规则和单一定义规则构造的。

与 (2) 相同,它的格式正确,但并没有达到您的预期。根据§8.3.4/6:

除了已为类(13.5.5)声明的情况外,下标运算符 [] 以这种方式解释 E1[E2] 等同于 *((E1)+(E2))。由于适用于 + 的转换规则,如果 E1 是 数组和 E2 一个整数,那么 E1[E2] 指的是 E1 的第 E2 个成员。因此,尽管它不对称 看起来,下标是一种交换操作。

所以在 (2) 中,当您可能想要获得地址 p+sizeof(Derived)*1 时,您将获得地址,它是 p+sizeof(Base)*1 的结果。

【讨论】:

  • 我真傻,我读的是未定义的,写的是格式错误的。已修复,谢谢。
  • 问题是,§5.7/5 似乎并不暗示 (char*)(p + 1) == (char*)p + sizeof(Base) - 除非我读错了。我认为这就是我的问题的重点。
【解决方案3】:

标准并没有禁止 (2),但它仍然很危险。

问题在于,执行p[1] 意味着将sizeof(Base) 添加到基地址p,并将该内存位置的数据用作Base 的实例。但是sizeof(Base) 小于sizeof(Derived) 的可能性非常高,因此您将从Derived 对象中间开始的内存块解释为Base 对象。

C++ FAQ Lite 21.4 中的更多信息。

【讨论】:

    【解决方案4】:
    p[1].member = 42; 
    

    格式正确。 p 的静态类型是 Derived,动态类型是 Basep[1] 等价于 *(p+1),这似乎是有效的,并且是指向数组中动态类型 Base 的第一个元素的指针。

    然而,*(p+1) 实际上是指Derived 类型的数组成员。代码 p[1].member = 42; 表明您认为您指的是类型为 Base 的数组成员。

    【讨论】:

      猜你喜欢
      • 2011-11-04
      • 2014-06-16
      • 1970-01-01
      • 1970-01-01
      • 2021-05-20
      • 1970-01-01
      • 2012-02-05
      • 2012-10-14
      相关资源
      最近更新 更多