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);
这显然违反了上述规则,因为p
和q
指针指向不同的"数组对象",并且根据规则,{{1}差异未定义。
但在实践中,为什么这样的事情会导致未定义的行为?毕竟,struct成员按顺序排列(就像数组元素一样),成员之间有任何潜在的填充。确实,填充量会因实现而异,这会影响计算结果,但为什么结果应该是"未定义"?
我的问题是,我们可以假设标准只是"无知"这个问题,还是有充分的理由不扩大这条规则?不能将上述规则改为" 两者都应指向同一数组对象的元素或同一结构的成员"?
我唯一的怀疑是分段内存架构,其中成员可能最终分成不同的段。是这样的吗?
我也怀疑这就是为什么GCC定义自己的q - p
,以便符合标准" __builtin_offsetof
宏的定义。
编辑:
正如已经指出的那样,标准不允许对void指针进行算术运算。它是一个GNU扩展,只有在GCC通过offsetof
时才会触发警告。我用-std=c99 -pedantic
指针替换void *
指针。
答案 0 :(得分:3)
明确定义了同一结构成员地址之间的减法和关系运算符(在类型char*
上)。
任何对象都可以视为unsigned char
的数组。
引用N1570 6.2.6.1第4段:
存储在任何其他对象类型的非位字段对象中的值 由 n ×
CHAR_BIT
位组成,其中n是对象的大小 type,以字节为单位。该值可以复制到类型的对象中unsigned char [
n]
(例如,memcpy
);得到的字节集是 称为值的对象表示。
...
我唯一怀疑的是成员的分段内存架构 可能最终会分成不同的部分。是这样的吗?
没有。对于具有分段内存体系结构的系统,通常编译器会强制限制每个对象必须适合单个段。或者它可以允许占用多个段的对象,但仍然必须确保指针算术和比较正常工作。
答案 1 :(得分:2)
指针运算要求将两个指针相加或相减为同一个对象的一部分,否则它就没有意义。
引用的标准部分特指两个不相关的对象,例如int a[b];
和int b[5]
。指针算术需要知道指针所指向的对象的类型(我相信你已经知道了这一点)。
即
int a[5];
int *p = &a[1]+1;
此处p
是通过知道&a[1]
引用int
对象并因此增加到4个字节(假设sizeof(int)
为4)来计算的。
来到struct示例,我不认为它可能以一种方式定义,使结构成员之间的指针算术合法。
让我们举个例子,
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
(来自<stddef.h>
)来获得差异在struct成员之间,它具有与成员之间的指针算术类似的效果:
printf("%zu\n", offsetof(struct test, y) - offsetof(struct test, x));
所以我认为没有必要通过C标准在struct成员之间定义指针算法。
答案 2 :(得分:1)
是的,您可以在结构字节上执行指针算术:
N1570 - 6.3.2.3指针p7:
...当指向对象的指针转换为指向字符类型的指针时, 结果指向对象的最低寻址字节。 连续递增 结果,直到对象的大小,产生指向对象剩余字节的指针。
这意味着对于程序员来说,结构的字节应被视为一个连续的区域,无论它如何在硬件中实现。
不是void*
指针,而是非标准的编译器扩展。如标准段落中所述,它仅适用于字符类型指针。
修改强>
正如mafso在评论中指出的那样,只要减法结果类型ptrdiff_t
具有足够的结果范围,上述情况才有效。由于size_t
的范围可能大于ptrdiff_t
,并且如果结构足够大,则地址距离太远可能。
因此,最好在结构成员上使用offsetof
宏并计算结果。
答案 3 :(得分:1)
我相信这个问题的答案比看上去简单,OP问道:
但为什么这个结果应该是“未定义的”?
好吧,让我们看看未定义行为的定义是在草案C99标准部分3.4.3
中:
行为,使用不可移植或错误的程序构造或 错误的数据,本国际标准没有规定 要求
这只是标准没有强制要求的行为,完全适合这种情况,结果会根据体系结构而变化,并且尝试指定结果可能很难,如果不是不可能在便携式方式。这留下了一个问题,为什么他们会选择未定义的行为而不是让我们说未实现的行为的实现?
很可能是为了限制无效指针的创建方式而设置了未定义的行为,这与我们提供offsetof
以删除一个可能需要指针减去不相关的对象。
虽然标准没有真正定义术语无效指针,但我们在Rationale for International Standard—Programming Languages—C中得到了一个很好的描述,在6.3.2.3
指针部分中说明了(强调我的< / em>的):
标准中隐含的是无效指针的概念。在 在讨论指针时,标准通常指的是“指向一个指针 object“或”指向函数的指针“或”空指针。“一个特殊的 地址算术中的情况允许指针刚好超过结束 一个数组。 任何其他指针无效。
C99理由进一步补充说:
无论如何创建无效指针,任何使用它都会产生 未定义的行为。甚至赋值,与空指针比较 不断,或与自身比较,可能在某些系统上导致 例外。
这强烈建议我们指向 padding 的指针是无效指针,尽管很难证明 padding 不是一个对象, object 的定义是:
执行环境中的数据存储区域的内容 它可以代表值
并注意到:
引用时,对象可能被解释为具有特定的对象 类型;见6.3.2.1。
我看不出我们如何推断结构元素之间填充的类型或值,因此它们不是对象或至少强烈表示 padding 不应被视为对象。
答案 4 :(得分:0)
我应该指出以下几点:
来自C99标准,第6.7.2.1节:
在结构对象内,非位字段成员和位域中的单位 驻留的地址按声明的顺序增加。指向a的指针 结构对象,适当转换,指向其初始成员(或者如果该成员是a 位字段,然后到它所在的单元,反之亦然。 可能有未命名的 在结构对象中填充,但不在其开头。
成员之间的指针减法结果并不是那么多,因为它是不可靠的(即,当应用相同的算术时,不能保证相同结构类型的不同实例之间的相同)。 / p>