相当多的程序员在求职时,都比较反感笔试,这其中有很大一部分原因是笔试题目常常比较“奇葩”,且几乎没有实践价值,更像是为了考倒求职者而出的。但是不得不承认,这些“奇葩”的问题也的确可以在一定程度上检验一个人的技术功底,对于我们,倒是一个检查自己知识盲点的好机会。
来看这道面试题
来看看这个中国某著名通信企业S公司的面试题:下面的C语言程序会输出什么?
#include <stdio.h>
int main()
{
unsigned int a = 0xf7;
unsigned char i = (unsigned char)a;
char* b = (char*)&a;
printf("%08x, %08x\n", i, *b);
return 0;
}
这里仅仅只是做了一些数据类型转换而已,并没有其他额外额操作。那么,该如何分析呢?老规矩,得到答案最简单粗暴的方法就是写好C语言程序,编译执行一次,在我的机器上,上述程序输出结果如下:
# gcc t.c
# ./a.out
000000f7, fffffff7
小伙伴们做对了吗?
“大端存储”和“小端存储”
首先应该明白的是,现代计算机存储数据的最小粒度是“字节”,也即计算机是逐字节存储数据的。C语言中的各种数据类型(如 char、int、double 等)之间最显著的区别就在于,它们存储数据时占用的内存空间不同。
现在来考虑变量 a 在内存中的存储。a 是 unsigned int 类型的,而C标准并没有定义 int 类型的数据占用多少字节内存,在我的机器上,int 类型占用 4 个字节,所以下面就以 int 占用 4 字节为例做分析了。
0xf7 仅占用一个字节的内存空间,而 a 占用了 4 个字节,a = 0xf7; 其实是将 0x000000f7 赋值给 a,那 a 在内存中是怎样存储 0x000000f7 的呢?请看下图:
有两种存储方式:一种是将数据的低位放在低地址单元,高位放在高地址单元,这就是所谓的数据“小端存储”方式。还有一种数据的“大端存储”方式则是反过来的,数据的高位被放在低地址单元,低位则被放在高地址单元。我的机器的数据存储方式属于“小端存储”,以下分析都是基于“小端存储”的。
进一步分析
知道了数据的两种存储方式后,再做进一步的分析就简单了。变量 i 是无符号 char 型的,它只占用一个字节的内存,i = (unsigned char)a; 的意思就是把 a 的最低一字节赋值给 i,赋值完成后 i 就等于 0xf7 了,经过 printf 的格式化输出,i 对应的显然会输出 000000f7。
接着分析:char* b 定义了一个有符号的 char 型指针变量 b, (char
)&a 可以分开看:&a 是取 a 的地址,加上(char) 即再将其强制转换为 char* 类型指针。这么看来,char* b = (char*)&a; 赋值完成后,b 就指向 0xf7 这个数据了。
那为什么程序最后输出的不是 f7,而是 fffffff7 呢?
b 是有符号的 char 型指针变量,那么对于计算机来说, * b 从内存中取出的 0xf7 也是有符号的数据。对于有符号的数值,最高位是符号位,0xf7 的最高位为 1,显然它是一个负数。事实上,如果将 printf 中 * b 对应的 %08x 改为 %d,再编译 C语言程序并执行,结果如下:
# ./a.out
000000f7, -9
容易看出,* b 其实等于 -9。但是,这也没能解释为什么最终程序会输出 fffffff7,而不是 000000f7 啊?这就是 printf 在“搞鬼”了。我们查看 printf 的C语言源代码,如下:
296 int printf(const char *fmt, ...)
- 297 {
| 298 char printf_buf[1024];
| 299 va_list args;
| 300 int printed;
| 301
| 302 va_start(args, fmt);
| 303 printed = vsprintf(printf_buf, fmt, args);
| 304 va_end(args);
| 305
| 306 puts(printf_buf);
| 307
| 308 return printed;
| 309 }
显然,核心是 vsprintf() 函数,继续跟踪, vsprintf() 函数的C语言源代码如下:
int vsprintf(char *buf, const char *fmt, va_list args)
{
...
if (qualifier == 'l')
num = va_arg(args, unsigned long);
else if (qualifier == 'h') {
num = (unsigned short)va_arg(args, int);
if (flags & SIGN)
num = (short)num;
} else if (flags & SIGN)
num = va_arg(args, int);
else
num = va_arg(args, unsigned int);
str = number(str, num, base, field_width, precision, flags);
}
*str = '\0';
return str - buf;
}
可以看出,对于 %x,printf 实际上会将对应的整数转换为 unsigned int 类型。这也就是说,虽然 * b 从内存中只取出一个字节的数据,printf 还是会使用 4 个字节来表示它。这就明白了,用 4 个字节表示 -9,显然应该是 0xfffffff7。
至此,我们就完全弄懂了这个题目。当然,得到答案并不是目的,发现自己的知识盲点,巩固自己的基础,这才是初衷。