嵌入式工程师C语言面试常见的0x10个问题

本文深入探讨了嵌入式系统开发的核心概念、关键技术和实践方法,包括预处理指令、宏定义、变量类型、内存管理、中断处理、数据操作及安全测试等关键领域。通过具体实例和代码解析,旨在帮助开发者理解和掌握嵌入式系统开发的精髓。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

要点:

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

②懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。

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

④如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。

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

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

要点:

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

②三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的;

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

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

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

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

如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。

4、嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?

首选的方案是: while(1) { }

一些程序员更喜欢如下方案: for(;;) { }

这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的 基本原理。如果他们的基本答案是:“我被教着这样做,但从没有想到过为什么。”这会给我留下一个坏印象。

第三个方案是用goto

Loop:

...

goto Loop;

应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。

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

①一个整型数(An integer);

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

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

④一个有10个整型数的数组(An array of 10 integers);

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

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

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

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

答案是:

①int a;

②int *a;

③int **a;

④int a[10];

⑤int *a[10];

⑥int (*a)[10];

⑦int (*a)(int);

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

人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。

但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?

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

这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:

①在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变;

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

③在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。

7、关键字const是什么含意?

我只要一听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么,如果你从没有读到那篇文章,只要能说出const意味着“只读”就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)如果应试者能正确回答这个问题,我将问他一个附加的问题:下面的声明都是什么意思?

const int a;

int const a;

const int *a;

int * const a;

int const * a const;

前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:

①关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)

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

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

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

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

①并行设备的硬件寄存器(如:状态寄存器);

②一个中断服务子程序中会访问到的非自动变量(Non-automatic variables);

③多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。 假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。

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

②一个指针可以是volatile 吗?解释为什么。

③下面的函数有什么错误:

int square(volatile int *ptr)

{

return *ptr * *ptr;

}

下面是答案:

①是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它;

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

③这段代码的有个恶作剧。这段代码的目的是用来返指针*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;

}

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

对这个问题有三种基本的反应

①不知道如何下手。该被面者从没做过任何嵌入式系统的工作;

②用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边;

③用#defines和bit masks操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:

#define BIT3 (0x1<<3)

static int a;

void set_bit3(void)

{

a |= BIT3;

}

void clear_bit3(void)

{

a &= ~BIT3;

}

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

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

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

int *ptr;

ptr = (int *)0x67a9;

*ptr = 0xaa55;

一个较晦涩的方法是:

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

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

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

__interrupt double compute_area (double radius)

{

double area = PI * radius * radius;

printf(" Area = %f", area);

return area;

}

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

①ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的;

②ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项;

③在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的;

④与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

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

void foo(void)

{

unsigned int a = 6;

int b = -20;

(a+b > 6) puts("> 6") : puts("<= 6");

}

这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是“>6”。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。 因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。

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

unsigned int zero = 0;

unsigned int compzero = 0xFFFF;

/*1's complement of zero */

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

unsigned int compzero = ~0;(提高兼容性,避免在非32位处理器下出错)

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

到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧…

