&((struct name*)NULL - > b)是否会在C11中导致未定义的行为?

M.M*_*M.M 57 c offsetof language-lawyer c11

代码示例:

struct name
{
    int a, b;
};

int main()
{
    &(((struct name *)NULL)->b);
}
Run Code Online (Sandbox Code Playgroud)

这是否会导致未定义的行为?我们可以辩论它是否"取消引用无效",但是C11没有定义术语"解除引用".

6.5.3.2/4明确指出*在空指针上使用会导致未定义的行为; 但它并没有说同样的->,也没有定义a -> b(*a).b; 它为每个运营商分别定义.

->6.5.2.3/4中的语义说:

后缀表达式后跟 - >运算符和标识符指定结构或联合对象的成员.该值是第一个表达式指向的对象的指定成员的值,并且是左值.

但是,NULL并没有指向一个对象,所以第二句似乎没有说明.

相关的可能是6.5.3.2/1:

约束:

一元运算&符的操作数应该是函数指示符,[]一元或一元运算*符的结果 ,或者是一个左值,它指定一个不是位字段的对象,并且不用寄存器存储类说明符声明.

但是我觉得粗体文本是有缺陷的并且应该读取可能指定对象的左值,按照6.3.2.1/1(左值的定义) - C99弄乱了左值的定义,所以C11必须重写它,也许这个部分错过了.

6.3.2.1/1确实说:

左值是一个表达式(对象类型不是void)可能指定一个对象; 如果左值在评估时未指定对象,则行为未定义

&操作员确实评估了它的操作数.(它不访问存储的值,但这是不同的).

这种长期推理似乎表明代码会导致UB,但它相当脆弱,我不清楚标准的作者是什么意图.如果事实上他们打算做任何事情,而不是让我们讨论:)

Ser*_*sta 23

从律师的角度来看,表达式&(((struct name *)NULL)->b);应该导致UB,因为你找不到没有UB的路径.恕我直言,根本原因是,您在某个时刻将->运算符应用于不指向对象的表达式.

从编译器的角度来看,假设编译器程序员没有过于复杂,很明显表达式会返回相同的值offsetof(name, b),我很确定如果编译时没有错误,任何现有的编译器都会给出结果.

如上所述,我们不能责怪编译器会注意到在内部部分中,您->对表达式使用运算符而不是指向对象(因为它为空)并发出警告或错误.

我的结论是,直到有一个特殊的段落说,只要它只是取其地址是合法的,取消引用空指针,这个表达式不合法C.

  • `offsetof`是一个特定于实现的宏.无论它做什么都是特定于提供它的编译器.您无法将其对一个或多个编译器的定义概括为语言要求.标准头文件有很多东西,一般没有明确定义的行为; 这些东西依赖于特定编译器的知识,这就是它们随编译器一起提供的原因. (6认同)
  • 或者他们将它用作红旗,意思是"无法达到此代码".您知道,对于优化:最快的代码是不存在的代码. (3认同)
  • @Deduplicator:我真的很鄙视"优化"的概念; 它对整数算术特别可怕(恕我直言,标准委员会应该缩小整数溢出的允许行为列表,其中包括变量采用任意或"不可能"的值,但不是完整的UB)但指针访问可能不好同样.虽然在发生空指针解除引用时标准允许不受约束的UB是完全合理的,但是一些实现可以指定发生特定行为.一些嵌入式系统上下文定义了...... (2认同)
  • ...空指针解除引用作为访问物理地址零,并且在某些此类系统上没有其他方法来访问该地址.即使标准没有指定空指针取消引用的行为,系统的文档也可能.在这种情况下,尝试使用未定义行为"变得聪明"的编译器可能最终"优化"其行为将由其他编译器针对相同硬件平台有用地定义的代码. (2认同)
  • @supercat可能存在地址为零的系统,空指针不指向地址0.您仍然可以通过形成指向地址1的指针并递减它来访问地址零. (2认同)

Jen*_*edt 17

是的,这种使用->具有未定义的行为,直接意义上的英文术语undefined.

