动态数组的惯用初始化是不是会调用未定义的行为?
Posted
技术标签:
【中文标题】动态数组的惯用初始化是不是会调用未定义的行为?【英文标题】:Does idiomatic initialization of a dynamic array invoke Undefined Behavior?动态数组的惯用初始化是否会调用未定义的行为? 【发布时间】:2022-01-08 02:15:02 【问题描述】:这个问题可能有点争议。 我在块范围内有以下代码:
int *a = malloc(3 * sizeof(int));
if (!a) ... error handling ...
a[0] = 0;
a[1] = 1;
a[2] = 2;
我认为这段代码调用 UB 是因为指针算术超出了界限。
原因是a
的对象指针的有效类型永远不会
设置为int[3]
,而仅设置为int
。因此,对索引处对象的任何访问
C 标准没有定义除 0 以外的值。
原因如下:
线a = malloc(...)
。
如果分配成功,则a
指向一个足以存储 3 个int
s 的区域。
a[0] = ...
等价于*a = ...
,即int
的左值。它将第一个sizeof(int)
字节的有效类型设置为int
,如规则6.5p6 中所示。
...对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。
现在指针a
指向int
类型的对象,不是 int[3]
。
a[1] = ...
等价于*(a + 1) =
。表达式a + 1
指向可通过*a
访问的int
对象末尾之后的一个元素。
该指针本身对比较有效,但访问未定义,原因是:
规则6.5.6p7:
...指向不是数组元素的对象的指针与指向长度为 1 的数组的第一个元素的指针的行为相同,该数组的元素类型为对象的类型。
和规则6.5.6p8:
...如果结果指向数组对象的最后一个元素,则不应将其用作计算的一元 * 运算符的操作数。
类似的问题与a[2] = ...
相关,但这里甚至隐藏在a[2]
中的a + 2
也会调用UB。
只要满足对齐要求和严格的别名规则,如果标准允许对内存有效区域进行任意指针运算,问题就可以解决。或者任何相同类型的连续对象的集合都可以被视为一个数组。但是,我找不到这样的东西。
如果我对标准的解释是正确的,那么一些 C 代码(全部?)将是未定义的。 因此,当我希望自己错时,这是极少数情况之一。
我是吗?
【问题讨论】:
你说得对,a
没有指向int[3]
类型的对象。一个原因是指向int[3]
的指针将具有int (*)[3]
类型,这与a
的类型非常不同。相反,它说a + i
(对于任何有效索引i
,包括0
)指向int
。
7.22.3 内存管理函数 "....然后用来访问空间中的这样一个对象或这样一个对象的数组分配......”可能是相关的。 malloc 的用法在 C 中无处不在,你想多了。
有效类型和严格的别名规则被完全破坏了,这就是一个例子。然而,关于指针运算只允许在数组中的规则同样被打破,无论何时应用于未知(有效)类型的数据块。每当对例如微控制器中的硬件寄存器映射进行指针运算时,都会遇到相同的问题。 C 标准通常不承认可以在地址空间中放置 C 编译器未放置的东西。
@Mat,是的,我想太多了,但是 language-lawyer 标签正是为了想太多。 7.22.3
的措辞看似相关,但与其他更明确的规则相矛盾。
@Mat 相反,提出有效类型规则的人都“没有考虑”这一点。它们不涉及数组/聚合类型,也不涉及类型限定符。整个 6.5 §6-§7 可以替换为“这里的实现可以随意在两行之间拼凑事物,以一种未记录的方式”。所有这些最终归结为实施质量。
【参考方案1】:
标准只是“半途而废”地定义了术语“对象”:它说每个对象都是一个存储区域,但它没有指定存储区域何时是或不是对象。对于大多数标准,可以说每个存储区域同时包含适合其中的所有类型的所有对象;任何修改对象的动作都会修改底层存储,任何修改底层存储的动作都会修改其中所有对象的存储值。
我认为很明显,标准的作者期望在标准说一个动作调用未定义行为的情况下,但在没有该声明的情况下会定义行为,质量实现应该以定义的方式表现 在他们的客户会觉得有用的情况下。然而,这些是哪些案例的问题是标准管辖范围之外的实施质量问题。因此,标准是否将某些行为描述为未定义行为并不重要,迄今为止所有实现都以相同的明显有用的方式处理了某些操作,因为没有人试图出售编译器会将标准未能强制要求这样的行为解释为以对客户有害的方式偏离它的邀请。
因为不同的编译器用于不同的目的,所以标准可以真正定义许多低级编程任务所需的所有行为,同时还允许对高端数字有用的所有优化的唯一方法处理将是识别进行不同优化的实现类别,或者添加更好的方法来邀请或阻止优化,这将有助于提高性能和/或导致不正确的程序行为。因为每个曾经存在或将可能存在的编译器都会避免进行一些原本有用的优化,和/或执行错误地处理某些严格符合 C11 程序的“优化”,标准是否允许的问题愚蠢的优化应该只适用于那些想要编写质量差的编译器,或者想要向后弯腰以与它们兼容的人。
【讨论】:
因为没有人试图出售编译器会将标准未能强制要求这样的行为视为一种偏离它的邀请...优化编译器离他们不远了,当他们利用潜在的未定义行为来生成反直观的优化并破坏未完全定义但在以前的技术状态下运行良好的现有代码。 @chqrlie:也许我应该重新放大我上面斜体的部分文本:......以任何方式偏离它会使编译器不太有用他们的客户。在大多数情况下,能够有意义地处理各种不可移植程序的编译器会比不能处理的编译器更有用。给定float *floatPtr
,一个高质量的编译器没有理由在没有一些不寻常的配置选项的情况下假设对*(unsigned*floatPtr
的访问不会访问float
类型的对象。实际上,如果一个人认识到这一原则......
...通过左值进行的访问,其地址是从一个特定类型中新可见地派生的,应被识别为 being 在后者的情况下该类型的访问将被定义,但将“新鲜可见”的含义作为实现质量问题留下,这对于程序员和大多数编译器编写者来说更可行,至少对于那些不必维护其前端编译器的人来说是这样的。结尾去掉了支持这种结构所必需的信息。
那么问题的答案是“技术 UB”的例子吗?一种 UB,所有相关/有用的 C 实现都以相同的方式定义。它看起来像是标准中的某种缺陷。
@tstanisl:该标准从未打算描述所有声称适用于任何特定目的的实现应该表现得有用的情况。它不这样做的事实并不是真正的缺陷。主要的失败是它没有明确表明它放弃了对许多正确但不可移植的程序的管辖权,并且放弃对程序行为的管辖权并不意味着任何判断该程序应该被视为“错误”或“损坏”。以上是关于动态数组的惯用初始化是不是会调用未定义的行为?的主要内容,如果未能解决你的问题,请参考以下文章
数组动态初始化时,数组元素会被赋予一个默认值,简述各数据类型的初始值?