简介:这个问题是我收集的C和C ++(和C / C ++常见子集)问题的一部分,关于允许具有完全相同的字节顺序表示的指针对象具有不同的情况“值“,即对某些操作表现不同(包括在一个对象上定义行为,在另一个对象上定义未定义的行为)。
关注another question which caused much confusion,这里有关于指针语义的问题,希望能够解决问题:
这个程序在所有情况下都有效吗?唯一有趣的部分是“pa1 == pb”分支。
#include <stdio.h>
#include <string.h>
int main() {
int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;
if (memcmp (&pa1, &pb, sizeof pa1) == 0) {
int *p;
printf ("pa1 == pb\n"); // interesting part
memcpy (&p, &pa1, sizeof p); // make a copy of the representation
memcpy (&pa1, &p, sizeof p); // pa1 is a copy of the bytes of pa1 now
// and the bytes of pa1 happens to be the bytes of pb
*pa1 = 2; // does pa1 legally point to b?
}
else {
printf ("pa1 != pb\n"); // failed experiment, nothing to see
pa1 = &a[0]; // ensure well defined behavior in printf
}
printf ("b = %d *pa1 = %d\n", b, *pa1);
return 0;
}
我想根据标准报价给出答案。
修改
根据大众的需求,这就是我想知道的:
这里假设有一个超过结束指针偶然指向另一个对象;如何使用这一个超过结束指针来访问另一个对象?
除了使用另一个对象的地址副本外,我有权做任何事情。 (这是一个了解C中指针的游戏。)
IOW,我试图像黑手党一样回收脏钱。但我通过提取其值表示来回收脏指针。然后它看起来像干净的钱,我的意思是指针。没有人能说出差异,不是吗?
答案 0 :(得分:6)
问题是:
此程序在所有情况下均有效吗?
答案是“不,不是”。
该程序唯一有趣的部分是在if
语句保护的块中发生的事情。要保证控制表达式的真实性有些困难,因此我通过将变量移到全局范围进行了一些修改。仍然存在相同的问题:该程序是否始终有效:
#include <stdio.h>
#include <string.h>
static int a[1] = { 2 };
static int b = 1;
static int *pa1 = &a[0] + 1;
static int *pb = &b;
int main(void) {
if (memcmp (&pa1, &pb, sizeof pa1) == 0) {
int *p;
printf ("pa1 == pb\n"); // interesting part
memcpy (&p, &pa1, sizeof p); // make a copy of the representation
memcpy (&pa1, &p, sizeof p); // pa1 is a copy of the bytes of pa1 now
// and the bytes of pa1 happens to be the bytes of pb
*pa1 = 2; // does pa1 legally point to b?
}
}
现在,防护表达式在我的编译器上是正确的(当然,通过使这些表达式具有静态存储持续时间,编译器无法真正证明它们没有被中间的其他操作修改)。
指针pa1
指向数组a
的末尾,并且是有效的指针,但不能取消引用,即*pa1
在给定该值的情况下具有未定义的行为。现在的情况是将 this 值复制到p
并再次返回将使指针 valid 。
答案是否定的,这仍然无效,但是在标准本身中并没有非常明确地说明。委员会对C standard defect report DR 260的回应是这样的:
如果两个对象具有相同的位模式表示形式,并且它们的类型相同,则它们仍可以比较为不相等(例如,如果一个对象具有不确定的值),并且如果一个对象是不确定的值,则试图读取此类对象将调用未定义的对象行为。允许实现跟踪位模式的来源,并将表示不确定值的源与表示确定值的源区分开来。即使它们按位相同,它们也可能将基于不同来源的指针视为不同的。
即您甚至无法得出以下结论:如果pa1
和pb
是相同类型的指针,并且memcmp (&pa1, &pb, sizeof pa1) == 0
是正确的,那么也有必要pa1 == pb
,更不用说复制位模式了无法指责的指针pa1
指向另一个对象然后再次返回将使pa1
有效。
响应继续:
请注意,使用赋值或通过
memcpy
或memmove
对确定值进行按位复制会使目标获取相同的确定值。
即它确认memcpy (&p, &pa1, sizeof p);
将使p
获得与pa1
相同的值,而该值是以前所没有的。
这不仅仅是一个理论问题-众所周知,编译器会跟踪指针的出处。例如the GCC manual指出
当从指针转换为整数并再次返回时,结果指针必须引用与原始指针相同的对象,否则行为未定义。也就是说,不能使用整数算术来避免C99和C11 6.5.6 / 8禁止的指针算术的不确定行为。
即该程序写为:
int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;
if (memcmp (&pa1, &pb, sizeof pa1) == 0) {
uintptr_t tmp = (uintptr_t)&a[0]; // pointer to a[0]
tmp += sizeof (a[0]); // value of address to a[1]
pa1 = (int *)tmp;
*pa1 = 2; // pa1 still would have the bit pattern of pb,
// hold a valid pointer just past the end of array a,
// but not legally point to pb
}
GCC手册指出,此明确地不合法。
答案 1 :(得分:5)
指针只是一个无符号整数,其值是内存中某个位置的地址。覆盖指针变量的内容与覆盖普通int
变量的内容没有区别。
是的,做例如memcpy (&p, &pa1, sizeof p)
等同于作业p = pa1
,但可能效率较低。
让我们尝试不同的方式:
你有pa1
指向某个对象(或者更确切地说,超出某个对象),然后你有指针&pa1
指向变量pa1
(即变量pa1
位于内存中。
图形上看起来像这样:
+------+ +-----+ +-------+ | &pa1 | --> | pa1 | --> | &a[1] | +------+ +-----+ +-------+
[注意:&a[0] + 1
与&a[1]
相同
答案 2 :(得分:5)
未定义的行为:n
部分中的游戏。
编译器1和编译器2进入,右侧。
int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;
[Compiler1]您好,
a
,pa1
,b
,pb
。和你相识真是太好了。现在你就坐在那里,我们将查看剩下的代码,看看我们是否可以为你分配一些不错的堆栈空间。
编译器1查看其余代码,偶尔皱眉并在纸上做一些标记。编译器2抠鼻子,盯着窗外。
[Compiler1]好吧,我害怕,
b
,我决定优化你。我根本无法找到修改你记忆的地方。也许你的程序员用Undefined Behavior做了一些技巧来解决这个问题,但是我可以假设没有这样的UB存在。对不起。
退出b
,由熊追捕。
[Compiler2]等等!等一下,
b
。我无法优化这段代码,所以我决定在堆栈上为你提供一个舒适的空间。
b
高兴地跳起来,但是一旦他被未定义的行为修改,就会被鼻子恶魔谋杀。
[讲述人]因此结束了变量
b
悲伤,悲伤的故事。这个故事的寓意是永远不能依赖未定义的行为。
答案 3 :(得分:2)
*pa1 = 2; // does pa1 legally point to b?
不,pa1
点b
纯属巧合。请注意,程序必须在编译时符合,指针恰好在运行时具有相同的值并不重要。
没有人能说出差异,不是吗?
编译器优化器可以分辨出来!
编译器优化器可以看到(通过代码的静态分析)b
并且永远不会通过“合法”指针访问,因此它假设将b
保存在寄存器中是安全的。这个决定是在汇编时做出的。
底线:
“Legal”指针是通过赋值或通过复制内存从合法指针获取的指针。您还可以使用指针算法获得“合法”指针,前提是结果指针位于分配/复制的数组/内存块的合法范围内。如果指针算术的结果恰好指向另一个存储块中的有效地址,则使用这样的指针仍然是UB。
另请注意,仅当两个指针指向同一个数组/内存块时,指针比较才有效。
编辑:
哪里出错?
标准规定访问数组越界会导致未定义的行为。你用一个指针取一个越界的地址,复制它然后取消引用它。
该标准规定,越界指针可以比较指向另一个碰巧在内存中相邻的对象的指针(6.5.9 pt 6)。但是,即使它们在语义上相等,它们也不会指向同一个对象。
在你的情况下,你不比较指针,你比较他们的位模式。无所谓。指针pa1
仍然被认为是指向数组末尾之后的指针。
请注意,如果用您自己编写的某个函数替换memcpy
,编译器将不知道pa1
具有什么值,但它仍然可以静态地确定它不能包含“合法”获得的副本&b
。
因此,在这种情况下,允许编译器优化器优化b
的读/存储。
是指针的语义“值”(根据规范的行为),仅由其数值(它包含的数字地址)确定,对于给定类型的指针?
没有。该标准推断有效指针只能通过使用address-of运算符(&
),通过复制另一个有效指针或通过/减少数组边界内的指针来获取对象。作为一种特殊情况,超过数组末尾的指针有效,但不能取消引用它们。这可能看起来有点严格,但如果没有它,优化的可能性就会受到限制。
如果没有,可以只复制指针中包含的物理地址,同时省略相关的语义?
不,至少不是可以移植到任何平台的方式。在许多实现中,指针值只是地址。语义在生成的代码中。
答案 4 :(得分:2)
您已经证明它似乎适用于特定的实现。这并不意味着它一般 。实际上,这是一种未定义的行为,其中一种可能的结果恰好“似乎有效”。
如果,我们回到MS-DOS时代,我们有近指针(相对于特定的段)和远指针(包含段和偏移)。
大型数组通常在它们自己的段中分配,只有偏移量被用作指针。编译器已经知道哪个段包含特定的数组,因此它可以将指针与适当的段寄存器组合在一起。
在这种情况下,你可以有两个具有相同位模式的指针,其中一个指针指向一个数组段(pa
)而另一个指针指向堆栈段(pb
)。指针比较相等,但仍指向不同的东西。
更糟糕的是,具有段:偏移对的远指针可能会形成重叠段,因此不同的位模式仍然指向相同的物理内存地址。例如,0100:0210
与0120:0010
的地址相同。
C和C ++语言的设计使其可以工作。这就是为什么我们有规则比较指针只能在同一个数组中工作(给出一个总顺序),并且指针可能不指向同一个东西,即使它们包含相同的位模式。
答案 5 :(得分:2)
在C99之前,预计实现的行为就好像任何类型的每个变量的值都存储了一系列unsigned char
值;如果检查了相同类型的两个变量的基础表示并且发现它们是相等的,那么这意味着除非未定义的行为已经已经发生,否则它们的值通常是相等且可互换的。在几个地方有一点点含糊不清,例如给定
char *p,*q;
p = malloc(1);
free(p);
q = malloc(1);
if (!memcmp(&p, &q, sizeof p))
p[0] = 1;
C的每个版本都清楚地表明q
可能或可能不等于p
,如果q
不等于p
,则应该预期编写p[0]
时可能会发生任何事情。虽然C89标准没有明确说明如果对p
的写入等同于对q
的写入,则实现可能只有p
比较等于q
,变量模型通常被隐含在unsigned char
值序列中。
C99增加了许多情况,其中变量可以按比例相等但不相等。例如,考虑一下:
extern int doSomething(char *p1, char *p2);
int act1(char * restrict p1, char * restrict p2)
{ return doSomething(p1,p2); }
int act2(char * restrict p)
{ return doSomething(p,p); }
int x[4];
int act3a(void) { return act1(x,x); }
int act3b(void) { return act2(x); }
int act3c(void) { return doSomething(x,x); }
调用act3a
,act3b
或act3c
会导致doSomething()
调用两个比较等于x
的指针,但是如果通过{调用{} {1}} act3a
中x
内写的任何元素都必须使用doSomething
专门使用x
,或专门使用p1
。如果通过p2
调用,该方法可以使用act3b
自由编写元素,并通过p1
访问它们,反之亦然。如果通过p2
访问,则该方法可以互换使用act3c
,p1
和p2
。 x
或p1
的二进制表示中的任何内容都不表示它们是否可以与p2
互换使用,但允许编译器在x
内进行内联展开doSomething
{1}}和act1
并且这些扩展的行为根据允许和禁止的指针访问而有所不同。
答案 6 :(得分:1)
不。考虑到memcmp()
的任何特定结果,我们甚至无法推断出该代码的任何一个分支都可以工作。即使指针相等,您与memcmp()
比较的对象表示形式也可能不同,即使对象表示形式匹配,指针也可能不同。 (自从我最初发布以来,我已经改变了主意。)
您尝试将一个数组的最后一个地址与该数组之外的对象的地址进行比较。该标准(n1548草案的第6.5.8.5节,增加了重点)说:
比较两个指针时,结果取决于所指向对象的地址空间中的相对位置。如果两个指向对象类型的指针都指向同一对象,或者都指向同一数组对象的最后一个元素,则它们的比较相等。如果所指向的对象是同一聚合对象的成员,则指向稍后声明的结构成员的指针大于指向早于该结构声明的成员的指针,指向具有较大下标值的数组元素的指针大于指向同一数组的元素的指针下标值较低。指向同一联合对象的成员的所有指针比较相等。如果表达式 P 指向数组对象的元素,而表达式 Q 指向同一数组对象的最后一个元素,则指针表达式 Q +1的比较结果大于 P 。 在所有其他情况下,行为都是不确定的。
它在附录J中重复此警告,指出指针比较的结果未定义。
也未定义的行为:
已被修改的对象可以通过指向const限定类型的限制限定指针来访问,或者通过两个都不都基于同一对象的限定限定指针和另一个指针来访问
但是,程序中的所有指针都没有限制限定符。您也不会执行非法的指针算术。
您尝试改用memcmp()
来解决这种不确定的行为。规范的相关部分(第7.2.3.4.1节)说:
memcmp
函数将n
指向的对象的前s1
个字符与n
指向的对象的前s2
个字符进行比较
因此,memcmp()
比较对象表示的位。 pa1
和pb
的位已经在某些实现上相同,而在其他实现上则相同。
该标准的§6.2.6.1做出以下保证:
两个具有相同对象表示形式的值(NaN除外)比较相等,但比较相等的值可能具有不同的对象表示形式。
指针值比较相等意味着什么? §6.5.9.6告诉我们:
当且仅当两个都是空指针时,两个指针比较相等,两个都是指向同一对象的指针(包括指向对象的指针和位于其起始处的子对象)或函数,都是指向最后一个元素的指针相同的数组对象,或者一个是指向一个数组对象的末尾的指针,另一个是指向另一个数组对象的起点的指针,而该数组对象恰好紧随地址空间中的第一个数组对象。
我认为,最后一条是关键。如果两个比较相等的指针不仅具有不同的对象表示形式,而且具有相同对象表示形式的两个指针可能不相等,如果其中一个是像&a[0]+1
这样的单端指针,而另一个是指针到数组外部的对象,例如&b
。情况就是这样。
答案 7 :(得分:0)
我说不,不求助于UB tarpit。从以下代码中:
extern int f(int x[3], int y[4]);
....
int a[7];
return f(a, a) + f(a+4, a+3);
...
C标准不应该阻止我编写执行边界检查的编译器;有几种可用。边界检查编译器必须通过用边界信息(*)扩展指针来使指针变胖。所以当我们到达f()时:
....
if (x == y) {
....
F()对 C 相等性概念会感兴趣,也就是说,它们指向相同的位置,而不是具有相同的类型。如果您对此不满意,请假设f()名为g(int * s,int * t),并且其中包含类似的测试。编译器将执行比较而无需比较 fat </ em>。
指针大小sizeof(int *)必须包含 fat </ em>,因此两个指针的memcmp也会对其进行比较,从而提供与比较不同的结果。
PS:我们是否应该为肚脐注视引入新标签?
答案 8 :(得分:0)
据我了解,问题是:
指针的memcpy是否与赋值相同?
我的回答是,是的。
memcpy
基本上是针对无内存对齐要求的可变长度数据的优化分配。几乎与:
void slow_memcpy(void * target, void * src, int len) {
char * t = target;
char * s = src;
for (int i = 0; i < len; ++i)
{
t[i] = s[i];
}
}
对于给定类型的指针,指针的语义“值”(根据规范的行为)仅由其数值(其包含的数字地址)确定吗?
是的。没有隐藏的数据字段是C,因此指针的行为完全取决于它的数字数据内容。
但是,指针算术由编译器解析,并且取决于指针的类型。
char * str
个指针算术将使用char
个单位(即,str[1]
与char
相距一个str[0]
),而{{1} }指针算术将使用int * p_num
个单位(即int
与p_num[1]
之间有1个int
)。
两个具有相同位模式的指针是否允许具有不同的行为? (编辑)
是,不是。
它们指向内存中的相同位置,从这个意义上说,它们是相同的。
但是,指针分辨率可能取决于指针的类型。
例如,通过取消引用p_num[0]
,(通常)仅从存储器中读取8位。但是,在取消引用uint8_t *
时,会从内存地址中读取64位。
如上所述,另一个区别是指针算法。
但是,当使用诸如uint64_t *
或memcpy
之类的函数时,指针的行为将相同。
好吧,这是因为示例中的代码未在标题中反映问题。代码的行为是不确定的,许多答案清楚地说明了这一点。
(编辑):
代码问题与实际问题无关。
例如,考虑以下行:
memcmp
在这种情况下,int a[1] = { 0 }, *pa1 = &a[0] + 1, b = 1, *pb = &b;
指向pa
,这超出了范围。
这几乎使代码陷入未定义的行为领域,这使许多答案都偏离了实际问题。