全部博文(105)
分类: C/C++
2016-07-21 15:49:04
原文地址:想成为嵌入式程序员应该知道的0x10个问题 作者:digdeep126
C语言:C语言测试是招聘嵌入式系统程序员必须而且有效的方法
预处理器preprocessor:
1.用预处理指令声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
这里考查的知识:
1)#define语法的基本知识,比如不能以分号结束
2)懂得预处理器将为我们计算常数表达式的值,因此,直接写出你是任何计算一年宗有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3)意识到这个表达式将使一个16位机的整数溢出,因此要用到长整型符号L,告诉编译器这个常数是长整型
4)如果你在表达式中用到了UL(表示无符号长整型),那么你有了一个好的起点。
2.写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个:
#define MIN(A,B) ((A) <= (B) ? (A) :(B))
这个测试为下面的目的而设:
1)在inline操作符成为标准C的一部分之前,宏是方便产生嵌入代码的唯一方式,对于嵌入式系统来说,为了达到要求的性能,嵌入代码经常是必须的方法。
2)三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
3)懂得在宏中小心的把参数用括号括起来。
4)宏的副作用:least = MIN(p++,b)
3.预处理器标志#error的目的是什么?这个问题是区分一个正常的家伙和一个书呆子的很有用的方法。
4.死循环(Infinite loops)
首选的方案是:
while(1)
{
}
一些程序员更喜欢入下的方案:
for(;;)
{
}
这个实现方式让我很为难,因为它没有确切表达到底是怎么回事。
第三个方案:
Loop:
...
goto Loop;
这说明它是一个汇编程序员,这或许是好事。
5.数据声明:
用变量a给出下面的定义:
a)一个整数(an integer)
b)一个指向整数的指针(a pointer to an integer)
c)一个指向指针的指针,它指向的指针是指向一个一个整数(a pointer to a pointer to an integer)
d)一个有10个整数的数组(an array fo 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 an 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;
b)int* a;
c)int** a;
d)int a[10];
e)int* a[10];
f)int (*a)[10]; //难点
g)int (*a)(int);
h)int (*a[10])(int); //难点
6.static关键字:
关键字static的作用是什么?这个问题很少有人能够回答完全。在C中,关键字static有三个明显的作用:
1)在函数体,一个被声明为静态的变量在这一函数被调用的过程中维持其值不变。
2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所有函数访问,但不能被模块外其它 的函数访问。它是一个本地的全局变量。
3)在模块内,一个被声明为静态的函数只能被这一模块内的其它函数调用,那就是说,这个函数被限制在 声明它的模块的本地范围内使用。
关于这个问题:大多数应试者能正确回答第一部分,一部分能正确回答第二部分,只有很少的人能懂得第三部分。这是一个应试者严重的缺点,因为它显然不懂得本地化数据和代码范围的好处和重要性。
7.const关键字:
关键字const有什么含义?Dan Saks已经在他的文章完全概括了const的所以用法。因此ESP(Embedded Systems programmer)的每一位读者都应该非常熟悉const能做什么和不能做什么。
const的附加问题:
下面的声明的含义:
const int a;
int const a;
const int * a;
int * const a;
int const * a const;
(int const * const * a;)
我们看重const关键字的理由:
1)const关键字给维护代码的人读代码的人传达了一个非常有用的信息!
2)通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3)合理的使用关键字const可以是编译器很自然的保护那些不希望被改变的变量,防止其被无意间修改。
8.关键字volatile
关键字volatile有什么含义并给出三个不同的例子。
一个定义为volatile的变量就是说这个变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确的说就是,优化器在用到这个变量时必须每次都小心的重新读取这个变量的值,而不是使用保存在寄存器里的备份。也就是不使用缓存中的值。
使用volatile变量的几个例子:
1)并行设备的硬件寄存器(如状态寄存器)
2)一个中断服务子程序中会访问到的非自动变量(Non-automatic variable)
3)多线程应用中被几个任务共享的变量。
回答不出这个问题的人是不会被雇用的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所以这些都要求用到volatile变量。不懂得volatile将会带来灾难。
如果面试者回答出了volatile的含义,我将深究面试者,看它是否真正懂得volaitle完全的重要性:
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;
}
由于*ptrd的值可能被意想不到地改变,因此a和b可能的值是不同的。结果这段代码返回的不是期望的平方
值。正确的代码如下:
long square(volatile int* ptr)
{
int a;
a = *ptr;
return a * a;
}
9.位操作(bit manipulation):
嵌入式系统总是要用户对变量或寄存器进行为操作。
给定一个整型变量a,写两段代码,第一个设置a的bit3,第二个清除a的bit3。在以上两个操作中,要保持其它的为不变。
对这个问题有三种基本的反应:
1)不知道如何下手,被面试者从没有做过任何嵌入式系统的工作。
2)用Bit fields。Bit fields是被丢到C语言死角的东西,它保证你的代码在不同的编译之间是不可移植的,同时也保证了你的代码是不可重用的。我最近不幸看到了Infineon为其较复杂的通信芯片写的驱动程序,他用到了bit fileds因此完全对我无用。因为我的编译器用其它的方式来实现bit fields。从道德上讲:永远不要让一个非嵌入式的家伙粘实际硬件边。
3)用#define和bit mask实现:这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
#define BIT3 (OX1 << 3)
static int a;
void set_bit3(void){ a |= BIT3;}
void clear_bit3(void){ a &= ~BIT3;}
答案的要点是:说明常量、|=、&=、~操作。
10.访问固定的内存位置(accessing fixed memory locations)
嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一个绝对地址为
0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
这一个问题是测试你是否知道为了访问一个绝对地址要把一个整型数强制转换成一个指针是合法的。这一个问题的实现方式随着个人风格不同而不同。典型的代码如下:
int* ptr;
ptr = (int*)0x6709;
*ptr = oxaa66;
a more obscure approach is:
一个较为晦涩的方法是:
*(int* const)(0x6709) = 0xaa66;
11.中断(interupt)
中断是嵌入式系统的重要组成部分,这导致了很多编译器开发商提供了一种扩展-让标准C支持中断。具有代表的事实是,产生一个新的关键字__interupt。下面的代码就使用了__interupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码:
__interupt double compute_area(double radius)
{
double area = PI * radius * radius;
printf("\nArea = %f, area);
return area;
}
这个函数有太多的错误了,以致让人不知从何说起了:
1)ISR不能返回一个值,如果你不懂这个,那么你就不会被雇用的。
2)ISR不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3)在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让而我的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4)printf()经常有重入和性能上的问题。
12.代码例子(code examples):
下面的代码输出是什么,为什么?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : put("<=6");
}
这个问题测试你是否懂得C语言中的整数的自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案输出是">6"。原因是当表达式中存在有符号类型和无符号类型时,所以的操作数都自动转换到无符号类型。因此-20变成了一个非常大的正整数。这一点对于应当频繁使用到无符号类型的嵌入式系统来说是非常重要的。如果你打错了就得不到这分工作了。
13.评价下面的代码片段:
unsigned int zero = 0;
unsigend int compzero = 0xffff; /* It's complement of zero */
对于一个int不是16位的处理器来说,上面的代码是不正确的。应该如下编写:
unsigned int compzero = ~0;
这一问题能揭示应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确的明白硬件的细节和它的局限,然而PC机程序员往往它硬件作为一个无法避免的烦恼。
到了这个阶段,应试者应该或者完全垂头丧气或者自信满满志在必得。
如果应试者显然做的不错,那么我们就扔出下面的追加问题,这些问题是比较难得,我想仅仅非常优秀的应试者能做的不错。提出这些问题,我更多的希望看到应试者应付问题的方法,而不是答案,你就当时这个是娱乐把:
14.动态内存分配(Dynamic memory allocation):
尽管不想非嵌入式计算机那么常见,嵌入式系统还是有从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,得到了一个合法的指针之后,才有了这个问题。我用这个来开始讨论这样的一个问题,看看被面试者是否想到例库程序这样做是正确的。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更加重要。
15.typedef:
typedef在C语言中被频繁用来声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。
例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上两种情况的意图都是要定义dPS和tPS作为一个指向结构s指针。哪种方法更好呢?为什么?
这是一个微妙的问题,任何人答对这个问题及其原因,都应该被恭喜的。
答案是:typedef更好。
思考下面的例子:
dPS p1, p2;
tPS p3, p4;
第一个扩展为:struct s* p1, p2;应该不是我们想要的结果。
第二个正确的定义了p3, p4两个结构指针。
16.晦涩的语法:
C语言同意一些令人震惊的结构,下面的结构是合法的吗?如果是它做些什么?
int a = 5, b = 7, c;
c = a+++b;
这个问题将作为这个测试的一个愉快的结尾。
上面的代码应该被处理成:
c = a++ + b;
这段代码执行后:a = 6, b = 7, c = 12;