一、前言
最近在找嵌入式软件开发岗位,由于第一次面临秋招,所以准备做个长文,将一些可能会问的、考的一些关于C语言问题记录下来。
二、经典面试题
堆栈
内存空间分布及堆栈的区别
进程与线程
进程与线程的区别
预处理和宏
-
用预处理指令 #define 声明一个常数,用于表明1年中有多少秒(不考虑闰年)
#define SECONDS_PER_YEAR (60*60*24*365)UL
这个计算会使16位机的整形数溢出,且不为负,所以需要使用无符号长整型UL
-
写一个标准宏MIN,这个宏输入两个参数并返回较小的一个
#define MIN(A,B) ((A)<=(B)?(A):(B))
-
#define 和 typedef 的区别
-
宏定义中#和##的使用
-
预处理器标识#error、#warning的目的
#error:用于产生一个编译的错误信息,并停止编译 #warning:用于产生一个编译的警告信息
位操作
将a的第n位,进行置0和清0操作:
- 置1: a|=(0x1<<n);
- 清0:a&=~(0x1<<n);
举例:
#define BIT3 (0x1<<3)
static int a;
// 置1
void Set_Bit3(void)
{
a |= BIT3;
}
// 置0
void Clear_Bit3(void)
{
a &= ~BIT3;
}
大端小端:
-
大端:数据的高位存放在内存的低地址,数据的低位存放在内存的高地址。
-
小端:数据的高位存放在内存的高地址,数据的低位存放在内存的低地址。
例如将0x1234存在地址起始位置为0x4000
大端:
内存地址 数据 0x4001 0x34 0x4000 0x12 小端:
内存地址 数据 0x4001 0x12 0x4000 0x34
循环
死循环
// 两种写法
for(;;);
while(true);
在编译后,for(;;)
指令少,不占用寄存器,同时没有判断跳转,相比较while(true)循环更简洁,效率更高。
do…while 和 while…do的区别
答:前一个循环一遍再判断,后一个判断以后再循环。
数据声明
- 用变量a给出下面的定义
-
a) 一个整型数
-
b) 一个指向整型数的指针
-
c) 一个指向指针的指针,它指向的指针是指向一个整型数
-
d) 一个有10个整数型的数组
-
e) 一个有10个指针的数组,该指针是指向一个整数型 (指针数组)
-
f ) 一个指向有10个整型数数组的指针(数组指针)
-
g) 一个指向函数的指针,该函数有一个整型参数,并返回一个整数型
-
h) 一个有10个指针的数组,该指针指向函数,该函数有一个整型参数,并返回一个整型数
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); //函数指针数组 //扩展 int(*a); //把指针a取值,并且转换成 int (int *) a; //把a变量转换为 int *
-
Static
- 关键词static 作用是什么?
- 在函数体,一个被声明为静态的变量,在这一函数被调用过程中维持其值不变。
- 在模块内(但是在函数体外),一个被声明为静态的变量可以被模块内所有函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。
- 在模块内,一个被声明为静态的函数只可以被这一模块内的其他函数调用。这个函数被限制在声明它的模块的本地范围内使用
Const
关键字const含义:“只读”
声明意思:
a) const int a; //常整数
b) int const a;
c) const int *a; //指向常整数的指针
d) int * const a; //常量指针
e) int const *a const; //指向常整数的常指针
- a)、b) 前两个作用是一样的,a是一个常整型数
- c) a是一个指向常整型数的指针(也就是,整型数不可修改,但是指针可以修改)
- d) a是一个指向整型数的常指针(指针指向的整型数可以修改,但是指针不能修改)
- e) a是一个指向常整型数的常指针(都不可以修改,指针指向的整形数不可修改,同时指针也不能修改)
作用:
- 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉用户这个参数的应用目的。
- 通过给优化器一些附加信息,使用关键字const也许能使代码更加紧凑
- 合理使用关键字const可以使编译器很自然的保护那些不希望被改变的参数,防止其被无意的代码修改。
Volatile
关键字volatile的含义:
一个定义为volatile的变量是说,这个变量可能会被意想不到的改变,这样,编译器就不会去假设这个变量的值。
也就是说,优化器在用到这个变量时必须每次都小心的重新读取这个变量的值,而不是使用保存在寄存器里的备份。
嵌入式系统程序员经常同硬件、中断、RTOS打交道,这些都要求volatile变量。
下面是几个例子:
- 并行设备的硬件寄存器(如:状态寄存器)
- 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
- 多线程应用中被几个任务共享的变量
问题
-
一个参数既可以是const还可以是volatile吗?为什么
答:是的,比如只读的状态寄存器。它是volatile因为它可能被意想不到的改变,它是const因为程序不应该试图去修改它。
-
一个指针可以是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; }
指针
- 在嵌入式系统中经常需要去访问某个特定的内存位置。比如,要求设置一绝对地址0x67a9的整型变量的值为0xaa66。
测试是否知道为了访问一绝对地址把一个整型数据强制转化为一个指针是合法的,典型的类似代码如下:
//第一种方式(建议)
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;
//第二种方式(比较难理解)
*(int *const)(0x67a9) = 0xaa66;
中断
中断是嵌入式系统中重要的组成部分,导致了许多编译开发商提供一种扩展使标准C支持中断。最代表事实的是,产生了一个新的关键字__interrupt
下面代码使用了__interrupt关键字定义了一个中断服务子程序(ISR),请评论这段代码:
__interrupt double computer_area(double radius)
{
double area = PI * radius * radius;
printf("Area = %f",area);
return area;
}
回答:
- 1). ISR不能返回一个值和传递参数,如果不懂这个,那么不会被雇用。
- 2). 在许多的处理器/编译器中,浮点数一般都是不可重入的。有些处理器/编译器需要让额外寄存器入栈,有些就不允许在ISR中做浮点运算。
- 3). ISR中应该是短而有效率的。所以不适合在里面进行复杂的浮点运算和有重入和性能问题的printf()。
代码例子
- 下面的代码输入的是什么?为什么?
答:这里涉及到C语言中的整数自动转换原则。原因是当表达式中同时存在有符号类型(int)和无符号类型(unsigned int)时,所有的操作数都自动转换为无符号类型,所以-20变成一个非常大的正整数,所以输出 >6 。void foo(void) { unsigned int a = 6; int b = -20; (a+b > 6) ? puts(">6") : puts("<=6"); } 输出是:>6