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