C语言是嵌入式开发的常用语言,也是主要语言,很多企业在招聘嵌入式软件工程师时,C语言的熟练程度是一个重要考察点。笔试也是以C语言为主,所以想要拿到面试机会,还得通过C语言笔试,本专题总结了各大企业C语言笔面试常见的考题,并做了详细解答。
文章目录
- 1、sizeof在什么阶段执行?它是函数还是啥?
- 2、指针的指针
- 3、什么是大小端?写一个简单的程序判断系统的大小端。
- 4、指针、指针数组、数组指针、函数指针
- 5、位操作
- 6、符号转换
- 7、typedef和define的区别
- 8、C语言程序代码优化方法
- 9、关键字static的作用
- 10、const的使用
- 11、评价下面的代码片断
- 12、写一个标准宏MIN,输入两个参数返回较小的一个
- 13、嵌入式系统中经常要用到无限循环,你能用C编写多少种死循环呢?
- 14、关键字volatile作用,以及常用场合。
- 15、宏和函数的优缺点?
- 16、register关键字的含义和场合
- 17、用typedef简化函数的声明
- 18、说出`(*(void(*)())0)();`的含义
- 19、int strlen(char s[]) 和 int strlen(char *s)是否等价
- 20、数组外部类型声明陷阱
- 21、.、->、[]、()、*的优先级问题
- 22、交换两个变量的值,不使用第三个变量。即a=3,b=5,交换之后a=5,b=3;
- 23、用两个栈实现一个队列的功能。
- 24、如何避免头文件被重复包含?
- 25、局部变量能否和全局变量重名?
1、sizeof在什么阶段执行?它是函数还是啥?
编译,单目运算符。
2、指针的指针
下面这段程序会怎样?
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
答案当然是段错误,试图修改指针的值,只能传递指针的地址。
3、什么是大小端?写一个简单的程序判断系统的大小端。
- 小端:低位字节数据存储在低地址
- 大端:高位字节数据存储在低地址
例如:int a=0x12345678;(假设a首地址为0x2000)
地址: 0x2000 0x2001 0x2002 0x2003
值 : 0x12 0x34 0x56 0x78
这就是大端格式。
基于以上特点,我们不难写出判断程序:
#include <stdio.h>
union{
unsigned int a;
char b;
}test;
int main()
{
test.a = 0x01;
if(test.b)
printf("little endian");
else
printf("little endian");
return 0;
}
上面的程序巧妙的利用了联合体共用地址空间的特点,如果是大端,01会被存放在高地处,那么b就不会等于1。
4、指针、指针数组、数组指针、函数指针
用变量a定义
- 一个整型数
int a
; - 一个指向整型数的指针
int *a
; - 一个指向指针的指针,它指向的指针指向一个整型数
int **a
; - 一个有10个整型数的数组
int a[10]
; - 一个有10个指针的数组,该指针是指向一个整型数
int *a[10]
; - 一个指向有10个整型数数组的指针
int (*a)[10]
; - 一个指向函数的指针,该函数有一个整型数参数并返回一个整型数
int (*a)(int);
- 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型数参数并返回一个整型
int (*a[10])(int)
; - 声明一个函数,它接受一个int参数,返回值是一个函数指针,函数指针指向的函数接受一个参数int且返回值是void
void (*a(int))(int)
;
5、位操作
给定一个整型变量a,写两段代码,第一个设置a的bit3,第二个清除a的bit,在以上两个操作中,要保持其它位不变。
#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}
6、符号转换
int main(void)
{
unsigned int a = 6;
int b = -20;
char c;
(a+b>6)?(c=1):(c=0);
return 0;
}
当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。
7、typedef和define的区别
typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。
思考一下下面的例子:
#define dPS struct s*
typedef struct s* tPS;
以上两种情况的意图都是要定义dPS 和 tPS 为结构体指针类型。哪种方法更好呢?为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一个扩展为
struct s * p1, p2;
上面的代码定义p1为一个指向结构体的指针,p2为一个实际的结构体,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个结构体指针。
这也就是我们平常开发时不使用#define来定义新类型(或者同义类型)的原因。
8、C语言程序代码优化方法
- 满足需要的情况下,使用尽量小的数据类型
- 求余运算用与实现(a=a%8改为a=a&7)
- 用移位实现乘除法运算
- switch语句中根据发生频率来进行case排序,对于if,else if语句也同样。
9、关键字static的作用
1、static局部变量定义时的赋值只在第一次有效,通常可以用于区分初始化调用和后续调用,如:
int fun(...) {
static int a = 0;
if(a = 0) {
//初始化操作
a = 1;//标记初始化完成
}
...
}
2、static全局变量和函数用于限制该变量或者函数的使用范围为本文件
10、const的使用
const int a;--1
int const a;--2
const int *a;--3
int * const a;--4
int const * a const;--5
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。
对于3和4我的记忆技巧是,看const和*哪一个在前面,比如const在*后面,表示修饰的是a,即a是个常指针,指向不可修改;如果const在*前面,表示修饰的是*,即a指向的是个常数,没有办法通过指针来修改它的值。
合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。const给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。
比如:
int fun(const char *p);
11、评价下面的代码片断
unsigned int compzero = 0xFFFF;`
对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;
12、写一个标准宏MIN,输入两个参数返回较小的一个
#define MIN(a,b)((a)>=(b)?(b):(a))
此题考查的是问号表达式和宏的展开问题,该有的括号不能省。
13、嵌入式系统中经常要用到无限循环,你能用C编写多少种死循环呢?
这个问题有几个解决方案。我首选的方案是:
while(1){
...
}
一些程序员更喜欢如下方案:
for(;;){
...
}
第三个方案是用goto
Loop:
...
goto Loop;
第三种比较难想到。
14、关键字volatile作用,以及常用场合。
告诉编译器不要随便优化我的代码,在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
常用场合:
- 一个硬件寄存器
- 中断中用到的变量
- 线程之间共享变量
15、宏和函数的优缺点?
(1)、函数调用时,先求出实参表达式的值,然后带入形参。而使用带参数的宏只是进行简单的字符替换。
(2)、函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
(3)、对函数中的实参和形参都要定义类型,二者的类型要求一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也是无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
(4)、使用宏次数多时,宏展开后源程序长,因为每次展开一次都使程序增长,而函数调用不使源程序变长。
(5)、宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。
16、register关键字的含义和场合
使用register修饰符有几点限制。
1.register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。
2.因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。
3.它和volatile是截然相反,所以对于一些可能被硬件修改的变量,谨慎使用。
我在ubuntu上做过尝试,编译时加-O2选项,程序会将频繁使用到的变量做类似于寄存器变量的优化。对于一个for循环,将循环变量或者循环内部操作的变量定义成寄存器变量,可以大大提高效率。
17、用typedef简化函数的声明
声明signal函数:
void (*signal(int sig,void (*func)(int)))(int);
使用typedef简化函数声明:
typedef void (*HANDLER)(int);
HANDLER signal(int,HANDLER);
typedef定义了一种类型HANDLER,其是函数指针,指向接受一个int型参数无返回值的函数。
typedef在定义回调函数指针类型的时候大有用途。
18、说出(*(void(*)())0)();
的含义
-
从最里层入手,void(*)()表示一种函数指针类型,回想下
typedef void(*)() func;
所以简化成(*(func)0)();
-
(类型)表示强制类型转换,
(func)0
表示将地址0转换成func类型的函数指针。 -
回想下如何调用函数指针指向的函数,即
(*p)()
;
串起来就是调用了存放在0地址的函数。
19、int strlen(char s[]) 和 int strlen(char *s)是否等价
等价。数组和指针在作为形参时,是等价的,可以互换。
20、数组外部类型声明陷阱
文件1:
char filename[] = "/ect/passwd" ;
文件2:
extern char* filename;
filename[2] = 't';
这样有什么问题?告诉你,段错误。
保证一个特定的名称的所有外部定义在每个目标模块中具有相同的类型,是程序员的责任,编译器可能检测不到这种错误。正确的声明格式是:
extern char filename[];
21、.、->、[]、()、*的优先级问题
[]、()、.、->
这四个运算符同级,且是最高优先级,结合性是从左到右。注意此处的()是指函数调用or表达式那个括号,注意与强制类型转换区分。
强制类型转换()和*属于第二优先级,结合性是从右到左。
记住这两点便能解决以下问题:
1、
int *p[10];//[]的优先级比*高,所以p和[]先结合,表明这是个数组。
int (*p)[10];//加了括号改变了优先级,p和*先结合,表明这是个指针。
2、下面是先*p,再访问*p的成员,还是先访问p->data,再取*?
*p->data;
*p.data;
由于->的优先级比*高,所以是先p->data,再取*。
这个例子告诉我们,如果要先*p,需要加括号,即:
(*p)->data
3、
int *fun(int);//由于()的优先级比*高,所以fun先和括号结合,成为函数。
int (*fun)(int);//括号改变了优先级,*和fun结合成为指针。
4、
struct test *a = (struct test *)p->a;
由于->的优先级比强制类型转换的优先级高,所以上面相当于:
struct test *a = (struct test *)(p->a);
22、交换两个变量的值,不使用第三个变量。即a=3,b=5,交换之后a=5,b=3;
有两种解法, 一种用算术, 一种用^(异或)
a = a + b; b = a - b; a = a - b;
或者:
a = a^b;
b = a^b;
a = a^b;
23、用两个栈实现一个队列的功能。
要理解栈是先入后出,队列是先进先出。
假设2个栈为A,B, 一开始均为空。
入队
: 将新元素push入栈A;
出队
:
(1)判断栈B是否为空,如果非空则执行第三步(最开始肯定是空的);
(2)如果为空,则将栈A中所有元素依次pop出并push到栈B(这个操作就将先入后出的顺序颠倒成了符合要求的先进先出)
(3)将栈B的栈顶元素pop出
这样实现的队列入队和出队的平摊复杂度都还是O(1)。
24、如何避免头文件被重复包含?
例如,为避免头文件my_head.h被重复包含,可在其中使用条件编译:
#ifndef _MY_HEAD_H
#define _MY_HEAD_H /*空宏*/
/*其他语句*/
#endif
25、局部变量能否和全局变量重名?
能,局部会屏蔽全局。