以下这些题目是我曾遇到过的,觉得有深度的问题,对题目的解析绝大部分是本人的思考(引用的已注明出处),可能有不对的地方,希望大家不吝指正。
(PS:如果您对以下题目表示无压力,只能说您对C有一定的理解,至于是否精通C,不能由此文判断。)参考答案在文末。
1★先来个简单的:
~~~
#include <stdio.h>
int main(void)
{
int a[3][2] = { (0,1), (2,3), (4,5) } ;
int *p ;
p = a[0] ;
printf(“%d”, p[0] ) ;
}
~~~
仔细看看花括号里面嵌套的是小括号,而不是花括号。即这花括号里嵌套了逗号表达式。(这考的是眼力^_^)
2★ int a[10]; 问下面哪些不可以表示 a[1] 的地址?
A. a+sizeof(int) B. &a[0]+1 C. (int*)&a+1 D. (int*)((char*)&a+sizeof(int))
//Tecent某年实习生笔试题目
此题对于理解了指针与数组的同学来说,很easy。(关于数组和指针那点事,可浏览本博客, [点这里](http://blog.csdn.net/yang_yulei/article/details/8071047))
3★下面的C程序是合法的吗?如果是,那么输出是什么?
~~~
#include <stdio.h>
int main()
{
int a=3, b = 5;
printf(&a["Ya!Hello!how is this? %s\n"], &b["junk/super"]);
printf(&a["WHAT%c%c%c %c%c %c !\n"], 1["this"],
2["beauty"],0["tool"],0["is"],3["sensitive"],4["CCCCCC"]);
return 0;
}//来源于酷壳网http://coolshell.cn/articles/945.html
~~~
本例主要展示了一种另类的用法。下面的两种用法是相同的:
“hello”[2]
2["hello"]
如果你知道:a[i] 其实就是 *(a+i)也就是 *(i+a),所以如果写成 i[a] 应该也不难理解了。
4★ 32 位机上根据下面的代码,问哪些说法是正确的?(多选题类型)
~~~
signed char a = 0xe0;
unsigned int b = a;
unsigned char c = a;
~~~
A. a>0 && c>0 为真 B. a == c 为真 C. b 的十六进制表示是:0xffffffe0 D. 上面都不对
//Tencent某年实习生笔试题目
此题深入地考察了C的类型转换方式。(此为多选题类型,一般人不敢确定他的答案是正确的)
A 错:a 是负数,c 是正数,跟 0 比较要转换到 int。
signed char a 其实也就是char a,其转换到int负数还是负数(高位填充1)转换后结果为0xFFFFFFE0
unsigned char c 也为0xE0,但其是正数(signed char转unsigned char 底层位不变 只是改变了解释规则)
unsigned char 转int,正数还是正数(高位填充0)转换后结果为0x000000E0
B 错:B错?首先说 a 和 c 的二进制表示一模一样,都是 0xe0,那么比较就不相等?!是的。
一个char型和一个unsigned char比较,其中的类型如何转换?
C语言的整型提升规则:C的整型算数运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型。
本博的早期文章:http://blog.csdn.net/yang_yulei/article/details/8068210
所以,a == c中,a和c都要先转换成int型,再比较。有A选项分析知,a转int型为负数,b转int型为正数,故它俩不等。
C 对:C对?C 怎么就对了?a 是一个 signed char,赋值给 unsigned int 的 b,前若干个字节不是补 0 吗?
但实际情况是:首先 signed char 转换为 int,然后 int 转换成 unsigned int,所以最初是符号扩展,然后一个 int 赋值给了 unsigned int(其实还是整型提升规则)
D 不解释。
至于对这个问题的解释,我认为(只是我认为,不是权威的解释)是因为你使用的是32位的操作系统,我们说的操作系统的位数其实值的是CPU GPRs(General-Purpose Registers,通用寄存器)的数据宽度为32位,32位指令集就是运行32位数据的指令,也就是说处理器一次可以运行32bit数据。你传递参数的时候输入指定的格式为%hd,但是压栈的时候还是压入了32bit的数据,只不过高位是0。要不然我们为什么经常会说c语言中字节对齐的问题?
5★ 下面程序的输出结果(32位小端机)
~~~
#include <stdio.h>
int main()
{
long long a = 1, b = 2, c = 3;
printf("%d %d %d\n", a, b, c);
return 0;
}
//Tencent某年实习生笔试题目
~~~
//以下是长篇大论
首先,sprintf/fprintf/printf/sscanf/fscanf/scanf等这一类的函数,它们的调用规则(calling conventions)是cdecl,cdecl调用规则的函数,所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。函数参数的传递都是放在栈里面的,而且是从右边的参数开始压栈,printf()是不会对传递的参数进行类型检查的,它只有一个format specification fields的字符串,而参数是不定长的,所以也没办法对传递的参数做类型检查,也没办法对参数的个数进行检查。所以了,压栈的时候,参数列表里的所有参数都压入栈中了,它不知道有多少个参数,所以它都压栈。
那么问题来了:编译器是怎么去定义压栈的行为的?是先把这longlong类型转换为int型再压栈么?还是直接压栈?
在32位机器上,64位的整数被拆分为两个32位整数,printf会把64位的按照两个32的参数来处理。此时printf会认为实际的参数为6个,而不是3个。
c,b,a压入之后,在最低的12字节处是a和b,a占2*4个bytes,b占1*4个byte。b先压入栈,a后压入栈。但是为什么a的布局是这样的?因为这是little endian,即每个数字的高字节在高地址,低字节在低地址。而栈的内存生长方向是从大到小的,也就是栈底是高地址,栈顶是低地址,所以a的低字节在低地址。(有条件的同学可以在big endian的机器上验证一下)
那么输出的时候,format specification fields字符串其匹配栈里面的内容,首先一个%d取出4个bytes出来输出,然后后面又有一个%d再取出4个bytes出来打印。所以结果就是这样了。也就是说刚开始压入栈的b的值在输出的时候根本都没有用到。
总结:
printf在压栈时,对于长度小于32位的参数,自动扩展成32位(由CPU的位数决定的)。
故在根据格式串解释时,对于%c %hd这样的小于32位数据的格式串,系统也会自动提取32位数据解释,而不会提取8位或16位来解释。(因为你把人家压入的时候就规定了扩展成32位嘛)
至于浮点参数压栈的规则:float(4 字节)类型扩展成double(8 字节)入栈。所以在输入时,需要区分float(%f)与double(%lf),而在输出时,用%f即可。printf函数将按照double型的规则对压入堆栈的float(已扩展成double)和double型数据进行输出。
至于longlong参数的规则:在32位机上,64位整数被拆分为两个32位整数压栈,在64位机上不存在这个问题,64位机上本题输出1,2,3
PS:
关于longlong型的输出:和平台和编译器有关
在**windows下需要用__int64配合%I64d**。而在**UNIX、Linux中必须使用标准C规定的long long配合%lld。**
unsigned __int64b= 9223372036854775808ll
printf("%I64u",b);
6★下面这段代码会挂么?会挂在哪一行?
~~~
#include <stdio.h>
struct str{
int len;
char s[0];
};
struct foo {
struct str *a;
};
int main(int argc, char**argv) {
struct foo f={0};
if (f.a->s) {
printf( f.a->s);
}
return 0;
}
~~~
//详细分析请见本博客,[点这里](http://blog.csdn.net/yang_yulei/article/details/23395315)。
7★请问下面的程序的输出值是什么?
~~~
#include <stdio.h>
#include <stdlib.h>
#define SIZEOF(arr)(sizeof(arr)/sizeof(arr[0]))
#define PrintInt(expr)printf("%s:%d\n",#expr,(expr))
int main()
{
/* The powers of 10*/
int pot[] = {
0001,
0010,
0100,
1000
};
int i;
for(i=0; i<SIZEOF(pot); i++)
PrintInt(pot[i]);
return 0;
}//来源于酷壳网http://coolshell.cn/articles/945.html
~~~
参考答案:好吧,如果你对于PrintInt这个宏有问题的话,你可以百度一下。不过,本例的问题不在这里,其实很简单了,以C/C++中,以0开头的数字都是八进制的。
8★请问下面的程序的输出值是什么?
~~~
#include <stdio.h>
int main(void)
{
char a[1000] ;
int i ;
for(i=0; i<1000; i++)
{
a[i]= -1-i ;
}
printf(“%d”,strlen(a)) ;
return 0 ;
}
~~~
按照负数补码规则,可知-1的补码为0xff,当i值为127时,a[127]的值为-128,此时右边整型转换后,正好是左边char型能够表示的最小负数。当i继续增加,右边为-129,对应的十六进制数为0xffffff7f 而char只有8位,故转换时高位被丢弃 左边得到0x7f。当i继续增加到255时,-256的低8位为0。然后当i增加到256时,-257的低8位为0xff 如此又开始一轮的循环。
从上面分析可知:a[0]到a[254]里面的值都不为0,而a[255]的值为0. 故strlen(a)为255
【char默认是有符号的,其表示的值的范围为[-128,127]】
9★在X86系统下,输出的值为多少?
~~~
#include <stdio.h>
int main(void)
{
int a[5]={1,2,3,4,5} ;
int* ptr1 =(int *)(&a+1) ;
int* ptr2 =(int *)((int)a+1) ;
printf(“%x,%x”,ptr1[-1], *ptr2) ;
return 0 ;
}
~~~
若对于指针ptr1和ptr2具体指向不明白的,[请点击这里浏览。。](http://blog.csdn.net/yang_yulei/article/details/8071068)
对于ptr[-1]的值为5,没什么好说的了。
此题主要涉及的是大小端的问题,intel机器一般是小端模式,即:例如对整数来说,内存中的低地址字节存储整型的低地址部分,内存中的高地址字节存整型的高地址部分。(与我们平时的书写顺序相反)
a[0] a[1]在内存中的存储为:(地址从小到大增长)0x1000 0000 0x2000 0000
ptr2指向a[0]的第二个字节处,且它为int型指针,故提取从a[0]的第二个字节开始的后面4个字节。在内存中即为0x10**000000 0x20**00 0000(阴影部分) 打印来为0x2000000
10★
~~~
(*(void(*)( ))0 )( ) 这是什么?
~~~
//可参考:[请点击这里浏览函数指针部分](http://blog.csdn.net/yang_yulei/article/details/8071068)
这个是《C陷阱与缺陷》中的一个例子。
从内层到外层分析:
1,void(*)()这是一个函数指针。 这个函数没有返回值也没有参数
2,void(*)()0这是将整型0强制转换为函数指针类型。(即:0号地址处开始存储着一段函数)
3,(*(void(*)( ))0 ) 取出0号地址处的函数
4,(*(void(*)( ))0 )( ) 调用0号地址处的函数。
【由此可见 指针和强制类型转换联手双剑合璧威力无穷! 可以实现汇编级的操作】
**【相信程序员,不阻止程序员做他们想做的事】**
11★请问下面的程序的输出值是什么?
~~~
#include <stdio.h>
int main()
{
int a[5][5] ;
int (*p)[4] ;
p=a ;
printf("%d", &p[4][2]-&a[4][2]) ;
return 0 ;
}
~~~
[详细分析请点击](http://blog.csdn.net/yang_yulei/article/details/8071068)
12★
~~~
struct S
{ char c ;
int i[2];
double v ;
} ;
~~~
在windows系统下成员i的偏移量是多少?在Linux系统下i的偏移量是多少?
[//请点击这里参考](http://blog.csdn.net/yang_yulei/article/details/8072567)
13★下面代码中有BUG,请找出:
~~~
int tadd_ok(int x, int y) //判断加法溢出
{
int sum = x+y ;
return (sum-x == y) && (sum-y == x) ;
}
int tsub_ok(int x, int y) //判断减法溢出
{
return tadd_ok(x, -y) ;
}
//此题来源于等重量黄金价值的书——《深入理解计算机系统》
~~~
有两个BUG:
1,tadd_ok判断加法溢出时,无论是否溢出,(x+y)-x都==y,若溢出结果sum再减y 再溢出得x。正确实现应为:分两种情况——正数运算是否溢出(即两正数相加为负数),负数运算是否溢出(即两负数相加为正数)。有两种情况之一者即判断溢出。
2,return tadd_ok(x, -y) ;当y为TMIN即int型所能表示的最小值,由于补码范围的不对称性,int的最小值无对应的正值,故y取TMIN时,对其取反会发生溢出,溢出的结果还是TMIN。
这样就导致判断结果错误。
14★
~~~
编写一些代码,确定一个变量是有符号数还是无符号数:
~~~
//来源于《C专家编程》
参考答案:
无符号数的本质特征是它永远不会是负的。
用宏定义的形式:#define ISUNSIGNED (type) ( (type)0 –1> 0 )
//也有其它方法实现,不过个人认为这种方法最简洁。
15★
再来一发关于复杂指针声明的,这是C中的难点:
写出变量abc的核心是什么,并用多个typedef改写下面的声明式
~~~
int *(*(*(*abc) ( ) ) [6]) ( ) ;
~~~
解析:
abc是一个函数指针,这类函数接收0个参数,返回一个指针,这个指针指向一个具有6个元素的数组,数组里的每个元素是函数指针,这类函数接收0个参数,返回值为int* 类型。abc的定义同下:
typedef int* (*type1)();
typedef type1 (*type2)[6];
typedef type2 (*type3)();
type3 abc;
【从内到外解读声明,从外到内typedef】
**关于变量的复杂声明:**
从外到内,层层剥开,先找核心,再向右看。(个人总结的,若有不妥请指正)
找到核心变量后,从右向左读。
* 读作”指向…的指针”
[] 读作”…的数组”
() 读作”返回…的函数”
简单的例子:
**int *f() ; // f: 返回指向int型的指针**
步骤:
1)找标识符f:读作”f是…”
2)向右看,发现”()”读作”f是返回…的函数”
3)向右看没有什么,向左看,发现*,读作”f是返回指向…的指针的函数”
4)继续向左看,发现int,读作”f是返回指向int型的指针的函数”
**int (*pf)() ; // pf是一个指针——指向返回值为int型的函数**
1)标识符pf,读作“pf是…”
2)向右看,发现),向左看,发现\*,读作 “pf是指向…的指针”
3)向右看,发现”()”,读作“pf是指向返回…的函数的指针”
4)向右看,没有,向左看发现int,读作”pf是指向返回int型的函数的指针
**部分答案:**
1题、答案应该是1.
2题、答案为A。
3题、本例是合法的,输出如下:
Hello! how is this? super
That is C !
4题、略
5题、结果为:1,0,2
6题、略
7题、本例的输出会是:1,8,64,1000
8题、答案是:255
8题、答案:5,2000000
9题、略
10题、略
11题、答案-4
12题、略
13题、略