【笔试题】嵌入式软件开发——嵌入式软件工程师经典笔试题

从CSDN各个博客上摘选的一些容易做错的嵌入式软件的笔试题,做一下记录,让自己记住。

文章转自:嵌入式经典面试题。

 

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

解答:这一题主要容易错的地方就是:意识到这个表达式将使一个16位机的整型数溢出,因此要用到长整型符号L,告诉编译器这个常数是的长整型数。

 #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

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

解答:这一题主要容易错的地方就是:懂得在宏中小心地把参数用括号括起来。

#define MIN(A,B) ((A)<=(B)?(A):(B)) 

当然,使用宏也是有副作用的。就拿这一个例子来说:该宏定义对MIN(*p++, b)的作用结果是:((*p++) <= (b) ? (*p++) : (b)) 这个表达式会产生副作用,指针p会作两次++自增操作。

3、用变量a给出下面的定义:一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数。

解答:这一道题主要容易错的地方就是:函数指针、指针数组。

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

4、关键字static的作用是什么?

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

  • 在函数体中,一个被声明为静态的变量在这一函数被调用过程中只会被分配一次内存,且整个运行期间不会重新分配;
  • 在函数体外、某个源文件内,一个被声明为静态的变量只可被该源文件内的所有函数访问,但不能被其他源文件的函数访问。它是一个本地的全局变量;
  • 在某个源文件内,一个被声明为静态的函数仅仅只可以被这个源文件的其它函数调用。也就是说,这个函数被限制在声明它的源文件的本地范围之内使用。

5、关键字const的作用是什么?

解答:简单地说,const意味着常数。

  • const定义的变量,它的值不能被改变,在整个作用域中都保持固定;
  • 同宏定义一样,可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改;
  • 可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。const是通过编译器在编译的时候执行检查来确保实现的。

const与指针

下面的声明都是什么意思:

const int a;
int const a;
const int *a;
int * const a;
const int * const a;
int const * const a;
  • 前两个的作用是一样,a是一个常整型数;
  • 第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以);
  • 第四个意思a是一个指向整型 数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的);
  • 最后两个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数 是不可修改的,同时指针也是不可修改的)。

const与函数

  • const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。比如在String的程序中有很多const修饰形参的情况:
void StringCopy(char* strDestination, const char *strSource);
  • const还可以表示该函数返回一个常量,放在函数的返回值的位置。比如:
const char * GetString(void);
  • 在类成员函数的声明和定义中,const放在函数的参数表之后,函数体之前,表示该函数的this指针是一个常量,不能修改该对象的数据成员。比如:
void getId() const;

6、关键字volatile有什么含意?并给出三个不同的例子。

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

  • 存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义;
  • 在中断函数中的交互变量,一定要加上volatile关键字修饰,这样每次读取非自动存储类型的值(全局变量,静态变量)都是在其内存地址中读取的,确保是我们想要的数据;
  • 多任务环境下各任务间共享的标志应该加volatile。

一个参数既可以是const还可以是volatile吗?

可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。软件不能改变,并不意味着我硬件不能改变你的值,这就是单片机中的应用。

参考文章:C语言中的volatile——让我保持原样。

一个指针可以是volatile 吗?

可以。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

下面的函数有什么错误:

int square(volatile int *ptr)
{
        return *ptr * *ptr;
}

这段代码的目的是用来返指针*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;
}

7、给定一个整型变量a,写两段代码,第一个设置a的bit3,第二个清除a的bit3。

解答:这道题清除a的bit3,使用“&=~”的方法。

#define BIT3 (0x1 << 3)
static int a;

void set_bit3(void) 
{
    a |= BIT3;
}
void clear_bit3(void) 
{
    a &= ~BIT3;
}

8、嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。

解答:这一问题测试你是否知道为了访问一绝对地址,把一个整型数(绝对地址)强制转换为一指针是合法的。

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

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

__interrupt double compute_area (double radius) 
{
    double area = PI * radius * radius;
    printf("/nArea = %f", area);
    return area;
}

解答:这个函数有太多的错误了,以至让人不知从何说起了:

  • ISR 不能传递参数,不能返回一个值;
  • 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的;
  • printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在ISR中采用。

关于这些要求的解释:

a.为什么不能有返回值?

中断服务函数的调用是硬件级别的,当中断产生,pc指针强制跳转到对应的中断服务函数入口,进入中断具有随机性,并不是某段代码对其进行调用,那么如果有返回值它的返回值返回给谁?显然这个返回值毫无意义,如果有返回值,它必定需要进行压栈操作,这样一来何时出栈怎么出栈将变得无法解决。

b.不能向ISR传递参数?

同理,也是由于这样会破坏栈的原因,因为函数传递参数必定会要求压栈出栈操作,由于进入中断服务函数的随机行,谁给它传递参数都成问题。

