嵌入式C语言经典面试

【预处理器】

1.用处理器指令#define声明一个常数,用以表明1年中有多少秒(忽略闰年问题)。

 
 
  1. #define SECONDS_RER_YEAR  (60 * 60 * 24 * 365)UL 

我在这想想看到几件事情:

1)#define语法的基本知识(例如:不能以分号结束,括号的使用,等等);

2)懂得预处理器将为你计算常数表达式的值,直接写出你是如何计算的而不是写上计算出的实际值,是更清晰而没有代价的;

3)意识到这个表达式将使一个16位机得整形数溢出,因此要用到长整型符号L,告诉编译器这个常数是长整型数;

4)如果进而能在表达式中用到UL(标识无符号长整型),那么你就有了一个好的起点。

2.写一个“标准宏”MIN,这个宏输入两个参数并返回较小的一个。

 
 
  1. #define MIN(A,B)  ((A<B) ? (A) : (B))  

这个测试是为下面的目的而设的:

1)标识#define在宏中应用的基本知识。这是很重要的,因为在嵌入(inline)操作符变为标准C的一部分之前,宏是方面产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法;

2)三重条件操作符知识。它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的;

3)懂得在宏中小心地把参数用括号括起来;

4)这个问题也可以用来作为讨论宏的副作用的开始,例如:当你写下面的代码时会发生什么事?

 
 
  1. least = MIN(*p++, b); 

3.预处理器标识#error的目的是什么?

如果你不知道答案,请参考文献1。这个问题用来区分一个正常的哥们和一个书呆子。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。

【死循环】

4.嵌入式系统中经常要用到无限循环,你怎么样用C编写实现。

这个问题有几种解决方案,首先方案是:

 
 
  1. while(1) { 
  2.     .... 

还有两种如下:

 
 
  1. for( ; ; ) { 
  2.     ... 
  3.  
  4. Loop: 
  5.     ... 
  6. goto Loop; 

【数据声明】

5.用变量a给出下面的定义:

1)一个整形数(An interger);

 
 
  1. int a; 

2)一个指向整型数的指针(A pointer to an interger);

 
 
  1. int *a; 

3)一个指向指针的指针,它指向的指针是一个指向一个整型数(A pointer to a pointer to an integer);

 
 
  1. int **a; 

4)一个有10个整型数的数组(An array of 10 intergers);

 
 
  1. int a[10]; 

5)一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointer to integers);

 
 
  1. int *a[10]; 

6)一个指向有10个整型数数组的指针(A pointer to an array of 10 integers);

 
 
  1. int (*a)[10]; 

7)一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and return an integer);

 
 
  1. int (*a)(int); 

8)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(An array of ten pointers to functions that take an integer as argumnent and return a integer);

 
 
  1. int (*a[10])(int); 

【static】

6.关键字static的作用

在C语言中,关键字static有三个明显的作用:

1)在函数体内,一个被声明为static的变量在这一函数被调用过程中维持其值不变;

2)在模块内(函数体外),一个被声明为static的变量可以被模块内所有函数访问,但不能被模块外的其他函数访问,它是一个本地的全局变量;

3)在模块内,一个被声明为static的函数只可被这一模块内其他函数调用,即这个函数被限制在声明它的模块的本地范围内使用。

【const】

7.关键字const的作用。

通常理解中“const意味着常数”其实是一个误区。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么。一般可以理解const意味"只读"。引申开来的附加问题,下面的声明都是什么意思?

 
 
  1. const int a;  
  2. int const a;  
  3. const int *a;  
  4. int * const a;  
  5. int const * a const;  

前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是 不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。

顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1)
关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理 其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的);

2)通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码;

3)合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

【volatile】

8.关键字volatile有什么含意,给出三个不同的例子。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

下面是volatile变量的几个例子:

1) 并行设备的硬件寄存器(如:状态寄存器);
2)
一个中断服务子程序中会访问到的非自动变量(Non-automatic variables);
3)
多线程应用中被几个任务共享的变量。

这个问题是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量,不懂得volatile的内容会带来灾难。

假设被面试者正确地回答了这个问题,我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性:

1) 一个参数既可以是const还可以是volatile吗?解释为什么。

   是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2)
一个指针可以是volatile 吗?解释为什么。

   是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。
3)
下面的函数有什么错误:

 
 
  1. int square(volatile int *ptr)  { 
  2.     return *ptr * *ptr; 

这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

 
 
  1. int square(volatile int *ptr) {  
  2.     int a,b;  
  3.     a = *ptr;  
  4.     b = *ptr;  
  5.     return a * b;  
  6. }  

由于*ptr的值可能被意想不到地该变,因此ab可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

 
 
  1. long square(volatile int *ptr) { 
  2.     int a; 
  3.     a = *ptr; 
  4.     return a * a; 

【位操作】

9.嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit3,第二个清楚a的bit3。在以上两个操作中,要保持其他位不变。

对这个问题有三种基本的反应:
1)
不知道如何下手。该被面者从没做过任何嵌入式系统的工作;
2)
bit fieldsBit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边!
3)
#defines bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。

最佳的解决方案如下:

 
 
  1. #define BIT3 (0x1 << 3
  2. static int a; 
  3. void set_bit3(void) { 
  4.     a |= BIT3; 
  5. void clear_bit3(void) { 
  6.     a &= ~BIT3; 

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=&=~操作。

【访问固定的内存位置】

10.嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器,写代码完成这个任务。

这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换 typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:

 
 
  1. int *ptr; 
  2. ptr = (int *)0x67a9; 
  3. *ptr = 0xaa55

一个较晦涩的方法是:

 
 
  1. *(int * const)(0x67a9) = 0xaa55; 

 【中断】

11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具体代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

 
 
  1. __interrupt double compute_area (double radius) {  
  2.     double area = PI * radius * radius;  
  3.     printf("\nArea = %f", area);  
  4.     return area;  
  5. }  

 这个函数有太多的错误了,以至让人不知从何说起了:
1)ISR 不能返回一个值;
2) ISR
不能传递参数;
3)
在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的;
4)
与第三点一脉相承,printf()经常有重入和性能上的问题。

【代码例子】

12.下面的代码输出是什么,为什么?

 
 
  1. void foo(void) {  
  2.     unsigned int a = 6;  
  3.     int b = -20;  
  4.     (a+b > 6) ? puts("> 6") : puts("<= 6");  

这个问题测试你是否懂得C语言中的整数自动转换原则,答案是输出是 ">6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式 计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。

13.评价下面的代码片段。

 
 
  1. unsigned int zero = 0;  
  2. unsigned int compzero = 0xFFFF;   

对于一个int型不是16 的处理器为说,上面的代码是不正确的。应编写如下:

 
 
  1. unsigned int compzero = ~0; 

这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避免的烦恼。

【动态内存分配】

14.尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?

这里,面试者被期望能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger, 他的解释远远超过笔者这里能提到的任何解释),所有回过头看一下这些杂志吧!

面的代码片段的输出是什么,为什么?

 

 
 
  1. char *ptr;  
  2. if ((ptr = (char *)malloc(0)) == NULL)   
  3.     puts("Got a null pointer");  
  4. else  
  5.     puts("Got a valid pointer");  

最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是"Got a valid pointer"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。

 

【typedef】

15.Typedef在C语言中用以声明一个已经存在的数据类型的同义字。也可用预处理器做类似的事。

思考一下下面的例子:

 
 
  1. #define dPS struct s*;  
  2. typedef struct s* tPS; 

以上两行代码的意图都是要定义dPS和tPS为一个指向结构体s的指针,哪种方法更好?为什么?

这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:

 
 
  1. dPS ptr1, ptr2  
  2. tPS prt1, ptr2  

第一行被扩展为:

 
 
  1. struct s *ptr1, ptr2 

面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这显然不是想要的。第二个例子正确地定义了p3 p4 两个指针。

【晦涩的语法】

16.C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?

 
 
  1. int a = 5b = 7, c;  
  2. c = a+++b;  

这个问题将做为这个测验的一个愉快的结尾。 不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:

 
 
  1. c = a++ + b; 

因此, 这段代码持行后a = 6, b = 7, c = 12

【参考文献】

1) Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.
2) Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值