14、尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(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”。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。

15、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 两个指针。

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

int a = 5, b = 7, c;

c = a+++b;

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

c = a++ + b;

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

如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是:这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题

<think>好的,我现在需要帮助用户寻找针对嵌入式软件工程师的C语言面试题汇总。首先,用户提到了几个引用资料,我需要先看看这些引用内容是否相关,是否能提供有用的信息。 引用[1]提到的是2022年的嵌入式经典面试题汇总,主要关于C语言,用户去年面试时遇到的常见问题,并且强调了总结归纳的重要性。引用[2]是2024年的最新面试题及答案,虽然答案比较简略,但覆盖了100道题,可能包含最新的趋势。引用[3]中的代码示例是关于结构体内存对齐的问题,这是一个常见的C语言考点,特别是在嵌入式系统中,因为内存管理很关键。 接下来,我需要整理用户的需求。用户明确想要嵌入式软件工程师的C语言面试题汇总,所以需要从这些引用中提取相关内容,并可能补充常见的考点。首先,结构体内存对齐在嵌入式开发中非常重要,因为涉及到底层内存操作,所以这应该是一个重点问题。比如结构体的sizeof计算,如引用[3]中的例子,结构体s1的成员有char、int、double等,需要考虑对齐后的总大小。 然后是位操作和位域,这在嵌入式开发中经常用于寄存器操作和节省内存。例如,如何设置或清除特定的位,使用位掩码等。这也是常见面试题。 指针和数组的区别,以及指针在嵌入式中的应用,比如函数指针、指针运算等。用户可能被问到指针和数组名的区别,或者如何用指针访问特定内存地址,这在操作硬件寄存器时很常见。 关键字const、volatile、static的作用。特别是volatile在嵌入式中的重要性,用于防止编译器优化,确保直接访问内存地址,比如处理硬件寄存器或中断变量。 动态内存分配在嵌入式中的使用限制,因为嵌入式系统通常资源有限,可能避免使用malloc/free,而使用静态分配或内存池技术。这也是一个考点。 中断服务程序(ISR)的注意事项,比如避免长时间操作、不可重入函数的使用、volatile变量等。这涉及到嵌入式系统的实时性和可靠性。 此外,可能需要包括一些代码分析题,比如看一段代码找出错误,或者预测输出结果。例如,使用未初始化的指针、缓冲区溢出、递归调用导致栈溢出等。 另外,嵌入式系统中常用的编译优化选项,比如GCC的-O选项,以及如何确保关键代码不被优化,可能涉及到volatile和内存屏障的使用。 可能还需要涉及多任务和并发控制,比如使用信号量、互斥锁,但在C语言层面可能更偏向于基础的多线程知识,或者如何避免竞态条件。 最后,关于调试和测试的问题,比如如何使用调试工具,查看内存泄漏,或者使用断言(assert)进行调试。 总结一下,我需要将以上提到的知识点组织成结构清晰的问题列表,确保覆盖嵌入式C语言的核心考点,并结合引用中的例子进行说明。同时,注意使用正确的LaTeX格式,行内公式用$...$,独立公式用$$...$$,并检查语法正确性。回答中使用中文,并在引用内容后添加标识符,比如[^1]、[^2]、[^3]。 现在需要确保问题分类合理,每个类别下列举几个典型问题,并给出简要答案或解析,同时引用相关参考资料。例如,在结构体内存对齐的问题中,可以引用中的结构体示例,说明其内存布局。 可能还需要补充一些没有在引用中明确提到但常见问题,如大小端问题、联合体的使用、预处理器指令等,但需要确保这些内容确实属于嵌入式C语言常见面试题。 最后,生成相关问题部分需要与回答内容相关,引导用户进一步思考或探索更深入的内容,例如关于优化方法、实际应用场景等。</think>以下是针对嵌入式软件工程师的C语言面试题汇总及解析,结合内存管理、语法特性和实际应用场景进行分类整理: --- ### 一、结构体内存对齐 1. **计算结构体大小** ```c struct s1 { char ch; // 1字节 int a; // 4字节 double b; // 8字节 char c1; // 1字节 }; ``` **答案**:假设系统对齐规则为按最大成员对齐(此处为8字节),则总大小为$1 + 3(\text{填充}) + 4 + 4(\text{填充}) + 8 + 1 + 7(\text{填充}) = 28$字节。 2. **如何手动调整对齐方式?** 使用`#pragma pack(n)`指令修改默认对齐值,或通过`__attribute__((aligned(n)))`指定成员对齐。 --- ### 二、指针与内存操作 1. **指针与数组的区别** - 数组名是常量指针,不可修改;指针是变量,可指向不同地址。 - `sizeof(数组)`返回总字节数,`sizeof(指针)`返回地址长度。 2. **函数指针的应用场景** 用于回调函数、状态机实现等。例如: ```c void (*callback)(int); // 定义函数指针 ``` 3. **访问特定内存地址** ```c volatile uint32_t *reg = (volatile uint32_t *)0x40000000; // 操作硬件寄存器 ``` --- ### 三、关键字与修饰符 1. **volatile的作用** 防止编译器优化,强制从内存读取变量值。常用于: - 多线程共享变量 - 硬件寄存器访问 2. **static的三种用途** - 函数内:变量持久化(只初始化一次) - 文件内:限制作用域 - 函数外:隐藏函数/变量 --- ### 四、嵌入式系统特有问题 1. **中断服务程序(ISR)注意事项** - 避免使用不可重入函数(如`printf`) - 使用`volatile`修饰共享变量 - 保持ISR短小,防止阻塞其他中断 2. **动态内存分配的限制** 嵌入式系统通常禁用`malloc/free`,因碎片化风险高,改用静态数组或内存池。 --- ### 五、代码调试与优化 1. **查找内存泄漏** 使用工具如Valgrind,或通过日志记录`malloc/free`调用。 2. **优化代码体积** - 编译时使用`-Os`选项(优化大小) - 移除未使用代码,启用链接时优化(LTO) --- ### 六、位操作与寄存器 1. **设置某一位为1** ```c PORT |= (1 << 3); // 设置第3位 ``` 2. **位域定义** ```c struct { unsigned flag1 : 1; unsigned flag2 : 2; } status; // 节省内存空间 ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值