总结下网上的嵌入式C面试"葵花宝典"
1、用预处理指令“#define”声明一个常数,用来表示一年中有多少秒:
答案:#define SECOND (365*24*60*60)UL
详解:
这个其实就是宏定义。(365*24*60*60),就是:(365天*24小时*60分*60秒),怎么样,够清楚了吧;再有就是后面的“UL”,后面加个“UL”的意思是,这个常数在存储器中要按照“无符号长整型”来存储,不过,我试过,貌似有些编译器要是在后面加上这个“UL”会报错,具体还是大家自己试验吧。
2、写一个标准宏"MIN",让这个宏可以输入两个参数,并且返回较小的一个。
答案:#define MIN(A,B) ((A)<(B)?(A):(B))
详解:
在这里用到了C语言中的唯一一个三目运算符“:”,意思就是:如果“A<B”这个条件成立,输出“A”,不成立则输出“B”,需要注意的是,我们需要老老实实的使用括号,万一你写的可复杂,造成各种运算符之间优先级混乱,最终会导致输出错误!
另外,大家看下这个宏定义有什么问题:“MIN(*p++,b)”,如果将这个式子按上面定义的宏定义运算,会出现什么问题呢?
答案:如果按照上面的宏定义展开,会是这样子:“MIN(*p++,b)”会被替换为:“((*p++)<(b)?(*p++),b)”,看懂了吧!本来是,如果(*p++)<(b),那就把(*p++)的值输出,但是,实际上输出的却又是一个"*p++",这样,等于就++了两次,结果不会是你想要的。在此,我们得牢记了,宏定义就是纯粹的、硬生生的把你定义的东西展开,它不管你写的什么东西的。
3、预处理器标示“#error”的目的是什么?
答案:看到这东西,大家都很熟悉,也都很烦它吧?是的,它就是总是出现的编译错误提示,其实我们自己也可以搞一些编译错误,看程序:
#include <stdio.h> #define SEC 1993 //这里定义成1993 int main() { #if(SEC!=1994) //我们想要的条件是SEC==1994(但上面我写成了1993),如果不是1994,接下来下面的"#error"就有用了 #error "SEC Not 1994" //上面条件不满足,就输出这句错误提示 #endif return 0; }
上面程序在编译的时候,就会出错了,这个错误是我自己设定的,如下图:
怎么样,这种错误看见就烦吧,不过这个错误看见不会烦,因为这是我让它出错的,不出错才有问题了。。。
4、数据声明:
其他论坛或者博客什么的地方也见过了,不打算总结前几个简单的,对后面几个相对难点的做下笔记:
e、一个有10个整型指针的数组;
f、一个指向有10个整型数数组的指针;
g、一个指向函数的指针,该函数有一个整型参数并返回一个整型数;
h、一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数;
答案:
e、int *a[10];
详解:“a”是一个数组,并且里面存放的是10个整型指针类型的数据;
f、int(*a)[10];
详解:“a”是指针,指向一个数组,另外,此数组是一个有10个int型元素的数组。
g、int (*a)(int);
详解:刚开始看到的时候,我也有些迷惑,经常见到的函数都是“int a(int x);”这种形式,但这里却是"int (*a)(int);"前面的“*a”还容易理解,这跟上面的数组指针一样嘛,但是后面却只有一个“(int)”了,刚看到的时候一直在想为什么这样。。。今天突然一想,C语言函数形参在声明的时候是可以只写一个类型名的,后面可以不写具体变量名(但是实际定义的时候就要写了),所以他这个是一个省略的写法:“int (*a)(int);”,我再写一个看起来没有那么绕的:int (*a)(int x);或者:int (*fun)(int i);这样就看清楚了。
h、int (*a[10])(int);
详解:
5、关键字“static”的作用是什么?
我的理解:
1、在子函数内,static修饰后,函数退出时,变量只初始化一次,而且不会释放,下次调用时,可以接着用;如:
1 #include <stdio.h> 2 3 void fun1() 4 { 5 static int i=0; 6 printf("%d ",i); 7 i=3;//函数退出前,将"i"=3,第二次调用时,“i”的值不会是1,而是这个3 8 } 9 10 int main() 11 { 12 fun1();//第一次调用 13 fun1();//第二次调用 14 return 0; 15 }
输出结果如下:
2、在一个文件内的变量加“static”修饰,则只在这个文件内有用,即使是全局的变量,仍然只在这个文件内有效;
6、关键字“const”有什么含义?
详解:“const”意味着只读;
1 #include <stdio.h> 2 3 const int i=0; 4 5 int main() 6 { 7 i=1;//"i"被"const"修饰过后,变为只读,在这里重新复制,就会报错 8 }
关于"const"的其它例子:
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型 数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数 是不可修改的,同时指针也是不可修改的)。
7、关键字“volatile”有什么作用?
一个被定义为“volatile”的变量是说这个变量可能会被意想不到的改变,这样,编译器就不会去假设这个变量的值了。准确的说,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
8、访问特定内存:
在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
代码:
1 #include <stdio.h> 2 3 int main() 4 { 5 int *ptr=NULL; 6 ptr=(int *)0x67a9; 7 *ptr=0xaa66; 8 printf("%d ",*ptr); 9 }
以上代码在实际测试中,运行后立马出错,估计是测试的时候,"0x67a9"这个地址里面的内容被修改后电脑哪里出了问题,但是其实上面这种写法,已经实现了。
9、位操作,给定一个整型变量"a",写两段代码,第一个设置"a"的 bit 3,第二个清除"a"的 bit 3。在以上两个操作中,要保持其它位不变。
1、a |= 0x1<<3;
2、a &= ~(0x1<<3);
10、关于中断服务子程序的注意事项。评价下面的代码:
1 __interrupt double compute_area (double radius) 2 { 3 double area = PI * radius * radius; 4 printf(" Area = %f", area); 5 return area; 6 }
上面这个中断服务程序有很多错误,下面,就以这个例子来对中断服务子函数做下总结:
1、中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字--“__interrupt”。下面的代码就使用了“__interrupt”关键字去定义了一个中断服务子程序(ISR)。
2、ISR 不能传递参数,中段函数往往是由硬件产生的,不是由其它函数调用的,这样以来要返回值干嘛??返回给谁???所以上面的形参和返回值都是错的。
3、在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4、与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。
11、代码例子 1,下面的代码输出是什么,为什么?
1 void foo(void) 2 { 3 unsigned int a = 6; 4 int b = -20; 5 (a+b > 6) ? puts("> 6") : puts("<= 6"); 6 }
这段代码的输出结果是:"> 6"。
这段代码测试你是否懂得C语言中的整数自动转换原则,当表达式中存在有符号类型和无符号类型时,所有的操作数都会自动转换为无符号类型,因此"-20"转换成了一个正整数,它与 "a(=6)" 相加后,结果肯定大于 6。
12、评价下面的代码片段:
1 unsigned int zero = 0; 2 unsigned int compzero = 0xFFFF; 3 /*1's complement of zero */
乍一看,没什么问题,其实也就是没什么问题。
我刚看过网上那个人写到的这个问题后,搞明白了他想说什么。
第一个变量赋值为"0",这段代码在哪写都对,但第二个变量,如果你试图给变量 "compzero"的每一位置 1,在16位系统中,这样写没错,“0xFFFF”刚好是16个 1,如果在32为系统中呢?那就不对了,这样写你只能给前16位或后16位置1,而且在8位系统中,还会被警告有溢出风险。如果你想让你得代码有好的可移植性,就是说,第二种变量赋值,在哪写都对,那应该这样写:
1 unsigned int compzero = ~0;
这样写,就是把"0"取反,赋值给变量 "compzero"。
13、动态内存分配(Dynamic memory allocation)
1 #include<stdio.h> 2 #include<stdlib.h> 3 4 int main() 5 { 6 char *ptr; 7 if ((ptr = (char *)malloc(0)) == NULL) 8 puts("Got a null pointer"); 9 else 10 puts("Got a valid pointer"); 11 }
上面代码会输出什么呢?
看到"malloc(0)"参数填写的是0的时候,是不是以为malloc仍然后返回一个空指针?其实,就算申请的空间是0,malloc也仍然是起到作用了,仍然能返回一个有效的空间地址,所以这段代码返回的是:
14、Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一个扩展为
struct s * p1, p2;
上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。
15、晦涩的语法
int a = 5, b = 7, c;
c = a+++b;
这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:
c = a++ + b;
因此, 这段代码持行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。