我有一个指向内存地址的void
指针。然后,我做
int
指针= void
指针
float
指针= void
指针
然后,取消引用它们获取值。
{
int x = 25;
void *p = &x;
int *pi = p;
float *pf = p;
double *pd = p;
printf("x: n%d\n", x);
printf("*p: %d\n", *(int *)p);
printf("*pi: %d\n", *pi);
printf("*pf: %f\n", *pf);
printf("*pd: %f\n", *pd);
return 0;
}
解除引用pi
(int
指针)的输出为25。
但是,解除引用pf
(float
指针)的输出为0.000。
同样取消pd
(double
指针)输出保留的负分数
变化?
为什么这和字节序有关(我的CPU是小端)?
答案 0 :(得分:7)
根据C
标准,您允许将任何指针转换为void *
并将其转换回来,它会产生相同的效果。
引用C11
,章节§6.3.2.3
[...]指向 任何对象类型都可以转换为指向
void
并再次返回的指针;结果应该 比较等于原始指针。
这就是为什么当你将void指针强制转换为int *
时,取消引用并打印结果,它会正确打印。
但是,标准并不保证您可以取消引用该指针为不同的数据类型。它本质上是在调用未定义的行为。
因此,取消引用pf
或pd
以获取float
或double
为undefined behavior,因为您正在尝试读取为其分配的内存int
为 float
或double
。有一个明显的mismtach案例导致UB。
详细说明,int
和float
(和double
)具有不同的内部表示形式,因此尝试将指针转换为另一种类型,然后尝试取消引用以获取值< em> in other type 不起作用。
相关,C11
,章节§6.5.3.3
[...]如果操作数的类型为''指向类型'',则结果的类型为''type''。如果 已为指针分配了无效值,一元
*
运算符的行为为 未定义。
和无效值部分,(强调我的)
unary *运算符解除引用指针的无效值是空指针 an 地址不恰当地对齐指向的对象类型,以及对象后的地址 它的一生结束。
答案 1 :(得分:4)
除了之前的答案之外,我认为由于浮点数的表示方式,你所期望的是无法实现的。
整数通常以Two's complement方式存储,基本上它意味着数字存储为一个整数。另一方面,浮点数使用符号,基数和指数Read here以不同的方式存储。
所以转换的主要思想是不可能的,因为你试图将一个数字表示为原始位(对于正数),并将其看作是不同的编码,即使转换是合法的,这也会导致意外的结果。
答案 2 :(得分:3)
所以......这可能是正在发生的事情。
但解除引用pf(浮点指针)的输出为0.000
它不是0.它真的很小。
你有4个字节的整数。你的整数在内存中看起来像这样......
5 0 0 0
00000101 00000000 00000000 00000000
解释为float看起来像......
sign exponent fraction
0 00001010 0000000 00000000 00000000
+ 2**-117 * 1.0
所以,你输出一个浮点数,但它非常小。它是2 ^ -117,几乎与0无法区分。
如果您尝试使用printf("*pf: %e\n", *pf);
打印浮动,那么它应该会给您一些有意义的东西,但是很小。 7.006492e-45
同样去除pd(双指针)输出一个不断变化的负分数?
双打是8个字节,但你只定义了4个字节。负分数变化是查看未初始化记忆的结果。未初始化的内存的值是任意的,看到它随着每次运行而改变是正常的。
答案 3 :(得分:2)
这里有两种UB:
1)严格别名
What is the strict aliasing rule?
“严格别名是由C(或C ++)编译器做出的假设,取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即彼此别名。)”
但是,严格别名可以作为编译器扩展关闭,如GCC中的-fno-strict-aliasing
。在这种情况下,您的pf
版本可以很好地运行,虽然实现已定义,但假设没有其他错误(通常float
和int
都是32位类型,并且在大多数计算机上都是32位对齐,通常)。如果您的计算机使用IEEE754 single,则可以获得一个非常小的denorm floating point数字,这可以解释您观察到的结果。
严格别名是最近版本的C的一个有争议的特征(并且被很多人认为是一个错误),并且使得在C中重新解释强制转换(aka type punning)变得非常困难和更加hacky。
在您非常了解类型惩罚以及它与您的编译器和硬件版本的行为之前,您应该避免这样做。
2)内存越界
您的指针指向与int
一样大的内存空间,但您将其取消引用为double
,通常是int
大小的两倍,您基本上只读了一半来自计算机某处的double
垃圾,这就是您double
不断变化的原因。
答案 4 :(得分:1)
int
,float
和double
类型具有不同的内存布局,表示和解释。
在我的机器上,int
为4个字节,float
为4个字节,double
为8个字节。
以下是解释您所看到的结果的方法。
显然,取消呈现int
指针有效,因为原始数据是int
。
取消引用float
指针,编译器生成代码以将内存中4个字节的内容解释为float
。当解释为float时,4个字节中的值为0.00。查看float
在内存中的表示方式。
取消引用double
指针,编译器生成代码以将内存中的内容解释为double
。由于double
大于int
,因此会访问原始int
的4个字节,并在堆栈上访问额外 4个字节。因为这些额外4个字节的内容取决于堆栈的状态,并且从运行到运行是不可预测的,所以您会看到对应于将整个8个字节解释为double
的变化值。
答案 5 :(得分:1)
以下,
printf("x: n%d\n", x); //OK
printf("*p: %d\n", *(int *)p); //OK
printf("*pi: %d\n", *pi); //OK
printf("*pf: %f\n", *pf); // UB
printf("*pd: %f\n", *pd); // UB
当您通过类型int
的左值类型访问int
时,前3个printfs中的访问权限正常。但接下来的2个并不好违反 6.5,7,表达式。
int *
不是兼容类型,其中包含float *
或double *
。因此,最后两次printf()调用中的访问会导致未定义的行为。
C11,$ 6.5,7州:
对象的存储值只能由左值访问 具有以下类型之一的表达式:
- 与对象的有效类型兼容的类型,- 与对象的有效类型兼容的类型的限定版本
- 与对象的有效类型对应的有符号或无符号类型
- 对应于对象有效类型的限定版本的有符号或无符号类型,
- 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或
- 字符类型。
答案 6 :(得分:0)
术语&#34; C&#34;用于描述两种语言:一种是由K&amp; R发明的,其中指针识别物理存储器位置,另一种是从指针以遵守某些规则的方式读取和写入的情况下工作相同的语言,但是如果以其他方式使用,则可能以任意方式表现。虽然后一种语言是由标准定义的,但前一种语言在20世纪80年代开始流行于微机编程。
从C代码生成高效机器代码的主要障碍之一是编译器无法告诉哪些指针可能为哪些变量添加别名。因此,任何时候代码访问可能指向给定变量的指针,生成的代码都需要确保由指针识别的内存的内容和变量的内容匹配。这可能非常昂贵。编写C89标准的人决定允许编译器假设命名变量(静态和自动)只能使用自己的类型或字符类型的指针访问;编写C99的人决定为分配的存储添加额外的限制。
一些编译器提供了一种方法,通过这种方法,代码可以确保使用不同类型的访问将通过内存(或者至少表现得像他们这样做),但不幸的是我不认为有任何标准为了那个原因。 C14添加了一个用于多线程的内存模型,它应该能够实现所需的语义,但我不认为编译器必须遵守这样的语义,以防他们知道没有办法外部线程访问某些东西[即使通过内存来实现正确的单线程语义也是必要的]。
如果您正在使用gcc并希望具有与K&amp; R预期相同的内存语义,请使用&#34; -fno-strict-aliasing&#34;命令行选项。为了提高代码效率,有必要大量使用&#34;限制&#34;在C99中添加的限定符。虽然gcc的作者似乎更关注基于类型的别名规则而不是&#34; restrict&#34;后者应该允许更有用的优化。