指针可以经过什么处理并且仍然有效?
What treatment can a pointer undergo and still be valid?
以下哪种处理和尝试恢复 C 指针的方法保证有效?
1) 转换为 void 指针并返回
int f(int *a) {
void *b = a;
a = b;
return *a;
}
2) 转换为适当大小的整数并返回
int f(int *a) {
uintptr_t b = a;
a = (int *)b;
return *a;
}
3) 几个简单的整数运算
int f(int *a) {
uintptr_t b = a;
b += 99;
b -= 99;
a = (int *)b;
return *a;
}
4) 整数运算非常重要,足以掩盖出处,但仍会使值保持不变
int f(int *a) {
uintptr_t b = a;
char s[32];
// assume %lu is suitable
sprintf(s, "%lu", b);
b = strtoul(s);
a = (int *)b;
return *a;
}
5) 更多的间接整数运算将保持值不变
int f(int *a) {
uintptr_t b = a;
for (uintptr_t i = 0;; i++)
if (i == b) {
a = (int *)i;
return *a;
}
}
显然情况1是有效的,情况2肯定也是。另一方面,我遇到了 Chris Lattner 的 post - 不幸的是我现在找不到了 - 说类似于案例 5 的内容 not 有效,即标准许可编译器将其编译为无限循环。然而,每个案例看起来都像是前一个案例的无可非议的延伸。
有效案例和无效案例之间的界线在哪里?
根据评论中的讨论添加:虽然我仍然找不到启发案例 5 的 post,但我不记得涉及的指针类型;特别是,它可能是一个函数指针,这可能就是为什么那个案例显示无效代码而我的案例 5 是有效代码的原因。
第二个补充:好的,这是另一个消息来源说有问题,我确实有一个 link。 https://www.cl.cam.ac.uk/~pes20/cerberus/notes30.pdf - 关于指针出处的讨论 - 说,并有证据支持,不,如果编译器忘记了指针的来源,这是未定义的行为。
示例 1
有效,根据 §6.5.16.1,即使没有显式转换。
示例 2
intptr_t
和 uintptr_t
类型是可选的。将指针分配给一个整数需要显式转换(§6.5.16.1),尽管 gcc 和 clang 只会在您没有转换时发出警告。考虑到这些注意事项,往返转换在 §7.20.1.4 之前有效。 ETA: John Bellinger 指出,仅当您对 void*
进行双向转换时才会指定行为。但是,gcc 和 clang 都允许直接转换为文档扩展。
示例 3
安全,但这只是因为你使用的是无符号算术,它不会溢出,因此可以保证得到相同的对象表示。 intptr_t
可能会溢出!如果你想安全地进行指针运算,你可以将任何类型的指针转换为 char*
,然后在相同的结构或数组中添加或减去偏移量。请记住,sizeof(char)
始终是 1
。 ETA: 标准保证两个指针比较相等,但是你的 link 给 Chisnall 等人 给出了编译器假设的例子这两个指针不互为别名。
示例 4
总是,总是,总是 每当您读取缓冲区,尤其是写入缓冲区时,检查缓冲区溢出!是否可以通过静态分析从数学上证明不会发生溢出?然后明确地写出证明这一点的假设,并且 assert()
或 static_assert()
它们没有改变。使用 snprintf()
,而不是已弃用的、不安全的 sprintf()
!如果您对此答案没有其他印象,请记住!
绝对迂腐,可移植的方法是使用 <inttypes.h>
中的格式说明符,并根据任何指针表示的最大值定义缓冲区长度。在现实世界中,您将使用 %p
格式打印指针。
虽然您想要问的问题的答案是肯定的:重要的是您得到相同的对象表示。这是一个不那么做作的例子:
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int i = 1;
const uintptr_t u = (uintptr_t)(void*)&i;
uintptr_t v;
memcpy( &v, &u, sizeof(v) );
int* const p = (int*)(void*)v;
assert(p == &i);
*p = 2;
printf( "%d = %d.\n", i, *p );
return EXIT_SUCCESS;
}
重要的是对象表示中的位。此代码还遵循 §6.5 中的严格别名规则。它在给 Chisnall 等人 带来麻烦的编译器上编译和运行良好。
示例 5
这有效,同上。
一个永远不会与您的编码相关的极其迂腐的脚注:一些过时的深奥硬件具有符号整数的补码或符号和大小表示,并且在这些上,可能有一个明显的负值可能会或可能不会陷阱的零。在某些 CPU 上,这可能是一个有效的指针或与正零不同的空指针表示。在某些 CPU 上,正零和负零可能比较相等。
PS
标准说:
Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.
此外,如果两个数组对象是同一个多维数组的连续行,则超过第一行末尾的一个是指向下一行开始的有效指针。因此,即使是故意引起标准允许的尽可能多的错误的病态实现也只能在您的操纵指针与数组对象的地址进行比较时才会这样做,在这种情况下,实现在理论上可能会决定将其解释为取而代之的是其他数组对象的尾数。
预期的行为显然是比较等于 &array1+1
和 &array2
的指针等同于两者:这意味着让您将它与 array1
或中的地址进行比较取消引用它以获得 array2[0]
。然而,标准实际上并没有这么说。
PPS
标准委员会 has addressed some of these issues 并提议 C 标准明确添加有关指针来源的语言。这将确定是否允许符合标准的实现假设由位操作创建的指针不会为另一个指针设置别名。
具体而言,拟议的勘误表将引入指针出处,并允许具有不同出处的指针不进行比较。它还将引入一个 -fno-provenance
选项,这将保证当且仅当它们具有相同的数字地址时,任何两个指针比较相等。 (如上所述,两个比较相等的对象指针互为别名。)
1) Cast to void pointer and back
这会产生一个等于原始指针的有效指针。标准第6.3.2.3/1段对此有明确规定:
A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
2) Cast to appropriately sized integer and back
3) A couple of trivial integer operations
4) Integer operations nontrivial enough to obscure provenance, but which will nonetheless leave the value unchanged
5) More indirect integer operations that will leave the value unchanged
[...] Obviously case 1 is valid, and case 2 surely must be also. On the other hand, I came across a post by Chris Lattner - which I unfortunately can't find now - saying case 5 is not valid, that the standard licenses the compiler to just compile it to an infinite loop.
C 在指针和整数之间转换时确实需要强制转换,而您在示例代码中省略了其中一些。从这个意义上说,你的例子 (2) - (5) 都是不合格的,但对于这个答案的其余部分,我会假装所需的演员表在那里。
仍然,非常迂腐,所有这些示例都具有实现定义的行为,因此它们不严格 符合。另一方面,"implementation-defined" 行为仍然是定义的行为;这是否意味着您的代码是 "valid" 取决于您对该术语的含义。无论如何,编译器可能为任何示例发出什么代码是另一回事。
这些是第 6.3.2.3 节中标准的相关规定(强调已添加):
An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.
Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.
uintptr_t
的定义也与您的特定示例代码相关。标准以这种方式描述它(C2011,7.20.1.4/1;强调):
an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer.
您正在 int *
和 uintptr_t
之间来回转换。 int *
不是 void *
,因此 7.20.1.4/1 不适用于这些转换,并且行为是根据第 6.3.2.3 节实现定义的。
但是,假设您通过中间件来回转换 void *
:
uintptr_t b = (uintptr_t)(void *)a;
a = (int *)(void *)b;
在提供 uintptr_t
(可选)的实现中,这将使您的示例 (2 - 5) 全部严格符合。在这种情况下,整数到指针的转换结果仅取决于 uintptr_t
对象的值,而不取决于该值的获取方式。
至于您归因于 Chris Lattner 的声明,它们基本上是不正确的。如果您准确地表示了它们,那么它们可能反映了实现定义的行为和 un 定义的行为之间的混淆。如果代码表现出未定义的行为,那么声明可能有些道理,但事实并非如此。
无论其值是如何获得的,b
都有一个类型为 uintptr_t
的确定值,并且循环最终必须将 i
递增到该值,此时 if
块将 运行。原则上,从 uintptr_t
直接转换为 int *
的实现定义行为可能会有些疯狂,例如跳过下一条语句(从而导致无限循环),但这种行为是完全不可信的。你遇到的每一个实现要么在那个时候失败,要么在变量 a
中存储一些值,然后,如果它没有崩溃,它将执行 return
语句。
因为不同的应用领域需要以不同方式操作指针的能力,并且因为针对某些目的的最佳实现可能完全不适合其他一些目的,C 标准将支持(或不支持)各种类型的操作作为 实施质量 问题。一般来说,为特定应用程序领域编写实现的人应该比标准的作者更熟悉哪些特性对该领域的程序员有用,并且人们真诚地努力产生适合编写应用程序的高质量实现。该字段将支持此类功能,无论标准是否要求它们。
在 Dennis Ritchie 发明的前标准语言中,所有标识为相同地址的特定类型的指针都是等价的。如果指针上的任何操作序列最终会产生另一个标识相同地址的相同类型的指针,则该指针(根据定义本质上)将等同于第一个指针。然而,C 标准指定了一些情况,其中指针可以标识存储中的相同位置并且在不等同的情况下彼此无法区分。例如,给定:
int foo[2][4] = {0};
int *p = foo[0]+4, *q=foo[1];
p
和 q
将相互比较,并与 foo[0]+4
和 foo[1]
进行比较。另一方面,尽管 p[-1]
和 q[0]
的计算会定义行为,但 p[0]
或 q[-1]
的计算会调用 UB。不幸的是,虽然标准明确表示 p
和 q
不等价,但它没有说明是否对例如执行各种操作序列。 p
将产生一个在 p
可用的所有情况下都可用的指针,一个在 either p
或 q
将可用,仅在 q
可用的情况下可用的指针,或仅在 both p
的情况下可用的指针q
将可用。
用于低级编程的高质量实现通常应该处理指针操作,而不是那些涉及 restrict
指针的操作,其方式将产生一个指针,该指针在指针比较等于的任何情况下都可用它可以使用。不幸的是,标准没有提供程序可以确定它是否被适合低级编程的质量实现处理的方法,如果不是,则拒绝 运行,因此大多数形式的系统编程必须即使标准不强加任何要求,也要依靠质量实施以环境特有的记录方式处理某些行为。
顺便说一句,即使用于操作指针的普通构造无法在不适用等价原则的地方创建指针,某些平台可能会定义创建 "interesting" 指针的方法。例如,如果一个通常会捕获空指针操作的实现在有时可能需要访问地址为零的对象的环境中 运行 开启,它可能会定义一个特殊的语法来创建一个指针,该指针可以用于访问创建它的上下文中的任何地址,包括零。 "legitimate pointer to address zero" 可能会比较等于一个空指针(即使它们不等价),但是执行到另一种类型的往返转换并返回可能会将合法的指向地址零的指针转换为空指针。如果标准规定 any 指针的往返转换必须产生一个与原始指针相同的可用方式,这将要求编译器忽略任何指针上的空陷阱可以以这种方式生成,即使它们更有可能是通过往返空指针生成的。
顺便说一句,从实际的角度来看,"modern" 编译器,即使在 -fno-strict-aliasing
中,有时也会尝试通过指针-整数-指针转换来跟踪指针的来源,这种方式使得由有时可能会假定强制转换相等的整数不能使用别名。
例如,给定:
#include <stdint.h>
extern int x[],y[];
int test(void)
{
if (!x[0]) return 999;
uintptr_t upx = (uintptr_t)x;
uintptr_t upy = (uintptr_t)(y+1);
//Consider code with and without the following line
if (upx == upy) upy = upx;
if ((upx ^ ~upy)+1) // Will return if upx != upy
return 123;
int *py = (int*)upy;
*py += 1;
return x[0];
}
在没有标记行的情况下,gcc、icc 和 clang 都将假定——即使在使用 -fno-strict-aliasing
时,对 *py
的操作不会影响 *px
,即使可以达到该代码的唯一方法是 upx
和 upy
保持相同的值(这意味着 px
和 py
都是通过铸造相同的 uintptr_t
值)。添加标记的行会导致 icc 和 clang 识别出 px 和 py 可以标识相同的对象,但 gcc 假设可以优化赋值,即使它应该意味着 py
将派生自 px
--质量编译器应该可以毫不费力地将这种情况识别为暗示可能存在别名。
我不确定编译器编写者希望从他们追踪 uintptr_t 值来源的努力中得到什么实际好处,因为我看不出在结果为转换可以以 "interesting" 方式使用。然而,考虑到编译器行为,我不确定我是否看到任何好的方法来保证整数和指针之间的转换将以与所涉及的值一致的方式运行。
以下哪种处理和尝试恢复 C 指针的方法保证有效?
1) 转换为 void 指针并返回
int f(int *a) {
void *b = a;
a = b;
return *a;
}
2) 转换为适当大小的整数并返回
int f(int *a) {
uintptr_t b = a;
a = (int *)b;
return *a;
}
3) 几个简单的整数运算
int f(int *a) {
uintptr_t b = a;
b += 99;
b -= 99;
a = (int *)b;
return *a;
}
4) 整数运算非常重要,足以掩盖出处,但仍会使值保持不变
int f(int *a) {
uintptr_t b = a;
char s[32];
// assume %lu is suitable
sprintf(s, "%lu", b);
b = strtoul(s);
a = (int *)b;
return *a;
}
5) 更多的间接整数运算将保持值不变
int f(int *a) {
uintptr_t b = a;
for (uintptr_t i = 0;; i++)
if (i == b) {
a = (int *)i;
return *a;
}
}
显然情况1是有效的,情况2肯定也是。另一方面,我遇到了 Chris Lattner 的 post - 不幸的是我现在找不到了 - 说类似于案例 5 的内容 not 有效,即标准许可编译器将其编译为无限循环。然而,每个案例看起来都像是前一个案例的无可非议的延伸。
有效案例和无效案例之间的界线在哪里?
根据评论中的讨论添加:虽然我仍然找不到启发案例 5 的 post,但我不记得涉及的指针类型;特别是,它可能是一个函数指针,这可能就是为什么那个案例显示无效代码而我的案例 5 是有效代码的原因。
第二个补充:好的,这是另一个消息来源说有问题,我确实有一个 link。 https://www.cl.cam.ac.uk/~pes20/cerberus/notes30.pdf - 关于指针出处的讨论 - 说,并有证据支持,不,如果编译器忘记了指针的来源,这是未定义的行为。
示例 1
有效,根据 §6.5.16.1,即使没有显式转换。
示例 2
intptr_t
和 uintptr_t
类型是可选的。将指针分配给一个整数需要显式转换(§6.5.16.1),尽管 gcc 和 clang 只会在您没有转换时发出警告。考虑到这些注意事项,往返转换在 §7.20.1.4 之前有效。 ETA: John Bellinger 指出,仅当您对 void*
进行双向转换时才会指定行为。但是,gcc 和 clang 都允许直接转换为文档扩展。
示例 3
安全,但这只是因为你使用的是无符号算术,它不会溢出,因此可以保证得到相同的对象表示。 intptr_t
可能会溢出!如果你想安全地进行指针运算,你可以将任何类型的指针转换为 char*
,然后在相同的结构或数组中添加或减去偏移量。请记住,sizeof(char)
始终是 1
。 ETA: 标准保证两个指针比较相等,但是你的 link 给 Chisnall 等人 给出了编译器假设的例子这两个指针不互为别名。
示例 4
总是,总是,总是 每当您读取缓冲区,尤其是写入缓冲区时,检查缓冲区溢出!是否可以通过静态分析从数学上证明不会发生溢出?然后明确地写出证明这一点的假设,并且 assert()
或 static_assert()
它们没有改变。使用 snprintf()
,而不是已弃用的、不安全的 sprintf()
!如果您对此答案没有其他印象,请记住!
绝对迂腐,可移植的方法是使用 <inttypes.h>
中的格式说明符,并根据任何指针表示的最大值定义缓冲区长度。在现实世界中,您将使用 %p
格式打印指针。
虽然您想要问的问题的答案是肯定的:重要的是您得到相同的对象表示。这是一个不那么做作的例子:
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int i = 1;
const uintptr_t u = (uintptr_t)(void*)&i;
uintptr_t v;
memcpy( &v, &u, sizeof(v) );
int* const p = (int*)(void*)v;
assert(p == &i);
*p = 2;
printf( "%d = %d.\n", i, *p );
return EXIT_SUCCESS;
}
重要的是对象表示中的位。此代码还遵循 §6.5 中的严格别名规则。它在给 Chisnall 等人 带来麻烦的编译器上编译和运行良好。
示例 5
这有效,同上。
一个永远不会与您的编码相关的极其迂腐的脚注:一些过时的深奥硬件具有符号整数的补码或符号和大小表示,并且在这些上,可能有一个明显的负值可能会或可能不会陷阱的零。在某些 CPU 上,这可能是一个有效的指针或与正零不同的空指针表示。在某些 CPU 上,正零和负零可能比较相等。
PS
标准说:
Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.
此外,如果两个数组对象是同一个多维数组的连续行,则超过第一行末尾的一个是指向下一行开始的有效指针。因此,即使是故意引起标准允许的尽可能多的错误的病态实现也只能在您的操纵指针与数组对象的地址进行比较时才会这样做,在这种情况下,实现在理论上可能会决定将其解释为取而代之的是其他数组对象的尾数。
预期的行为显然是比较等于 &array1+1
和 &array2
的指针等同于两者:这意味着让您将它与 array1
或中的地址进行比较取消引用它以获得 array2[0]
。然而,标准实际上并没有这么说。
PPS
标准委员会 has addressed some of these issues 并提议 C 标准明确添加有关指针来源的语言。这将确定是否允许符合标准的实现假设由位操作创建的指针不会为另一个指针设置别名。
具体而言,拟议的勘误表将引入指针出处,并允许具有不同出处的指针不进行比较。它还将引入一个 -fno-provenance
选项,这将保证当且仅当它们具有相同的数字地址时,任何两个指针比较相等。 (如上所述,两个比较相等的对象指针互为别名。)
1) Cast to void pointer and back
这会产生一个等于原始指针的有效指针。标准第6.3.2.3/1段对此有明确规定:
A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
2) Cast to appropriately sized integer and back
3) A couple of trivial integer operations
4) Integer operations nontrivial enough to obscure provenance, but which will nonetheless leave the value unchanged
5) More indirect integer operations that will leave the value unchanged
[...] Obviously case 1 is valid, and case 2 surely must be also. On the other hand, I came across a post by Chris Lattner - which I unfortunately can't find now - saying case 5 is not valid, that the standard licenses the compiler to just compile it to an infinite loop.
C 在指针和整数之间转换时确实需要强制转换,而您在示例代码中省略了其中一些。从这个意义上说,你的例子 (2) - (5) 都是不合格的,但对于这个答案的其余部分,我会假装所需的演员表在那里。
仍然,非常迂腐,所有这些示例都具有实现定义的行为,因此它们不严格 符合。另一方面,"implementation-defined" 行为仍然是定义的行为;这是否意味着您的代码是 "valid" 取决于您对该术语的含义。无论如何,编译器可能为任何示例发出什么代码是另一回事。
这些是第 6.3.2.3 节中标准的相关规定(强调已添加):
An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.
Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.
uintptr_t
的定义也与您的特定示例代码相关。标准以这种方式描述它(C2011,7.20.1.4/1;强调):
an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer.
您正在 int *
和 uintptr_t
之间来回转换。 int *
不是 void *
,因此 7.20.1.4/1 不适用于这些转换,并且行为是根据第 6.3.2.3 节实现定义的。
但是,假设您通过中间件来回转换 void *
:
uintptr_t b = (uintptr_t)(void *)a;
a = (int *)(void *)b;
在提供 uintptr_t
(可选)的实现中,这将使您的示例 (2 - 5) 全部严格符合。在这种情况下,整数到指针的转换结果仅取决于 uintptr_t
对象的值,而不取决于该值的获取方式。
至于您归因于 Chris Lattner 的声明,它们基本上是不正确的。如果您准确地表示了它们,那么它们可能反映了实现定义的行为和 un 定义的行为之间的混淆。如果代码表现出未定义的行为,那么声明可能有些道理,但事实并非如此。
无论其值是如何获得的,b
都有一个类型为 uintptr_t
的确定值,并且循环最终必须将 i
递增到该值,此时 if
块将 运行。原则上,从 uintptr_t
直接转换为 int *
的实现定义行为可能会有些疯狂,例如跳过下一条语句(从而导致无限循环),但这种行为是完全不可信的。你遇到的每一个实现要么在那个时候失败,要么在变量 a
中存储一些值,然后,如果它没有崩溃,它将执行 return
语句。
因为不同的应用领域需要以不同方式操作指针的能力,并且因为针对某些目的的最佳实现可能完全不适合其他一些目的,C 标准将支持(或不支持)各种类型的操作作为 实施质量 问题。一般来说,为特定应用程序领域编写实现的人应该比标准的作者更熟悉哪些特性对该领域的程序员有用,并且人们真诚地努力产生适合编写应用程序的高质量实现。该字段将支持此类功能,无论标准是否要求它们。
在 Dennis Ritchie 发明的前标准语言中,所有标识为相同地址的特定类型的指针都是等价的。如果指针上的任何操作序列最终会产生另一个标识相同地址的相同类型的指针,则该指针(根据定义本质上)将等同于第一个指针。然而,C 标准指定了一些情况,其中指针可以标识存储中的相同位置并且在不等同的情况下彼此无法区分。例如,给定:
int foo[2][4] = {0};
int *p = foo[0]+4, *q=foo[1];
p
和 q
将相互比较,并与 foo[0]+4
和 foo[1]
进行比较。另一方面,尽管 p[-1]
和 q[0]
的计算会定义行为,但 p[0]
或 q[-1]
的计算会调用 UB。不幸的是,虽然标准明确表示 p
和 q
不等价,但它没有说明是否对例如执行各种操作序列。 p
将产生一个在 p
可用的所有情况下都可用的指针,一个在 either p
或 q
将可用,仅在 q
可用的情况下可用的指针,或仅在 both p
的情况下可用的指针q
将可用。
用于低级编程的高质量实现通常应该处理指针操作,而不是那些涉及 restrict
指针的操作,其方式将产生一个指针,该指针在指针比较等于的任何情况下都可用它可以使用。不幸的是,标准没有提供程序可以确定它是否被适合低级编程的质量实现处理的方法,如果不是,则拒绝 运行,因此大多数形式的系统编程必须即使标准不强加任何要求,也要依靠质量实施以环境特有的记录方式处理某些行为。
顺便说一句,即使用于操作指针的普通构造无法在不适用等价原则的地方创建指针,某些平台可能会定义创建 "interesting" 指针的方法。例如,如果一个通常会捕获空指针操作的实现在有时可能需要访问地址为零的对象的环境中 运行 开启,它可能会定义一个特殊的语法来创建一个指针,该指针可以用于访问创建它的上下文中的任何地址,包括零。 "legitimate pointer to address zero" 可能会比较等于一个空指针(即使它们不等价),但是执行到另一种类型的往返转换并返回可能会将合法的指向地址零的指针转换为空指针。如果标准规定 any 指针的往返转换必须产生一个与原始指针相同的可用方式,这将要求编译器忽略任何指针上的空陷阱可以以这种方式生成,即使它们更有可能是通过往返空指针生成的。
顺便说一句,从实际的角度来看,"modern" 编译器,即使在 -fno-strict-aliasing
中,有时也会尝试通过指针-整数-指针转换来跟踪指针的来源,这种方式使得由有时可能会假定强制转换相等的整数不能使用别名。
例如,给定:
#include <stdint.h>
extern int x[],y[];
int test(void)
{
if (!x[0]) return 999;
uintptr_t upx = (uintptr_t)x;
uintptr_t upy = (uintptr_t)(y+1);
//Consider code with and without the following line
if (upx == upy) upy = upx;
if ((upx ^ ~upy)+1) // Will return if upx != upy
return 123;
int *py = (int*)upy;
*py += 1;
return x[0];
}
在没有标记行的情况下,gcc、icc 和 clang 都将假定——即使在使用 -fno-strict-aliasing
时,对 *py
的操作不会影响 *px
,即使可以达到该代码的唯一方法是 upx
和 upy
保持相同的值(这意味着 px
和 py
都是通过铸造相同的 uintptr_t
值)。添加标记的行会导致 icc 和 clang 识别出 px 和 py 可以标识相同的对象,但 gcc 假设可以优化赋值,即使它应该意味着 py
将派生自 px
--质量编译器应该可以毫不费力地将这种情况识别为暗示可能存在别名。
我不确定编译器编写者希望从他们追踪 uintptr_t 值来源的努力中得到什么实际好处,因为我看不出在结果为转换可以以 "interesting" 方式使用。然而,考虑到编译器行为,我不确定我是否看到任何好的方法来保证整数和指针之间的转换将以与所涉及的值一致的方式运行。