c.ISR应尽可能的短小精悍?

如果某个中断频繁产生,而它对应的ISR相当的耗时,那么对中断的响应就会无限的延迟,会丢掉很多的中断请求。

d.printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在ISR中采用。

这就涉及到一个中断嵌套问题,由于printf之类的glibc函数采用的是缓冲机制,这个缓冲区是共享的,相当于一个全局变量,第一层中断来时,它向缓冲里面写入一些部分内容,恰好这时来了个优先级更高的中断,它同样调用了printf,也向缓冲里面写入一些内容,这样缓冲区的内容就错乱了。

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

中断服务函数与函数调用的联系与区别:

联系:两者都需要保护断点(即下一条指令地址)、跳至子程序或中断服务程序、保护现场、子程序或中断处理、恢复现场、恢复断点(即返回主程序)。两者都可实现嵌套,即正在执行的子程序再调另一子程序或正在处理的中断程序又被另一新中断请求所中断,嵌套可为多级。

区别:两者的根本区别主要表现在服务时间与服务对象不一样上。首先,调用子程序过程发生的时间是已知和固定的,即在主程序中的调用指令(CALL)执行时发生主程序调用子程序,调用指令所在位置是已知和固定的。而中断过程发生的时间一般的随机的,CPU在执行某一主程序时收到中断源提出的中断申请时,就发生中断过程,而中断申请一般由硬件电路产生,申请提出时间是随机的(软中断发生时间是固定的),也可以说,调用子程序是程序设计者事先安排的,而执行中断服务程序是由系统工作环境随机决定的;

其次,子程序完全为主程序服务的,两者属于主从关系,主程序需要子程序时就去调用子程序,并把调用结果带回主程序继续执行。而中断服务程序与主程序两者一般是无关的,不存在谁为谁服务的问题,两者是平行关系;

第三,主程序调用子程序过程完全属于软件处理过程,不需要专门的硬件电路,而中断处理系统是一个软、硬件结合系统,需要专门的硬件电路才能完成中断处理的过程;

第四,子程序嵌套可实现若干级,嵌套的最多级数由计算机内存开辟的堆栈大小限制,而中断嵌套级数主要由中断优先级数来决定,一般优先级数不会很大。

10、下面的代码输出是什么,为什么?

void foo(void)
{
    unsigned int a = 6;
    int b = -20;
    (a+b > 6) ? puts("> 6") : puts("<= 6");
}

解答:这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是 ">6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式 计算出的结果大于6。

还有一个重要的知识点:

通常情况下,在对int类型的数值作运算时,CPU的运算速度是最快的。在x86上,32位算术运算的速度比16位算术运算的速度快一倍。C语言是一个注重效率的语言,所以它会作整型提升,使得程序的运行速度尽可能地快。因此,你必须记住整型提升规则,以免发生一些整型溢出的问题。

整型提升是C程序设计语言中的一项规定,在表达式计算时(包括比较运算、算术运算、赋值运算等),比int类型小的类型(char, signed char, unsigned char, short, unsigned short等)首先要提升为int类型,然后再执行表达式的运算。

至于提升的方法,是根据原始类型进行位扩展(如果原始类型为unsigned char,进行零扩展,如果原始类型为signed char,进行符号位扩展)到32位。也就是说:

  • 对于unsigned char,进行零扩展,即在左边的高位用 0 填充至32位;
  • 对于signed char,进行符号位扩展。如果其符号位为0,则左边的高位用 0 填充至32位;如果其符号位为1,则左边的高位用 1 填充至32位。

11、评价下面的代码片断:

unsigned int compzero = 0xFFFF;

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

unsigned int compzero = ~0;

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

解答:动态分配将不可避免会产生问题:

  • 内存泄露:内存泄露通常是程序自身编码缺陷造成,常见的 malloc内存后没有free等类似的操作, 系统在运行过程当中反复的malloc,吃掉系统内存,造成内核OOM,将某个进程需要申请内存的杀死而退出。
  • 内存碎片:内存碎片是一个系统问题,反复的malloc和 free,而free后的内存又不能马上被系统回收利用。这个是因为负责动态分配内存的分配算法使得这些空闲的内存无法使用,这一问题的发生,原因在于这些空闲内存以小且不连续方式出现在不同的位置。

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

char *ptr;
if ((ptr = (char *)malloc(0)) == NULL) 
    puts("Got a null pointer");
else
    puts("Got a valid pointer");

函数malloc()的参数是可以时0的。

13、Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?

解答:typedef更好。思考下面的例子:

dPS p1,p2;
tPS p3,p4;

如果是第一个define的扩展:

struct s * p1, p2;

p1为指针,p2为结构体。很明显,不是我们想要的答案。

【笔试题】嵌入式软件开发——嵌入式软件工程师经典笔试题_第1张图片

 

你可能感兴趣的:(《面试》笔试面试)