# 前导知识 ## GCC 编译一共分4个阶段:**预处理、编译、汇编、链接** > gcc 【选项】要编译的文件【选项】【输出文件】 | 选项 | 说明 | | -- | -- | | -E | 控制GCC编译器仅对源码做预处理操作 | | -S | 控制GCC编译器仅对指定文件处理之编译阶段 | | -c | 控制GCC编译器仅对制定文件处理至汇编阶段,并生成相应的目标文件 | | -o outfile | 指定输出文件的文件名 | 1. 预处理阶段 预处理阶段是编译的第一个阶段。在这个阶段,GCC会扫描源代码并执行以下操作: 1. 删除注释 1. 替换宏定义 1. 处理条件编译指令 1. 将头文件内容插入源代码中(展开头文件) ```shell gcc -E hello.c -o hello.i ``` 2. 编译阶段 在预处理阶段之后,GCC会将源代码翻译成汇编代码,这个过程称为编译。编译器会检查源代码是否符合语法,以及是否存在语义错误。 在编译阶段,编译器将源代码翻译成汇编代码,以便下一步的汇编阶段使用。 ```shell gcc -S hello.i -o hello.s ``` 3. 汇编阶段 汇编阶段是将汇编代码转换为机器代码的过程。在这个阶段,汇编器将汇编代码转换为机器指令,生成目标文件。 在汇编阶段,汇编器将汇编代码转换为机器指令,生成目标文件。 ```shell gcc -c hello.s -o hello.o ``` 4. 链接阶段 链接阶段是将所有目标文件合并成一个可执行文件的过程。在这个阶段,链接器将目标文件中未定义的符号与其他目标文件中定义的符号进行匹配,并生成一个可执行文件。 在链接阶段,链接器将目标文件合并成一个可执行文件。 ```shell gcc hello.o -o hello ``` # **预处理** 在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的阶段统一由所谓预处理器(cc1)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括: 1. 头文件:#include 1. 定义宏:#define 1. 取消宏:#undef 1. 条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif 1. 显示错误:#error 1. 修改当前文件名和行号:#line 1. 向编译器传送特定指令:#progma | 指令 | 描述 | 使用示例 | | -- | -- | -- | | #define | 定义宏(符号常量或函数式宏) | #define PI 3.14159 | | #include | 包含头文件 | #include | | #undef | 取消已定义的宏 | #undef PI | | #ifdef | 如果宏已定义则编译后续代码 | #ifdef DEBUG | | #ifndef | 如果宏未定义则编译后续代码(常用于头文件保护) | #ifndef HEADER_H | | #if | 条件编译(可配合defined操作符使用) | #if VERSION > 2 | | #else | #if | #ifdef WIN32 | | #elif | 类似于else if | #if defined(UNIX) | | #endif | 结束条件编译块 | 如上例所示 | | #error | 产生编译错误并输出消息 | #if !defined(C99) | | #pragma | 编译器特定指令(非标准,各编译器不同) | #pragma once | - 基本语法 - 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠连接成一个逻辑行 - 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接 - 可以通过如下编译选项来指定来限定编译器只进行预处理操作: ```c gcc example.c -o example.i -E ``` ## **宏** 宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如: ```c #define PI 3.14 ``` 此处,PI 就是宏(宏一般习惯用大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如: ```c int main() { printf("圆周率: %f\n", PI); // 此语句将被替换为:printf("圆周率: %f\n", 3.14); } ``` - 宏的作用: - 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。 - 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。 - 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。 ### **无参宏** 无参宏意味着使用宏的时候,无需指定任何参数,比如: ```c #define PI 3.14 #define SCREEN_SIZE 800*480*4 int main() { // 在代码中,可以随时使用以上无参宏,来替代其所代表的表达式: printf("圆周率: %f\n", PI); mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, ...); } ``` 注意到,上述代码中,除了有自定义的宏,还有系统预定义的宏: ```c // 自定义宏: #define PI 3.14 #define SCREEN_SIZE 800*480*4 // 系统预定义宏 #define NULL ((void *)0) #define PROT_READ 0x1 /* Page can be read. */ #define PROT_WRITE 0x2 /* Page can be written. */ #define MAP_SHARED 0x01 /* Share changes. */ ``` 宏的最基本特征是进行直接文本替换,以上代码被替换之后的结果是: ```c int main() { printf("圆周率: %f\n", 3.14); mmap(((void *)0), 800*480*4, 0x1|0x2, 0x01, ...); } ``` ### **带参宏** 带参宏意味着宏定义可以携带“参数”,从形式上看跟函数很像,例如: ```c #define MAX(a, b) a>b ? a : b #define MIN(a, b) ay ? x : y); // printf("最小值:%d\n", xb ? a : b int main() { int x = 100, y = 200; printf("最大值:%d\n", MAX(x, y==200?888:999)); } ``` 直观上看,无论 y 的取值是多少,表达式 y==200?888:999 的值一定比 x 要大,但由于宏定义仅仅是文本替换,中间不涉及任何运算,因此等价于: ```c printf("最大值:%d\n", x>y==200?888:999 ? x : y==200?888:999); ``` 可见,带参宏的参数不能像函数参数那样视为一个整体,整个宏定义也不能视为一个单一的数据,事实上,不管是宏参数还是宏本身,都应被视为一个字串,或者一个表达式,或者一段文本,因此最基本的原则是: - 将宏定义中所有能用括号括起来的部分,都括起来,比如: ```c #define MAX(a, b) ((a)>(b) ? (a) : (b)) ``` ### **宏定义中的符号粘贴** 有些时候,宏参数中的符号并非用来传递数据,而是用来形成多种不同的字串,例如在某些系统函数中,系统本身规范了函数接口的部分标准,形如: ```c void __zinitcall_service_1(void) { ... } void __zinitcall_service_2(void) { ... } void __zinitcall_feature_1(void) { ... } void __zinitcall_feature_2(void) { ... } ``` 此时,若需要向用户提供一个方便整合字串的宏定义,可以这么写: ```c #define LAYER_INITCALL(layer, num) __zinitcall_##layer##_##num ``` 用户的调用如下: ```c LAYER_INITCALL(service, 1)(); LAYER_INITCALL(service, 2)(); LAYER_INITCALL(feature, 1)(); LAYER_INITCALL(feature, 2)(); ``` **注意:** 在书写非字符串的字串时(如上述例子),使用两边双井号来 ``` #define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num## ``` 但如果粘贴的字串并非出现在最末尾,则前后都必须加上双井号: ```c #define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num##end ``` **注意:** 另外,如果字串本身拼接为字符串,那么只需要使用一个井号即可,比如: ```c #define domainName(a, b) "www." #a "." #b ".com" int main() { printf("%s\n", domainName(yueqian, lab)); } ``` 执行打印如下: ```shell gec@ubuntu:~$ ./a.out www.yueqian.lab.com gec@ubuntu:~$ ``` ### **无值宏定义** 定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现,例如: ```c #define BIG_ENDIAN #define __cplusplus ``` ```c #include #define __DEFINE__H__ // 定义无值宏 #ifdef __DEFINE__H__  // 如果定义了宏__DEFINE__H__ // #ifndef __DEFINE__H__  // 如果未定义了宏__DEFINE__H__ // #define __DEFINE__H__  // 上下两个无值宏一般用于头文件中 #define PI    3.141526 // 无参宏 // 带参宏 #define MAX(a,b)  ((a)>(b)?(a):(b))  // 推荐 #define MIN(a,b)  (a #define A 1   //声卡 #define B 0   //网卡 #define C 0   //串口 #if A void fun_init_1(void) {     printf("%s is running...\n", __FUNCTION__); } #endif #if B void fun_init_2(void) {     printf("%s is running...\n", __FUNCTION__); } #endif #if C void fun_init_3(void) {     printf("%s is running...\n", __FUNCTION__); } #endif void fun_destory_1(void) {     printf("%s is running...\n", __FUNCTION__); } void fun_destory_2(void) {     printf("%s is running...\n", __FUNCTION__); } void fun_destory_2_end(void) {     printf("%s is running...\n", __FUNCTION__); } int main() { #if A     fun_init_1(); #endif #if B     fun_init_2(); #endif #if C     fun_init_3(); #endif     return 0; } ``` ```c #include #define A 0   //声卡 #define B 0   //网卡 #define C 1   //串口 #if A void fun_init_1(void) {     printf("%s is running...\n", __FUNCTION__); } #elif B void fun_init_2(void) {     printf("%s is running...\n", __FUNCTION__); } #else void fun_init_3(void) {     printf("%s is running...\n", __FUNCTION__); } #endif void fun_destory_1(void) {     printf("%s is running...\n", __FUNCTION__); } void fun_destory_2(void) {     printf("%s is running...\n", __FUNCTION__); } void fun_destory_2_end(void) {     printf("%s is running...\n", __FUNCTION__); } int main() { #if A     fun_init_1(); #elif B     fun_init_2(); #else     fun_init_3(); #endif     return 0; } ``` ```c #include void fun_destory_1(void) {     printf("%s is running...\n", __FUNCTION__); } // #define DEBUG #ifndef DEBUG void fun_destory_2(void) {     printf("%s is running...\n", __FUNCTION__); } #endif #ifdef  DEBUG   //gcc demo4.c -DDEBUG 编译时添加宏定义 void fun_destory_3_end(void) {     printf("%s is running...\n", __FUNCTION__); } #endif int main(void) {     fun_destory_1(); #ifdef  DEBUG     fun_destory_3_end(); #endif #ifndef DEBUG     fun_destory_2(); #endif     return 0; } ``` ## **头文件** 通常,一个常规的C语言程序会 ![](images/WEBRESOURCE61a25737180b4d579df0faf4cf8bb1c4stickPicture.png) ### **头文件的内容** - 头文件中所存放的内容,就是各个源码文件的彼此可见的公共资源,包括: 1. 全局变量的声明。 1. 普通函数的声明。 1. 静态函数的定义(内联函数)。 1. 宏定义。 1. 结构体、联合体的定义。 1. 枚举常量列表的定义。 1. 其他头文件。 - 示例代码: ``` // head.h extern int global; // 1,全局变量的声明 extern void f1(); // 2,普通函数的声明 static void f2() // 3,静态函数的定义 { ... } #define MAX(a, b) ((a)>(b)?(a):(b)) // 4,宏定义 struct node // 5,结构体的定义 { ... }; union attr // 6,联合体的定义 { ... }; #include // 7,其他头文件 #include #include ``` - 特别说明: 1. 全局变量、普通函数的定义一般出现在某个源文件(*.c )中,其他的源文件想要使用都需要进行声明(extern),因此一般放在头文件中更方便。 1. 静态函数、宏定义、结构体、联合体的定义都只能在其所在的文件可见,因此如果多个源文件都需要使用的话,放到头文件中定义是最方便,也是最安全的选择。 ### **头文件的使用** 头文件编写好了之后,就可以被各个所需要的源码文件包含了,包含头文件的语句就是如下预处理指令: ``` // main.c #include "head.h" // 包含自定义的头文件 #include // 包含系统预定义的文件 int main() { ... } ``` 可以看到,在源码文件中包含指定的头文件有两种不同的形式: - 使用双引号:在指定位置 + 系统标准路径搜索 head.h - 使用尖括号:在系统标准路径搜索 stdio.h ### **头文件的格式** 由于头文件包含指令 #include 的本质是复制粘贴,并且一个头文件中可以嵌套包含其他头文件,因此很容易出现一种情况是:头文件被重复包含。 - 使用条件编译,解决头文件重复包含的问题,格式如下: ``` #ifndef _HEADNAME_H #define _HEADNAME_H ... ... (头文件正文) ... #endif ``` 其中,HEADNAME一般取头文件名称的大写 # 文件组织 一个简易示例 ![](images/WEBRESOURCEb5c62efd51d146318ed20b94a949b34astickPicture.png) 由于自定义的头文件一般放在源码文件的周围,因此需要在编译的时候通过特定的选项来指定位置,而系统头文件都统一放在标准路径下,一般无需指定位置。 假设在源码文件 main.c 中,包含了两个头文件:head.h 和 stdio.h ,由于他们一个是自定义头文件,一个是系统标准头文件,前者放在项目 pro/inc 路径下,后者存放于系统头文件标准路径下(一般位于 /usr/include),因此对于这个程序的编译指令应写作: ``` gec@ubuntu:~/pro$ gcc main.c -o main -I /home/gec/pro/inc ``` 其中,/home/gec/pro/inc 是自定义头文件 head.h 所在的路径 - 语法要点: - 预处理指令 #include 的本质是复制粘贴:将指定头文件的内容复制到源码文件中。 - 系统标准头文件路径可以通过编译选项 -v 来获知,比如: ``` gec@ubuntu:~/pro$ gcc main.c -I /home/gec/pro/inc -v ... ... #include "..." search starts here: #include <...> search starts here: /usr/lib/gcc/x86_64-linux-gnu/7/include /usr/local/include /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed /usr/include/x86_64-linux-gnu /usr/include ... ... ``` ![](images/WEBRESOURCEc1cc8a34d5ae4ef6bd588fcb003a0b83image.png) ![](images/WEBRESOURCEcb03335b039dd3101bd1e33868113656image.png) **注意:*是通配符,表示将src目录下的所有以.c结尾的文件都参与编译** ![](images/WEBRESOURCE54710c7a76c34ce186adf7550689b814image.png) > **表示告诉编译器头文件所在路径,编译会去指定的路径寻找头文件** **应用场景(模块化编程)** ## 练习 将图书管理系统的C语言文件使用 ![](images/WEBRESOURCE2abc07694c4f358e3cffaedba77bcb90image.png)