15.0.0 预处理指令
15.1.0 预处理指令概述
15.1.1 预处理指令的特点
- 都是以#开头
- 预处理指令都是在编译之前执行
- 预处理指令后面都没有分号
C程序的一般流程:
- 新创建一个.c的源文件,.c的文件是C程序的源文件
- 在.c的源文件中写上符合C语法规范的源代码
- C语言严格区分大小写
- 除了字符串的内容,其他地方都必须使用英文字符
- 使用cc -c指令编译源文件
格式:cc -c 源文件名称
- 先执行源文件中的预处理指令
- 检查源代码语法是否符合规范
- 如果符合语法九生成.o目标文件,这个文件就是源代码.c文件对应的二进制指令;如果不符合就报错,不会生成目标文件
- 链接
格式:cc .o目标文件
- 为.o的目标文件添加启动代码
- 链接函数。告诉编译器,要调用的函数在什么地方
- 链接成功以后,就会生成一个可执行文件a.out
预处理指令的分类
- 文件包含指令
- 宏定义:可以将一段C代码定义为1个标识,使用这个标识就可以使用这段代码
- 条件编译指令:只编译指定的C代码为二进制指令
15.2.0 宏
15.2.1 宏定义
- 宏定义:它是一个预处理指令,所以它在编译之前执行。
- 作用:可以为一段C代码定义一个标识,如果你要使用这段C代码,那么你就使用这个标识就可以了。
- 语法结构:#define 宏名宏值
- 如何使用宏:在C代码中,直接使用宏的名称就可以了。
#define N 10
int a = N + 1
- 宏的原理:在预编译的时候,就会执行源文件中的预处理指令,会将C代码中使用宏名的地方替换为宏值。
将C代码中的宏名替换为宏值的过程叫做宏替换/宏代换。
15.2.2 使用宏的注意事项
- 宏值可以是任意的东西。
- 在定义宏的时候,并不会检查语法,只有在完成了宏替换的时候,才会检查替换后是否符合语法。
- 如果宏值是一个表达式,宏值并不是这个表达式的结果,而是这个表达式本身。
- 如果宏值中包括一个变量名,那么在使用这个宏之前必须要保证这个变量已经存在
- 无法通过赋值符号为宏赋值
- 宏的作用域:
- 宏可以定义在函数的外部,也可以定义在函数的内部
- 从定义宏的地方开始,后面的所有的地方都可以直接使用这个宏。就算宏在函数内部,也可以被外部的其他函数使用。
- 默认情况下,宏从定义的地方一直到文件结束都可以使用,而undef可以使宏提前失效。
#undef 宏名
- 字符串中如果出现了宏名(双引号中出现了宏名),系统不会认为这是一个宏,而认为是字符串的一部分,字符串中并不会出现宏替换。
- 宏可以实现层层替换
#define PI 3.14
#define R 5
#define AREA PI * R * R
- 如果宏定义的宏值后面跟了一个分号,系统就会认为分号是宏值的一部分,替换的时候会连分号一起
- 可以把任意的C代码定义为宏
- #define和typedef的区别
- #define是一个预处理指令,在预编译的时候执行,在预编译的时候会把宏名换成宏值
- typedef是一个c代码,在运行的时候才执行
- #define可以将任意的C代码去一个标识名
- typedef只能为数据类型取名字
15.2.3 带参数的宏
我们在定义宏的时候,宏名是可以带参数的。
在这个宏值当中,可以直接使用这个参数。
语法结构:#define 宏名(参数) 宏值
如果使用的宏有参数,那么就必须要在使用它的时候为这个宏的参数传值。
例如:
#define N(a) a + 10
int b = N(20);
宏代换的原理是什么?
- 先将传入的值传递给宏的参数,那么宏的参数的值就是我们传递的值
- 再把宏值当中使用参数的地方换成参数的值
- 再把这个最终的宏值替换到使用这个宏名的地方
使用有参数的宏的注意事项:
- 宏不是函数,所以宏的参数不需要加类型说明符,直接写参数名就可以了
- 在定义宏的时候,编译器是如何区分宏名和宏值的?
#define后面第一个空格后的连续无空格内容是宏名,宏名后的第一个空格后是宏值。
- 为带参数的宏传值的时候是本色传递。本色传递是指:如果传递一个变量,并不是传递这个变量的值,而是直接传递的是这个变量的串。
宏一定程度上可以实现函数的功能效果,但是宏值一旦换行就表示这个宏定义就结束了,不适合大量代码的情况。如果代码只有一句或两句的情况是可以的,但是代码量多的时候是不适合使用宏的。
15.3.0 条件编译指令
15.3.1 条件编译指令的用法
条件编译指令,它是一个预处理指令,所以在预编译阶段执行。
作用:默认情况下,我们所有的C代码都会被编译为二进制代码,而条件编译指令的作用是可以让编译器只编译指定部分的代码。
- 条件编译指令的第一种用法:
#if 条件
C代码;
#endif
在预编译的时候,如果条件成立,就会将其中的C代码编译成二进制指令;如果条件不成立,就不会将其中的C代码编译成二进制指令。
注意:条件只能是宏,不能是变量。
- 条件编译指令的第二种用法:
#if 条件1
C代码;
#elif 条件2
C代码;
#elif 条件3
C代码;
#endif
这个类似if-else if结构,但记得最后一定要用#endif结束这段预编译指令。
与if-else if结构的区别是:
- 条件编译指令是一个预处理指令,在预处理阶段执行,而if结构是C代码,在程序运行时执行,要晚于预编译指令。
- if语句无论如何全部都要被编译为二进制指令,条件编译指令只会将符合条件的C代码编译为二进制指令。
- 实际上,if语句一定程度上可以换成条件编译指令,但是条件编译指令的条件不能是变量参与,只能是宏。
- 条件编译指令的第三种用法:
#ifdef 宏名
C代码;
#endif
如果定义了指定的宏,就编译其中的C代码,否则就结束。
还有一种:
#ifndef 宏名
C代码;
#endif
如果没有定义了指定的宏,就编译其中的C代码,否则就结束。
15.3.2 使用条件编译指令防止同一个文件被重复包含
实现的效果:
无论一个文件被#include多少次,我只希望它最终只会被包含一次。
解决方案:
文件
#ifndef 宏名
#define 宏名
代码;
#endif
这种做法通常是为了防止同一个头文件被重复包含多次代码。
15.4.0 static和extern两个关键字
static和extern是C语言中的两个关键字,用来修饰变量和函数。
15.4.1 static和extern修饰局部变量
static修饰局部变量的效果
如果局部变量被static修饰,那么这个变量就叫做静态变量。
静态变量不再存储于栈区域,而是存储在常量区。
当函数执行完毕之后,这个静态变量不会被回收。下次再执行这个函数的时候,当初在函数中声明静态变量的那句代码就不会再执行,直接略过,直接使用上一次运行后存储在常量区的变量的值。函数无论执行多少次,这个静态变量只有一份。
例如:
void test()
{
static int num = 0;
num++;
printf(“num = %d\n”,num);
}
int main()
{
test();
test();
test();
test();
test();
return 0;
}
我们可以发现,最终输出的结果是:
1
2
3
4
5
而不是从前我们学习过的都是1。这就是static修饰局部变量的效果。
extern修饰局部变量的效果
extern不能修饰局部变量。
15.4.2 static和extern修饰全局变量
我们知道,写一个函数,完整的步骤应该是先声明,后实现。
而写一个全局变量,完整的步骤也应该分两步:
- 先写全局变量的声明。即只定义全局变量而不赋值;
例如:int num;
- 再写全局变量的定义,定义全局变量并初始化,也叫做全局变量的实现。
例如:int num = 10;
注意:如果全局变量只有定义没有实现,那么系统会自动去实现这个全局变量,自动初始化为0.
当我们分模块开发的时候,声明全局变量需要注意以下几点:
- 全局变量的声明要写在.h的头文件中
- 全局变量的定义也就是实现,要写在包含该头文件的.c文件中
- 如果将全局变量定义在模块中,这个全局变量就必须要用static或extern修饰
- 如果定义在模块中的全局变量用extern修饰,这个模块中的全局变量就可以跨模块访问
- 如果定义在模块中的全局变量用static修饰,这个模块中的全局变量就只能在当前模块中访问。如果跨模块使用,虽然编译时不会报错,但是会发生取不到真正的值这样的问题。
15.4.3 static和extern修饰函数
- 如果函数被extern修饰,那么这个函数可以跨模块调用。
- 如果函数被static修饰,那么这个函数只能在当前模块中调用,无法跨模块使用。
- 如果函数没有写extern或者static,则默认是extern修饰。