仅当第一个表达式指向一个对象而未定义(= undefined)时,才会定义该行为.一般来说,你不应该在术语undefined中搜索更多,这意味着:标准没有为你的代码提供意义.(有时它明确指出它没有定义的情况,但这并不会改变该术语的一般含义.)

这是为了帮助编译器构建者处理事情而引入的松弛.他们可能会定义一种行为,即使对于您呈现的代码也是如此.特别是,对于编译器实现,对offsetof宏使用这样的代码或类似代码是完全正确的.使此代码成为约束违规将阻止编译器实现的路径.

  • @MattMcNabb,我也是,因为这个案例是*"行为,在使用本国际标准没有要求的非便携式程序结构时"*.这个"未定义的行为"一词不应该神秘化,它代表着它自己. (4认同)

250*_*501 10

让我们从间接运算符开始*:

6.5.3.2 p4:一元*运算符表示间接.如果操作数指向函数,则结果是函数指示符; 如果它指向一个对象,则结果是指定该对象的左值.如果操作数的类型为"指向类型的指针",则结果的类型为"type".如果为指针分配了无效值,*则未定义一元运算符的行为.102)

*E,其中E是空指针,是未定义的行为.

有一个脚注说:

102)因此,&*E等效于E(即使E是空指针),和(E1 [E2])到((E1)+(E2)).如果E是函数指示符或左值是一元&运算符的有效操作数,则总是如此,*&E是函数指示符或等于E的左值.如果*P是左值,则T是名称对象指针类型,*(T)P是左值,其类型与T指向的类型兼容.

这意味着定义了E为NULL的&*E,但问题是对于&(*E).m是否也是如此,其中E是空指针,其类型是具有成员m的结构?

C Standard没有定义该行为.

如果定义了,将出现新的问题,其中一个列在下面.C标准是正确的,以保持未定义,并提供一个宏内置处理问题的宏偏移量.

6.3.2.3指针

  1. 值为0的整型常量表达式或类型为void*的表达式称为空指针常量.66)如果将空指针常量转换为指针类型,则保证将结果指针(称为空指针)与指向任何对象或函数的指针进行比较.

这意味着值为0的整型常量表达式将转换为空指针常量.

但是,空指针常量的值未定义为0.该值是实现定义的.

7.19通用定义

  1. 宏是NULL,它扩展为实现定义的空指针常量

这意味着C允许一个实现,其中空指针将具有一个值,其中所有位都已设置,并且对该值使用成员访问将导致溢出,这是未定义的行为

另一个问题是你如何评价&(*E).m?括号是否适用并首先*进行评估.保持未定义可以解决这个问题.


Ded*_*tor 5

首先,让我们确定我们需要一个指向对象的指针:

6.5.2.3 结构和工会成员

4 后缀表达式后跟->运算符和标识符,指定结构或联合对象的成员。该值是第一个表达式指向的对象的命名成员的值,并且是一个左值。96) 如果第一个表达式是一个指向限定类型​​的指针,则结果具有指定成员。

不幸的是,从来没有空指针指向一个对象。

6.3.2.3 指针

3 值为 0 的整数常量表达式,或转换为 type 的此类表达式 void *,称为空指针常量.66) 如果将空指针常量转换为指针类型,则保证生成的指针称为空指针比较不等于指向任何对象或函数的指针

结果:未定义的行为。

作为旁注,还有一些其他需要咀嚼的东西:

6.3.2.3 指针

4 将空指针转换为另一种指针类型会产生该类型的空指针。任何两个空指针比较相等。
5 整数可以转换为任何指针类型。除了前面指定的,结果是实现定义的,可能没有正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示。67)
6 任何指针类型都可以转换为整数类型。除了前面指定的之外,结果是实现定义的。如果结果不能以整数类型表示,则行为未定义。结果不必在任何整数类型的值范围内。

67) 将指针转换为整数或将整数转换为指针的映射函数旨在与执行环境的寻址结构保持一致。

所以,即使UB碰巧是良性的这个时候,它仍然可能导致一些完全意想不到的数量。