编译过程
Linux下编译C语言,即使用gcc命令直接编译,然后产生a.out可执行文件。
所谓GCC,实为GNU C语言编译器(GNU C Compiler),只能处理C语言。但其很快扩
展,变得可处理C++,后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective -
C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU
Compiler Collection),也就是个工具集合。
事实上,从源代码生成可执行文件可以分为四个步骤,分别是预处理(Preprocessing,命
令为cpp)、编译(Compilation,命令为cc)、汇编(Assembly,命令为as)和链接
(Linking,命令为ld)。
**预处理(Preprocessing)cpp**
命令:gcc -E hello.c -o hello.i
亦可: cpp hello.c
-E表示只进行预处理,生成经过预处理之后的文件,默认只输出预处理之后的内容,不会
生成文件,-o指定输出到hello.i中预处理过程主要是处理那些源文件和头文件中以#开头的命令,比如 #include、#define、
#ifdef 等。预处理的规则一般如下:
将所有的#define删除,并展开所有的宏定义。
处理所有条件编译命令,比如 #if、#ifdef、#elif、#else、#endif 等。
处理#include命令,将被包含文件的内容插入到该命令所在的位置,这与复制粘贴的效果一样。注意,这个过程是递归进行的,也就是说被包含的文件可能还会包含其他的文件。
删除所有的注释//和/* ... */。
添加行号和文件名标识,便于在调试和出错时给出具体的代码位置。
保留所有的#pragma命令,因为编译器需要使用它们。
预处理的结果是生成.i文件。.i文件也是包含C语言代码的源文件,只不过所有的宏已经被展
开,所有包含的文件已经被插入到当前文件中。当你无法判断宏定义是否正确,或者文件包含是
否有效时,可以查看.i文件来确定问题。
**编译(Compilation)cc**
命令:gcc -S hello.i 或者 gcc -S hello.c
亦可:cc -S hello.i 或者 cc -S hello.c
-S表示进行编译,执行该命令之后,如果没有语法错误,会默认生成hello.s文件。编译就是把预处理完的文件或源代码进行一些列的
词法分析
语法分析
语义分析以及优化
生成相应的汇编代码文件
**汇编(Assembly)as**
命令:gcc -c hello.s 或者 gcc -c hello.c
亦可:as hello.s -o hello.o
-c只编译,不链接,这个选项非常重要,在多文件编程中,可以对每个目标文件进行语法检
查。执行该命令之后会默认生成一个hello.o的目标文件。
汇编的结果是产生目标文件,在 GCC 下的后缀为.o,在 Visual Studio 下的后缀为.obj。
汇编真实的命令其实是as
**链接(Linking)ld**
命令:gcc hello.o
亦可:ld -e main -dynamic-linker /lib/ld-linux.so.2 -lc hello.o -o hello
链接的作用就是找到这些目标地址,将所有的目标文件组织成一个
可以执行的二进制文件。
预处理指令
#define
1.宏定义
#define H printf("")//预处理指令 宏 替换体
替换体可以是常量,字符串,字面值,或者是C语言的表达式,语句,函数。
如果替换体是常量,可以称宏定义为明示常量(宏常量),用#define指令来定义明示常量
也叫符号常量。
语法:
(1)#之前可以有空格或者TAB,#和define之间也可以有空格
(2)宏名称不能有空格
(3)宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简
单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程
序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
(4)宏折行需要用 \
(5)宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换
(6)预处理过程从宏变成最终替换文本的过程称为宏展开
(7)代码中的字符串如果与宏名相同,那么它仅仅是个字面值字符串,那么预处理程序不对
其作宏代替
(8)宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理
程序层层代换,简单替换,不能凭空加括号。
(9)习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母
(10)用宏定义类型
特别注意区别:宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶
段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一
种新的数据类型。
#define DT int
typedef int TT;
int main(){
unsigned DT a;
unsigned TT b; //错误的 编译报错
}
(11)用const声明常量和#define定义宏常量的区别
(a) 编译器处理方式不同
#define 宏是在预处理阶段展开。
const 常量是编译运行阶段使用。
(b) 类型和安全检查不同
#define 宏没有类型,不做任何类型检查,仅仅是展开。
const 常量有具体的类型,在编译阶段会执行类型检查。
(c) 存储方式不同
#define 宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏
定义不分配内存,变量定义分配内存。)
const定义常量会分配内存。
对于宏函数和和普通函数而言,普通函数只有一份,而宏函数替换之后,代码中
就会存储多份。
(12)可以在代码中删除一个宏定义 #undef
(13)C语言中预定义的宏
__LINE__:表示当前源代码的行号;
__FILE__:表示当前源文件的名称;
__DATE__:表示当前的编译日期;
__TIME__:表示当前的编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus:当编写C++程序时该标识符被定义;
__func__ 和 __FUNCTION__ : 表示当前编译的函数名。
上述宏不能用#undef取消
C99标准提供的__func__预定义标识符,是代表函数名的字符串,__func__必须具
有函数作用域。而从本质上看宏具有文件作用域。因此,__func__是C语言的预定义标识
符,而不是预定义宏。
**2.在#define中使用参数:宏函数 类函数宏**
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
带参宏定义的一般形式为:
#define 宏名(形参列表) 字符串
在字符串中可以含有各个形参。
带参宏调用的一般形式为:
宏名(实参列表);
宏函数需要注意的问题:
(1) 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
#define MAX(a,b) (a>b)?a:b
#define MAX (a,b) (a>b)?a:b //预处理不报错 编译报错
将被认为是无参宏定义,宏名 MAX 代表字符串(a,b) (a>b)?a:b。宏展开时,宏调用语句:
max = MAX(x,y);
max = (a,b)(a>b)?a:b(x,y);//这显然是错误的
(2) 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包
含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用
时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进
行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重
复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。
(3) 在宏定义中,字符串内的形参和整体通常要用括号括起来以避免出错
1 #define SQU1(x) x*x
#define SQU2(x) (x)*(x)
#define SQU3(x) ((x)*(x))
int main(){
printf("%d\n",SQU1(1+2));
printf("%d\n",9/SQU2(1+2));
printf("%d\n",9/SQU3(1+2));
return 0;
}
**3.用宏参数创建字符串:#运算符**
#用来将宏参数转换为字符串,这个过程称为字符串化,也就是在宏参数的开头和
末尾添加引号。
#define STR(x) #x
int main(){
printf("%s\n",STR(hello));
printf("%s\n",STR(9527));//把一个数字变成字符串的方法
return 0;
}
**4.预处理器黏合剂:##运算符**
##运算符可用于类函数宏的替换部分。而且,##还可以用于对象宏的替换部分。##运
算符把两个记号组合成一个记号(标识符)。
#define XNAME(n) x##n
#define PRINT_XN(n) printf("x"#n"=%d\n",x##n);
5.变参宏:... 和 __VA_ARGS__和 ##args
#define LOG1(fmt,args...) \
printf("%s %s %d:"fmt,__FILE__,__func__,__LINE__,##args)
#define LOG2(fmt,...) \
printf("%s %s %d:"fmt,__FILE__,__func__,__LINE__,__VA_ARGS__);
#include 文件包含
<> 从系统指定的目录下查找头文件,查找不到,出错。
" " 从当前工作目录下查找,如果当前工作目录下找不到,则从系统指定目录下查
找,查找不到报错。
系统指定的目录可以配置。
条件编译、头文件
#ifdef #else很像分支选择语句中的if else。两者的主要区别是,预处理器不识别用
于标记块的花括号({}),因此它使用#else(如果有需要)和#endif(必须存在)来标记
指令块。
条件编译相对于if else的代码来说,目标文件和可执行文件字节数会比较小,因为条
件编译有部分代码不会编译到目标文件中,而if else全部的代码都会编译到目标文件中。
#ifndef指令与#ifdef指令的用法类似,也可以和#else、#endif g一起使用,但是它们
的逻辑相反。
#ifndef PI
#define PI 3.1415926
#endif
文件卫士,在多文件中很重要的部分,防止头文件被重复引用。
#ifndef FILENAME_H__ //FILENAME为.h文件的名字
#define FILENAME_H__//.h中的内容
#endif // FILENAME_H__
多文件编程
1.头文件 头文件卫士 (一般会把功能相似或者处理同种事务的函数放在同一个头文件中)
函数声明
宏
类型
声明变量
不要把类型定义 和 函数定义放在头文件中
2.实现文件
包含头文件
实现在头文件声明的函数
3.在某个.c文件中要调用上述函数
包含头文件
最终编译:
把每一个.c编译成目标文件 gcc -c xxx.c
把所有的目标文件链接成可执行文件 gcc xxx.o yyy.o zzz.o
编译方法:
对单个.c文件进行编译检查语法 gcc -c x.c
对所有的.o文件进行链接生成可执行程序(以后会有动态库和静态库)
当编写好头文件和实现文件时,应该对它们逐一进行语法检查。
oper.h 头文件,一些关于运算的函数声明。
实现文件oper.c。
array.h,关于数组的操作函数声明。
gvar.h,此文件中包含全局变量和函数的声明。
最后,写一个测试文件main.c。
gcc -c oper.h 如果没有语法问题,会生成oper.h.gch ,需要把.gch文件删除
gcc -c oper.c 如果没有语法问题,会生成oper.o目标文件,保留
gcc -c array.h 生成array.h.gch 删除array.h.gch
gcc -c array.c 生成目标文件 array.o,保留
gcc -c gvar.h 生成gvar.h.gch 删除gvar.h.gch
gcc -c gvar.c 生成目标文件 gvar.o
gcc -c main.c 生成目标文件 main.o
如果所有源文件都编译通过,并生成对应的目标文件,则可以用
gcc oper.o array.o gvar.o main.o -o m
1.方便查错
2.支持增量编译 只编译修改了的代码 节省时间
对于.h文件也是可以通过 gcc -c x.h 进行检查语法错误
会生成一个 x.h.gch 文件
如果#include 一个.h文件时,它首先导入的不是.h 而是查找是否有.h.gch
这个文件,如果有这个文件,不会再依赖.h文件
如果.h修改,没有任何效果