分类: 其他平台
2015-09-26 20:25:42
全面解析《嵌入式程序员应该知道的16个问题》
----Sailor_forever分析整理,sailing_9806@163.com
http://blog.csdn.net/sailor_8318/archive/2008/03/25/2215041.aspx
1、预处理器(Preprocessor)
2、如何定义宏
3、预处理器标识#error的目的是什么?
4、死循环(Infinite loops)
5、数据声明(Data declarations)
6、关键字static的作用是什么?
7、关键字const有什么含意?
8、Volatile的使用
9、位操作(Bit manipulation)
10、访问固定的内存位置(Accessing fixed memory locations)
11、中断(Interrupts)
12、符号扩展的代码例子(Code examples)
13、处理器字长导致的数据扩展问题
14、动态内存分配(Dynamic memory allocation)
15、用Typedef构造复合类型
16、晦涩的语法及代码风格
C语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织了许多这种测试,在这过程中我意识到这些测试能为面试者和被面试者提供许多有用信息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。
从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对ANSI标准细节的知识而不是技术技巧而设计吗?这是个愚蠢的问题吗?如要你答出某个字符的ASCII值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这标志着出题者也许花时间在微机上而不是在嵌入式系统上。如果上述任何问题的答案是"是"的话,那么我知道我得认真考虑我是否应该去做这份工作。
从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解应试者C语言的水平。不管怎么样,看一下这人如何回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口呢,还是表现出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们的测试成绩一样有用。
有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给正在找工作的人一点帮助。这些问题都是我这些年实际碰到的。其中有些题很难,但它们应该都能给你一点启迪。
这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问题的偏好,每个问题没有分配分数,如果选择这些考题为你所用,请自行按你的意思分配分数。
用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL(大小写都行,常量后面可以加此标志,宏的命名风格要大写,多个之间用下划线)
我在这想看到几件事情:
1) #define 语法的基本知识(例如:不能以分号结束,括号的使用(表达式、参数等要括起来),等等)
2)懂得预处理器将为你计算常数表达式的值(难道不是替换么,先算再替?会将常数合并),因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3) 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
4) 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
考点:(表达式、参数等要括起来)
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
这个测试是为下面的目的而设的:
1) 标识#define在宏中应用的基本知识。这是很重要的。因为在嵌入(inline)操作符变为标准C的一部分之前,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能(当然主要是实时性哦,牺牲代码空间换取时间效率),嵌入代码经常是必须的方法。
2)三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else(存在条件转移会中断指令流水线)更优化的代码,了解这个用法是很重要的。
3) 懂得在宏中小心地把参数用括号括起来
4) 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
此处考点:inline函数和宏的区别
宏只是将参数完全替换,即MIN(*p++, b)进行宏展开后为((*p++) <= (b) ? (*p++) : (b)),如果(*p++) <= (b)成立,则表达式的值为(*p++),但由于在(*p++)<= (b)判断过程中改变了p的值,使得此时的? (*p++)非(*p++)<= (b)中的值了,违背了?号表达式的原意。
但是内联inline函数将进行参数检查,求出参数的值后再将此值带入函数中,因此((A) <= (B) ? (A) : (B))中的A是一致的。
第一部分:宏
为什么要使用宏呢?
因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。
而宏只是在预处理的地方把代码展开,不需要额外的空间和时间方面的开销,所以调用一个宏比调用一个函数更有效率。
但是宏也有很多的不尽人意的地方。
1、宏不能访问对象的私有成员。
2、宏的定义很容易产生二意性。
3、宏定义的常量在代码区,很多调试器不能够对其调试
我们举个例子:
#define square(x) (x*x)
避免这些错误的方法,一是给宏的参数都加上括号。
#define square(x) ((x)*(x))
第二部分:内联函数
从上面的阐述,可以看到宏有一些难以避免的问题,怎么解决呢?
内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。
内联函数和宏很类似,而本质区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
声明内联函数看上去和普通函数非常相似:
void f(int i, char c);
当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件:
inline void f(int i, char c)
{
// ...
}
内联函数必须是和函数体的定义申明在一起,才有效。
像这样的申明inline function(int i)是没有效果的,编译器只是把函数作为普通的函数申明,我们必须定义函数体。
inline int function(int i) {return i*i;}
这样我们才算定义了一个内联函数。我们可以把它作为一般的函数一样调用。但是执行速度确比一般函数的执行速度要快。
当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。
有上面的两者的特性,我们可以用内联函数完全取代预处理宏。
如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。
嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢? 这个问题用几个解决方案。
我首选的方案是:
while(1)
{
}
一些程序员更喜欢如下方案:
for(;;) (此处的判断效率要低的多,在汇编代码中看看???)
{
}
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。 (很多时候面试官关注你思考问题的方式,是否留意某些东西善于思考,可能并没有对错,只是偏好而已,比如memset和memcopy以及strcpy都能拷贝字符串,到底有什么区别呢?看你是否善于比较是否关注细节)
第三个方案是用 goto (goto语句在C中是应该尽量避免的,只在处理错误代码时用)
...
goto
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。
用变量a给出下面的定义
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an integers)
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )
答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer // 不是(int x),不需要具体的参数
h) int (*a[10])(int)(可以从e、g类比得到); // An array of 10 pointers to functions that take an integer argument and return an integer
人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么做准备呢?
这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
1)在函数体内,一个被声明为静态的变量在这一函数被调用过程中维持其值不变(该变量存放在静态变量区)。
2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,但是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。
考点:在嵌入式系统中,要时刻懂得移植的重要性,程序可能是很多程序员共同协作同时完成,在定义变量及函数的过程,可能会重名,这给系统的集成带来麻烦,因此保证不冲突的办法是显示的表示此变量或者函数是本地的,static即可。
在Linux的模块编程中,这一条很明显,所有的函数和全局变量都要用static关键字声明,将其作用域限制在本模块内部,与其他模块共享的函数或者变量要EXPORT到内核中。
static关键字至少有下列n个作用:
(1)设置变量的存储域,函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)限制变量的作用域,在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3)限制函数的作用域,在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
(4)在类中的static成员变量意味着它为该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;
(5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
我只要一听到被面试者说:"const意味着常数"(不是常数,可以是变量,只是你不能修改它),我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思?
Const只是一个修饰符,不管怎么样a仍然是一个int型的变量
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
本质:const在谁后面谁就不可修改,const在最前面则将其后移一位即可,二者等效
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,指向的整型数是不可修改的,但指针可以,此最常见于函数的参数,当你只引用传进来指针所指向的值时应该加上const修饰符,程序中修改编译就不通过,可以减少程序的bug)。
第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。
如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 ,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
const关键字至少有下列n个作用:
(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。例如:
const classA operator*(const classA& a1,const classA& a2);
operator*的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错:
classA a, b, c;
(a * b) = c; // 对a*b的结果赋值
操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。
关键字volatile有什么含意?并给出三个不同的例子。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份(由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化)。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器,通常在头文件中将硬件寄存器地址define为某个意义明确的表达式)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables,即static变量) ;在中断服务程序中修改的供其他程序检测用的变量需要加volatile声明;否则编译器可能对变量更新一次后每次都使用缓存值不再立即更新;
3) 多线程应用中被几个任务共享的变量(可能被多个线程随时修改)
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2); 一个指针可以是volatile 吗?解释为什么。
3); 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2); 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。
3) 这段代码有点变态。这段代码的目的是用来返回指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
关于volatile关键字在中断函数中的影响实例
串口发送数据,中断中对其检测,当中断产生后,置接收标志,主循环中检测此主标志,未用valotile修饰时,编译结果如下:
[0xe
0x302031b8 [0xe5d40000] ldrb r0,[r4,#0]
while(!uart1_rxFlag); //uart1_rxFlag为全局变量,在串口接收中断中置1
0x302031bc [0xe3500000] cmp r0,#0
0x
即编译器对其进行了优化,读取一次uart1_rxFlag的值之后,将其存放在寄存器r0中,比较后,条件不满足,继续等待,但未重新取存储器中uart1_rxFlag的值,此时即使中断服务函数中修改了uart1_rxFlag的值,比较处仍然不能发现,就出现了无论如何程序就停在此处的问题。
// 加了volatile关键字后,编译的结果
302031b4 ldr r4,0x30203378 ; = #0x
while(uart1_rxFlag == 0);
302031b8 [0xe5d40000] ldrb r0,[r4,#0]
302031bc [0xe3500000] cmp r0,#0
添加了关键字后,比较不等,跳转到重新取存储器中的uart1_rxFlag,因此任何时候uart1_rxFlag的值都是最新的。
一定程度的优化,去掉了读取uart1_rxFlag地址的语句。
定义一个易失性变量,编译器有一种技术叫数据流分析,分析程序中的变量在哪里被赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化。当编译器检查到代码没有修改字段的值,就有可能在你访问字段时提供上次访问的缓存值,这能够提高程序的效率,但有时这些优化会带来问题,不是我们程序所需要的,特点是对硬件寄存器操作的程序,这时可以用volatile关键字禁止做这些优化。
多任务环境下各任务间共享的标志应该加voatile关键字:在多线程访问某字段时,代码希望这些访问能够操作(读取)到字段的最新值,同时写到变量的操作能立即更新;对字段加上volatile关键字,那么对该字段的任何请求(读/写)都会立刻得到执行。
嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。 对这个问题有三种基本的反应
1)不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
2) 用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。
3) 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
#define BIT3 (0x1 << 3) (采用宏将数字定义为有意义的BIT3,明确,不易出错,改起来方便)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}
一些人喜欢为设置和清除值而定义一个掩码(待操作位全1,其余位全0的数,对于某个意义靠多位同时表示的最好带上掩码,隔离其他位的影响)同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作,先取反再&是对某位清0的最好操作。
考点:
在嵌入式系统中,时刻要关注移植性,具体的程序中不要出现具体的数字,这些数字都应该define成某个有意义的符号,可读性可移植性都很强,比如
#define BIT(x) (0x1 << (x))
X作为参数可以很方便的对任意位进行操作,意义明确,更改替换方便
嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。
在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;
A more obscure approach is: ( 一个较晦涩的方法是):
*(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。
在嵌入式系统中,对于大量此类型数据如硬件寄存器应该采用如下方式
typedef volatile unsigned int HARD_REG;
#define REG_NAME (*(HARD_REG *)ADDR)
即将ADDR强制转换为一个指向HARD_REG类型数据的指针,*HARD_REG为volatile的无符号整型数
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展-让标准C支持中断。其代表事实是,产生了一个新的关键字 __interrupt(51即如此)。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf("/nArea = %f", area);
return area;
}
这个函数有太多的错误了,以至让人不知从何说起了(前提是非操作系统下的中断服务函数):
1)ISR 不能返回一个值(都应该为void类型)。如果你不懂这个,那么你不会被雇用的。
2)ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3)在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额外的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
///////////////////////////////
另外中断服务程序是运行在内核态的(linux),内核通常是不支持浮点运算的。
http://access911.net/n/doc1.asp?mode=a&aid=4750647
内核中的printk和标准库的printf不一样,前者因为由内核直接实现,不能支持浮点。
在<linux内核设计与实现>的第一章中内核开发的特点一小节里就有比较了内核开发与应用开发的差异。其中一点就是内核编程时浮点数的问题,书中有一句话是:内核编程时浮点数很难使用
因为没有浮点单元,内核要支持浮点必须把内核以soft-float 方式重新编译,其连接所有的库也都要用soft-float 方式编译.
否则另外一种方式使用整数定义浮点类型加浮点预算库完成你的工作,
http://topic.csdn.net/u/20070417/16/a4b56569-228c-4b70-b5ab-30ee61c99a3d.html
如果你的内核里编译进了浮点支持,那么是可以的。要不内核或是模块不能用float或是double内型的变量或函数
在配置内核的时候把浮点模拟器选上,应该是可以支持的,但是速度非常慢。
我曾经遇到过,硬件明明支持浮点运算的FPU,但是编译内核的时候选上了浮点模拟器,结果所有的应用程序的浮点运算速度都非常慢。所以我怀疑要支持浮点只要编译内核的时候选上,对于应用程序不需要怎么关心。
///////////////////////////////
4) 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。
下面的代码输出是什么,为什么?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
Vc6.0测试情况
void main(void)
{
unsigned int a = 6;
int b = -20;
printf("unsigned int a + int b = %x/n", (a + b));
}
/*unsigned int a + int b = fffffff2*/
这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是 ">6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于频繁用到无符号数据类型的嵌入式系统(硬件寄存器的值全部是无符号的)来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。
评价下面的代码片断:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */ 0的补码为全1的数
对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;
这一问题真正能揭露出应试者是否懂得处理器字长的重要性(嵌入式平台可能是8、16、32的,移植的角度来说写出固定的0xFFFF是不对的)。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法(很重要哦,面试者关注的是你思考问题解决问题的过程,当你不知道答案时千万千万不要猜一个答案给他,因为现在不是选择题,面试官要的是过程,你只需要将你考虑问题的过程说明白就OK了),而不是答案。不管如何,你就当是这个娱乐吧...
尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger,他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:下面的代码片段的输出是什么,为什么?
char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
puts("Got a null pointer");
else
puts("Got a valid pointer");
这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是"Got a valid pointer"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确(因为如果申请失败,则程序处理认为内存不足了,一般会终止程序,是很严重的问题?)。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。
返回一個控指針還是指向 0 字節的指針甚至指向一个可以操作的指针?
(取决于系统平台的实现,C99及其他标准规定可以不同的)
malloc(0) in glibc returns a valid pointer to something(!?!?) while in uClibc calling malloc(0) returns a NULL. The behavior of malloc(0) is listed as implementation-defined by SuSv3, so both libraries are equally correct. This difference also applies to realloc(NULL, 0). I personally feel glibc's behavior is not particularly safe. To enable glibc behavior, one has to explicitly enable the MALLOC_GLIBC_COMPAT option.
在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 两个指针。
C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
int a = 5, b = 7, c;
c = a+++b;
这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,编译器应尽可能多的从左至右将若干个字符组成一个运算符。因此,上面的代码被处理成:c = a++ + b;
逗号表达式依次对每个表达式计算,最后的结果为最后一个表达式的值
因此, 这段代码执行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格(要明确的加上括号,避免歧义或者编译器不同带来的差异),代码的可读性,代码的可修改性的好的话题。
注:引出代码风格的问题正是作者问此问题的目的,这告诉我们要揣摩面试管每个问题背后隐藏的考查点,能够趁机发挥下就大功告成了!
好了,伙计们,你现在已经做完所有的测试了。这就是我出的C语言测试题,我怀着愉快的心情写完它,希望你以同样的心情读完它。如果是认为这是一个好的测试,那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年,我就不做现在的工作,也需要找一个。