基类指针指向派生对象数组

12

在今天早些时候从这里提出的一个问题以及众多类似主题的问题后,我想从标准的角度来讨论这个问题。

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):

类型为“指向cv D”的prvalue可以转换为类型为“指向cv B”的prvalue, 其中B是D的基类(Clause 10)。 如果B是D的不可访问(Clause 11)或 有歧义的(10.2)基类,则需要此转换的程序是不良构的。 转换的结果是指向派生类对象的基类子对象的指针。 空指针值被转换为目标类型的空指针值。

(3)由于§5.3.5 / 3存在未定义的行为,因此会导致未定义的行为:

在第一种情况下(删除对象),如果要删除的对象的静态类型与其动态类型不同, 则静态类型必须是要删除的对象的动态类型的基类,且静态类型必须具有虚拟析构函数,否则行为是未定义的。 在第二种情况下(删除数组),如果要删除的对象的动态类型与其静态类型不同,则行为是未定义的。

根据标准,(2)是否合法?会导致良构程序还是未定义的行为?


为什么我们假设(2)是不正确的形式? - Kerrek SB
5
这段话的意思是:它并不是格式错误,只是由于p指向的不是数组对象的元素(指针算术运算有效的条件),而是指向数组元素的基类子对象,所以数组访问是无效的。 - CB Bailey
1
@Kerrek SB:也许最后一个问题应该用不同的措辞,但是由于主要实现(使用gcc、clang和MSVC测试)都没有做对,我“假设”(2)是非法的。我花了最近两个小时搜索类似Bo Persson所说的东西,即(p + n)使用p的静态类型来计算偏移量,但我有一种感觉,关于operator+的段落并没有暗示这一点。 - Vitus
@Charles Bailey:噢,那确实有道理。请把它发布为答案。 - Vitus
抱歉,我现在没有访问标准的权限,但是在指针和整数应用加法运算符时,有一个“否则行为未定义”的部分描述了要求。(如果我没记错的话) - CB Bailey
显示剩余3条评论
4个回答

5
如果您看一下表达式p[1],那么p是一个Base*Base是一个完全定义的类型),而1是一个int,所以根据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对象,尝试从中读取或写入将导致未定义的行为

我想知道你是否愿意对我之前提出的一个类似问题发表一下看法(链接为https://dev59.com/LGIj5IYBdhLWcg3w04Up),其中`Derived`没有在`Base`已有的数据成员上添加任何数据成员。这个问题已经得到了一些答案,但我需要帮助来判断哪个是正确的。 - Rob Kennedy
1
@RobKennedy:我重新阅读了我的回答,不知道你为什么认为它只适用于 sizeof(Derived) != sizeof(Base) 的情况。 - CB Bailey

4
(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的结果。

我太傻了,我读成未定义并且写错了。已修正,谢谢。 - Vitus
问题在于,§5.7/5似乎并没有暗示(char*)(p + 1) == (char*)p + sizeof(Base) - 除非我读错了。这就是我的问题所在,我想。 - Vitus

1

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

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

更多信息请参见C++ FAQ Lite 21.4


0
p[1].member = 42; 

格式正确。变量p的静态类型为Derived,动态类型为Base。表达式p[1]等价于*(p+1),看起来是有效的,并且是指向数组中动态类型为Base的第一个元素的指针。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接