7

C99 标准规定:

当两个指针相减时,都应指向同一个数组对象的元素,或者指向数组对象的最后一个元素

考虑以下代码:

struct test {
    int x[5];
    char something;
    short y[5];
};

...

struct test s = { ... };
char *p = (char *) s.x;
char *q = (char *) s.y;
printf("%td\n", q - p);

这显然违反了上述规则,因为pandq指针指向不同的“数组对象”,并且根据规则,q - p差异是未定义的。

但在实践中,为什么这样的事情会导致未定义的行为?毕竟,结构成员是按顺序排列的(就像数组元素一样),成员之间有任何潜在的填充。诚然,填充量会因实现而异,这会影响计算的结果,但为什么该结果应该是“未定义的”?

我的问题是,我们可以假设标准只是“无知”这个问题,还是有充分的理由不扩大这条规则?上述规则不能改写为“两者都应指向同一数组对象的元素或同一结构的成员”吗?

我唯一的怀疑是分段内存架构,其中成员可能最终位于不同的段中。是这样吗?

我也怀疑这就是 GCC 定义自己的原因__builtin_offsetof,以便对offsetof宏进行“符合标准”的定义。

编辑:

正如已经指出的那样,标准不允许对 void 指针进行算术运算。它是一个 GNU 扩展,仅在通过 GCC 时才会发出警告-std=c99 -pedantic。我正在用void *指针替换char *指针。

4

5 回答 5

3

char*相同结构的成员地址之间的减法和关系运算符(在 type 上)定义明确。

任何对象都可以被视为unsigned char.

引用N1570 6.2.6.1 第 4 段:

存储在任何其他对象类型的非位域对象中的值由n ×CHAR_BIT位组成,其中 n 是该类型对象的大小,以字节为单位。该值可以被复制到一个类型为 n的对象中(例如, by );结果的字节集称为值的对象表示。unsigned char [ ]memcpy

...

我唯一的怀疑是分段内存架构,其中成员可能最终位于不同的段中。是这样吗?

不可以。对于具有分段内存架构的系统,通常编译器会施加限制,即每个对象必须适合单个段。或者它可以允许占用多个段的对象,但它仍然必须确保指针算术和比较正常工作。

于 2014-11-03T16:33:23.940 回答
2

指针算术要求被添加或减去的两个指针是同一个对象的一部分,因为否则它没有意义。标准的引用部分特指两个不相关的对象,例如int a[b];int b[5]。指针算法需要知道指针指向的对象的类型(我相信你已经知道了)。

IE

int a[5];
int *p = &a[1]+1; 

这里p是通过知道&a[1]引用一个int对象并因此增加到 4 个字节(假设sizeof(int)为 4)来计算的。

来到结构示例,我认为不可能以使结构成员之间的指针算术合法的方式定义它。

我们举个例子,

struct test {
    int x[5];
    char something;
    short y[5];
};

C 标准的指针不允许使用指针算术void(编译时gcc -Wall -pedantic test.c会捕捉到这一点)。我认为您正在使用 gcc,它假定void*类似于char*并允许它。所以,

printf("%zu\n", q - p);

相当于

printf("%zu", (char*)q - (char*)p);

因为如果指针指向同一个对象并且是字符指针(char*unsigned char*),则指针算术是明确定义的。

使用正确的类型,它将是:

struct test s = { ... };
int *p = s.x;
short *q = s.y;
printf("%td\n", q - p);

现在,如何q-p执行?基于sizeof(int)sizeof(short)?如何 char something;计算这两个数组中间的大小?

这应该解释不可能对不同类型的对象执行指针算术。

即使所有成员都是同一类型(因此没有如上所述的类型问题),那么最好使用标准宏offsetof(from <stddef.h>) 来获取结构成员之间的差异,这与成员之间的指针运算具有相似的效果:

printf("%zu\n", offsetof(struct test, y) - offsetof(struct test, x));

所以我认为没有必要通过 C 标准定义结构成员之间的指针算术。

于 2014-11-03T10:47:43.877 回答
1

是的,您可以对结构字节执行指针算术运算:

N1570 - 6.3.2.3 指针 p7:

...当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。结果的连续增量,直到对象的大小,产生指向对象剩余字节的指针。

这意味着对于程序员来说,结构的字节应该被视为一个连续的区域,不管它是如何在硬件中实现的。

但不是void*指针,那是非标准的编译器扩展。如标准段落所述,它仅适用于字符类型指针。

编辑:

正如 mafso 在评论中指出的那样,只要减法结果的类型对结果ptrdiff_t有足够的范围,上述内容才是正确的。由于 range ofsize_t可以大于ptrdiff_t,并且如果结构足够大,则地址可能相距太远。

因此,最好offsetof在结构成员上使用宏并从中计算结果。

于 2014-11-03T10:27:08.690 回答
1

我相信这个问题的答案比看起来更简单,OP 问:

但为什么该结果应该是“未定义的”?

好吧,让我们看看未定义行为的定义在草案 C99 标准部分3.4.3

使用不可移植或错误程序结构或错误数据时的行为,本国际标准对此没有要求

它只是标准没有强加要求的行为,完全适合这种情况,结果将根据架构而有所不同,并且尝试以可移植的方式指定结果可能会很困难,如果不是不可能的话。这就留下了一个问题,为什么他们会选择未定义的行为,而不是说实现未指定的行为?

很可能它是未定义的行为来限制可以创建无效指针offsetof的方式的数量,这与我们被提供以消除对不相关对象的指针减法的一个潜在需求的事实一致。

尽管该标准并没有真正定义术语无效指针,但我们在国际标准的基本原理-编程语言-C中得到了很好的描述,在6.3.2.3 指针部分中说(强调我的):

标准中隐含的是无效指针的概念。在讨论指针时,标准通常指的是“指向对象的指针”或“指向函数的指针”或“空指针”。地址算术中的一种特殊情况允许指针刚好超过数组的末尾。任何其他指针均无效。

C99 的基本原理进一步补充道:

不管无效指针是如何创建的,对它的任何使用都会产生未定义的行为。即使赋值、与空指针常量比较或与自身比较,在某些系统上也可能导致异常。

这向我们强烈暗示了指向padding的指针将是一个无效的指针,尽管很难证明padding不是object ,但object的定义说:

执行环境中的数据存储区域,其内容可以表示值

和注意事项:

引用时,对象可能被解释为具有特定类型;见 6.3.2.1。

我看不出我们如何推断结构元素之间填充的类型,因此它们不是对象,或者至少强烈表明填充不应该被视为对象

于 2014-11-03T15:35:00.657 回答
0

我应该指出以下几点:

来自 C99 标准,第 6.7.2.1 节:

在结构对象中,非位域成员和位域所在的单元的地址按声明顺序递增。一个指向结构对象的指针,经过适当的转换,指向它的初始成员(或者如果该成员是位域,则指向它所在的单元),反之亦然。结构对象中可能有未命名的填充,但不是在其开头。

成员之间的指针减法结果与其说是不确定的不如说是不可靠的(即,当应用相同的算术时,不能保证在相同结构类型的不同实例之间是相同的)。

于 2014-11-03T10:31:31.680 回答