目录
前言:
在初始学习嵌入式的时候,我们会买开发板来进行入门,在我们调用的库函数中,会用到一些预处理命令和一些关键字,这些是我们经常忽略不使用的点,但其实它们是有着重要作用的,这些也恰恰是公司面试官着重考察的点。
一、预处理:
预处理的作用以及常用的命令
(1)C语言的编译过程:
其中预处理起到的作用:头文件包含(#include)、宏替换(#define)、条件编译(#ifdef、#ifndef)、去除注释以及添加行号。
(2)常用的预处理命令
预处理名称 | 意义 |
#define | 宏定义 |
#include | 使编译程序将另一源文件嵌入到带有#include的源文件中 |
#if、#endif | 如果#if后面的常量表达式为true,则编译它与#endif之间的代码,否则跳过。 |
#else | 在#if失败的情况下,#else建立另一个选择 |
#ifdef、#ifndef | 分别表示“如果有定义”及“如果无定义”,条件编译的另一种方式 |
#error | 编译程序时,只要遇到# error就会生成一个编译提示错误,并停止编译。 |
预处理命令的作用以及实例
(1)宏定义
优点:代码复用性;提高性能
缺点:不可调试(预编译阶段进行了替换);无类型安全检查;可读性差,容易出错。
- 无参宏:一般用于替换任意常数以及字符串等
#define M 5 // 宏定义
#define PI 3.14 // 宏定义
int a[M]; // 会被替换为: int a[5];
int b = M; // 会被替换为: int b = 5;
printf("PI = %.2f\n", PI); // 输出结果为: PI = 3.14
- 有参宏:它形似于函数,但又不是函数。
- 带参宏只是在编译预处理阶段进行简单的字符替换;而函数则是在运行时进行调用和返回。
- 宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。
- 带参宏在处理时不分配内存;而函数调用会分配临时内存。
- 宏不存在类型问题,宏名无类型,它的参数也是无类型的;而函数中的实参和形参都要定义类型,二者的类型要求一致。
- 具体示例:
//输入两个参数并返回较小的一个
//应用了三目条件运算符
#define MIN(A, B) ((A) <= (B) ? (A) : (B))
//已知数组table,用宏求数组元素个数
//数组元素个数 = 数组长度 / 数组元素长度
#define COUNT(table) (sizeof(table) / sizeof(table[0]))
注:如果将*p++带入宏体时,指针p会做两次自增操作。
(2)#error
作用:编译程序时,只要遇到#error就会跳出一个编译错误。
应用场景:当程序比较大时,往往有些宏定义是在外部指定的(如makefile),或是在系统头文件中指定的,当你不太确定当前是否定义了 XXX 时,可以采用#error
具体的一个示例如下:
//如果遇到abc时,执行#error
#ifdef abc
#error "abc has been defined"
#else
…
#endif
二、关键字:
这里着重阐述我们常忽略的但又重要的关键字,具体有:
- 存储类:const、static、extern、register、auto
- 数据类型:volatile、typedef
- 运算符:sizeof
const关键字:
(1)作用
- 定义只读变量的关键字,常类型的变量或对象的值是不能被改变或更新的。
- const修饰后,程序不应该试图去修改它。
(2)与预编译指令的异同
- 同:都可以用于定义常数。
- 异:const有数据类型,编译器可以做静态类型检查;而宏定义没有类型,可能会导致类型出错
static关键字:
(1)修饰局部变量时:
- 改变了其存储位置,存储在静态区;
- 同时改变了其生命周期,为整个源程序,因此它只被初始化一次,若没显式初始化则自动初始化为0。
(2)修饰全局变量时:
- 改变了其作用域,只可以被文件内所用函数访问。
(3)修饰函数时:
- 改变了其作用域,只可被这一文件内的其它函数调用。
Volatile关键字:
(1)作用:
- 遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行假设(优化),从而可以提供对特殊地址的稳定访问。
- 优化器在用到volatile修饰的变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。例如:并行设备的硬件寄存器(如:状态寄存器)
(2)具体示例:
一个参数既可以是const还可以是volatile吗?一个指针可以是volatile吗?下面的函数有什么问题?
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
①可以。一个例子是只读的状态寄存器,它是volatile因为它可能被意想不到地改变,它是const因为程序不应该试图去修改它。
②可以。一个例子是当一个中断服务子程序修改一个指向一个缓冲区的指针时。
③这个函数本义上是想获得指针*pr指向值的平方,但是*ptr指向的是volatile型参数,编译器将产生类似下面的代码。
int square(volatile int *ptr)
{
int a, b;
a = *ptr;
b = *ptr;
return a * b;
}
typedef关键字:
(1)作用
- 在C语言中一般用于声明一个已经存在的数据类型的同义字
- 也可以做预处理类似的事情
(2)与#define的区别
#define dPS struct s *
typedef struct s * tPS;
dPS p1, p2;
tPS p3, p4;
- define是字符串的替换,因此dPS p1, p2;其实写成struct s * p1, p2; 即定义p1为一个指向结构体的指针,p2为一个实际的结构体。
- tPS p3, p4;;则正确地定义了p3 和p4 两个指针。
sizeof关键字:
(1)作用
- 用来计算变量、数据类型所占内存的字节数。
- sizeof(数组名)得到数组所占字节数。sizeof(字符串指针名)得到指针所占字节数。
(2)strlen()函数
- 用来测试字符串所占字节数,不包括结束字符 ’\0’。
- strlen(字符数组名)得到字符串所占字节数,strlen(字符串指针名)得到字符串所占字节数。
extern关键字:
(1)作用
- extern在链接阶段起作用。
- 用于跨文件引用全局变量,即在本文件中引用一个已经在其他文件中定义的全局变量。
(2)注意点
- 引用时不能初始化,如extern var,而不能是extern var = 0。
register关键字:
(1)作用
- 编译器会将register修饰的变量尽可能地放在CPU的寄存器中,以加快其存取速度,一般用于频繁使用的变量
(2)注意点
- register变量可能不存放在内存中,所以不能用&来获取该变量的地址;
- 只有局部变量和形参可以作为register变量;
- 寄存器数量有限,不能定义过多register变量。
auto关键字:
(1)作用
- 用来定义自动局部变量,自动局部变量在进入声明该变量的语句块时被建立,退出语句块时被注销,仅在语句块内部使用。
- auto修饰时,可以自动推理数据类型。
(2)应用场景
- 用于代替冗长复杂、变量使用范围专一的变量声明。
- for循环中的i将在编译时自动推导其类型,而不用我们显式去定义那长长的一串。
注:在真正编程的时候不建议这样来使用auto,直接写出变量的类型更加清晰易懂。