这是我运行的程序:
#include <stdio.h>
int main(void)
{
int y = 1234;
char *p = &y;
int *j = &y;
printf("%d %d\n", *p, *j);
}
我对输出感到有些困惑。我所看到的是:
-46 1234
我把这个程序写成了一个实验,并不知道它将要输出什么。我期待y
可能只有一个字节。
正在发生的事情&#34;幕后花絮&#34;这里?解除引用p
如何为我提供-46
?
正如其他人所指出的那样,我必须进行明确的施法才能导致UB。我没有将该行从char *p = &y;
更改为char *p = (char *)&y;
,因此我不会使下面的答案无效。
此程序不会引起任何UB行为,如指向here。
答案 0 :(得分:13)
如果你有类似的东西,
int x = 1234;
int *p = &x;
如果您取消引用指针p
,那么它将正确读取整数字节。因为你声明它是int
的指针。它将知道sizeof()
运算符要读取的字节数。通常int
的大小为4 bytes
(对于32/64位平台),但它与机器有关,这就是为什么它将使用sizeof()
运算符来知道正确的大小并将读取。
为您的代码
int y = 1234;
char *p = &y;
int *j = &y;
现在pointer p
指向y
,但我们已将其声明为指向char
的指针,因此它只会读取一个字节或字符字符串。
二进制文件中的1234
将表示为
00000000 00000000 00000100 11010010
现在如果您的机器是小端,它将存储反转它们的字节
11010010 00000100 00000000 00000000
11010010
位于address 00
Hypothetical address
,00000100
位于address 01
,依此类推。
BE: 00 01 02 03
+----+----+----+----+
y: | 00 | 00 | 04 | d2 |
+----+----+----+----+
LE: 00 01 02 03
+----+----+----+----+
y: | d2 | 04 | 00 | 00 |
+----+----+----+----+
(In Hexadecimal)
现在,如果您取消引用pointer p
,它将仅读取第一个字节,如果-46
,则signed char
,210
unsigned char
根据C标准,普通字符的签名是#34;实现定义。)字节读取将是11010010
(因为我们指出signed char
(在这种情况下它是{{} 1}})。
在您的PC上,负数表示为2's Complement,因此signed char
是符号位。第一位most-significant bit
表示符号。 1
如果您取消引用11010010 = –128 + 64 + 16 + 2 = –46
,它将完全读取pointer j
的所有字节,因为我们声明它是int
的指针,输出将是int
如果将指针j声明为1234
,则int *j
将在此处读取*j
4个字节(取决于机器)。与sizeof(int)
或指针指向它们的任何其他数据类型相同的是,读取的字节大小为char
为1字节。
正如其他人指出的那样,您需要明确转换为char
,因为char*
违反了约束 - char *p = &y;
且char *
不是兼容类型,而是写{{ 1}}。
答案 1 :(得分:10)
编写的代码存在一些问题。
首先,您尝试使用char
转换说明符打印%d
对象的数字表示来调用未定义的行为:
Online C 2011 draft,§7.21.6.1,第9条:
如果转换规范无效,则行为未定义.282)如果有任何参数 不是相应转换规范的正确类型,行为是 未定义。强>
是的,char
类型的对象在传递给可变函数时会被提升为int
; printf
是特殊的,如果您希望输出定义良好,那么参数的类型和转换说明符必须匹配。要使用char
,%d
或unsigned char
打印带有%u
或%o
参数的%x
的数值,您必须使用{ {1}}长度修饰符作为转化规范的一部分:
hh
第二个问题是该行
printf( "%hhd ", *p );
违反约束 - char *p = &y;
和char *
不兼容类型,可能有不同的尺寸和/或表示 2 。因此,必须显式地将源转换为目标类型:
int *
当其中一个操作数为char *p = (char *) &y;
时,会发生此规则的一个异常;那么演员阵容就没有必要了。
说了这么多,我拿了你的代码并添加了一个实用程序来转储程序中对象的地址和内容。以下是void *
,y
和p
在我的系统上的样子(SLES-10,gcc 4.1.2):
j
我在x86系统上,它是little-endian,所以它存储多字节对象,从最低地址的最低有效字节开始:
Item Address 00 01 02 03
---- ------- -- -- -- --
y 0x7fff1a7e99cc d2 04 00 00 ....
p 0x7fff1a7e99c0 cc 99 7e 1a ..~.
0x7fff1a7e99c4 ff 7f 00 00 ....
j 0x7fff1a7e99b8 cc 99 7e 1a ..~.
0x7fff1a7e99bc ff 7f 00 00 ....
在little-endian系统上,被寻址的字节是最不重要的字节,在这种情况下是BE: A A+1 A+2 A+3
+----+----+----+----+
y: | 00 | 00 | 04 | d2 |
+----+----+----+----+
LE: A+3 A+2 A+1 A
(0xd2
无符号,210
签名)。
简而言之,您将打印该单字节的带符号十进制表示。
至于更广泛的问题,表达式的类型 -46
是*p
,表达式的类型 char
是*j
;编译器只是按表达式的类型。编译器在将源转换为机器代码时会跟踪所有对象,表达式和类型。因此,当它看到表达式int
时,它知道它正在处理整数值并适当地生成机器代码。当它看到表达式*j
时,它知道它正在处理*p
值。
<小时/>
答案 2 :(得分:2)
(请注意这个答案是指问题的原始形式,它询问程序如何知道要读取多少字节,等等。尽管有地毯,但仍然保留了这个字节。被从它下面拉出来。)
指针指向内存中包含特定对象的位置,并且必须以特定的步幅大小递增/递减/索引,反映sizeof
指向的类型。
指针本身的可观察值(例如通过std::cout << ptr
)不需要反映任何可识别的物理地址,++ptr
也不需要将所述值增加1,sizeof(*ptr)
或任何其他值其他。指针只是对象的句柄,具有实现定义的位表示。这种表示对用户来说并不重要。用户应该使用指针的唯一方法是......好吧,指向东西。谈论它的地址是不可移植的,只在调试时有用。
无论如何,简单地说,编译器知道读取/写入多少字节,因为指针是键入的,并且该类型具有已定义的sizeof
,表示和映射到物理地址。因此,基于该类型,ptr
上的操作将被编译为适当的指令,以便计算实际硬件地址(同样,不需要对应ptr
的可观察值),阅读右侧sizeof
内存数&#39;字节&#39;,加/减正确的字节数,使其指向下一个对象等。
答案 3 :(得分:1)
首先阅读说的警告 警告:从不兼容的指针类型初始化[默认启用] char * p =&amp; y;
这意味着您应该根据标准§7.21.6.1,子条款9 (由@john Bode指出)
来执行显式类型转换以避免未定义的行为chat *p = (char*)&y;
和
int y =1234;
此处y
是local variable
,它将存储在stack
的{{1}}部分。在Linux中,机器整数根据{{1}存储在内存中格式。假设为RAM
保留的little endian
内存来自4 bytes
到y
0x100
如上所述,0x104
和 -------------------------------------------------
| 0000 0000 | 0000 0000 | 0000 0100 | 1101 0010 |
-------------------------------------------------
0x104 0x103 0x102 0x101 0x100
y
p
j
都指向同一地址j
但编译器将执行p
,因为0x100
为*p
默认情况下,它会检查p
,此处signed character pointer
为sign bit
表示有一件事是确定要打印的输出是负数。
如果sign bit
为1
,即负数且负数存储在内存中为2的赞美那么
sign bit
在打印时,如果您使用的1
格式说明符用于打印 actual => 1101 0010 (1st byte)
ones compliment => 0010 1101
+1
------------
0010 1110 => 46 and since sign bit was one it will print -46
等效项,则会%u
检查unsigned
t,最后not
中的数据是sign bi
1}}被打印出来。
终于
1 byte
在上面的语句中,在解除引用printf("%d\n",*j);
时默认为j
及其signed pointer
指针,因此它会检查第31位 符号,int
表示输出为0
否,即1234。