目录
本文主要和大家分享可执行文件的生成过程以及深入学习预编译(预处理)。
一、翻译环境和执行环境
翻译环境:C语言的代码--->二进制的指令(可执行程序)。
执行环境:执行二进制的代码。
二、编译和链接
翻译环境:
· 组成一个程序的每个源文件通过编译分别转换成目标代码(.obj)
· 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
· 连接器同时也会引入标准C函数库中任何被该程序用到的函数,而且它可以搜索程序个人的 程序库,将其需要的函数也链接到程序中
画个图,简单说明一下:
编译本身分为几个阶段:
预编译:处理预处理指令
编译:语法分析,词法分析,语义分析,符号汇总
汇编:汇编代码翻译成二进制指令,形成符号表
链接:
合并段表 、符号表的合并和符号表的重定位
例如:在程序的编译过程中,一个未定义的函数最终会在链接阶段被发现。
运行环境:
程序执行的过程
1、程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2、程序的执行便开始,接着便调用main函数。
3、开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4、终止程序。正常终止main函数;也有可能是意外终止。
三、预编译
1、预定义符号
__FILE__ 进行编译的源文件
__LINE__ 文件当前行号
__DATE__ 文件被编译的日期
__TIME__ 文件被编译的时间
__STDC__ 如果编译器遵循ANSI C(C语言标准),,其值为1,否则未定义
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __TIME__);
printf("%s\n", __DATE__);
//printf("%d\n", __STDC__);
//当前VS不支持ANSI C
return 0;
}
2、#define
#define 定义标识符
语法:
#define name stuff
示例:
#define M 100
#define reg register //缩短名字
#define FOR for( ; ; ) //替换for循环实现
#define CASE break;case //实现了自动添加break,但是不建议经常使用
#define PRINTF printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" , \
__FILE__, __LINE__, \
__DATE__, __TIME__ );
//当定义内容太长时,可以分成几行,除了最后一行,每一行末尾加上反斜杠(续行符)
#define 定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者定义宏。
申明方式:
#define name(parament-list) stuff
parament-list 是由逗号隔开的符号表,还有可能出现在stuff中
注意:
参数列表的左括号必须于name紧邻,如果两者间有空白,参数列表会被解释为stuff的一部分
示例:
#define MAX(a,b) ((a)>(b)?(a):(b))
参数替换文本,参数不做任何处理,所以对数值表达式进行求值的宏定义都应该对个体以及整体加上括号,避免在使用时操作符之间的不可预料的相互作用。
演示:
#define M1(a) a*2
#define M2(a) ((a)*2)
int main()
{
int n = M1(3+2);
printf("%d\n", n); //7
//预期输出应该是10,但实际情况是 3+2*2=7 ,但是如果加上括号,就不会发生这种情况
int m = M2(3+2);
printf("%d\n", m); //10
return 0;
}
#define替换规则
· 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换。
· 替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被它们的值所替换
· 最后再次对结果文件进行扫描,看看是否包含任何由#define定义的符号,如果有则重复上述步骤
注意:
1、宏参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不能出现递归
2、当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#和##
注意:只能在宏中使用
# 将参数插入字符串中
演示:
#define PRINT(n, format) printf("the value of "#n" is "format"\n", n);
int main()
{
int a = 10;
int b = 20;
float f = 3.14f;
PRINT(a, "%d"); //the value of a is 10
PRINT(b, "%d"); //the value of b is 20
PRINT(f, "%.2f");//the value of f is 3.14
return 0;
}
## 把位于它两边的符号合成一个符号
这样的连接必须产生一个合法的标识符,否则其结果就是未定义的
示例:
#define CAT(x,y) x##y
int main()
{
int class113 = 2023;
printf("%d\n", CAT(class, 113)); //2023
printf("%d\n", class113); //2023
//连接产生了一个已定义的合法标识符
return 0;
}
带副作用的宏参数
当宏参数在宏定义中出现超过一次的时侯,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预知的后果,表达式求值的时候出现永久性效果。
示例:
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int a = 5;
int b = 6;
int c = MAX(a++, b++);
printf("c = %d\n", c); //7
printf("a = %d\n", a); //6
printf("b = %d\n", b); //8
return 0;
}
上述例子中参数在宏的定义中多次出现,而调用了带有副作用的参数使得表达式求值出现了永久性效果。所以:在传参时尽量不要传带有副作用的参数。
宏和函数的对比
宏通常用于执行简单的运算。
1、相对于函数来说,宏在程序的规模和速度方面跟胜一筹
2、函数的参数必须声明特定类型,所以函数只能在类型合适的表达式上使用。而宏则与类型无关,可以适用于整形,浮点型等用来比较的类型。
但宏同样有着它的缺点:
1、每次使用宏的时候,一份宏定义的代码将会插入到程序中,除非宏比较短,否则会大幅度增加程序的长度。
2、宏是无法调试的。
3、宏与类型无关,代表着不够严谨。
4、宏可能会带来运算符优先级的问题,导致程序容易出错。
但是宏可以做到函数做不到的事情,宏的参数可以出现类型,而函数不行
例如:
#define MALLOC(n,type) (type*)malloc(n*sizeof(type))
int main()
{
MALLOC(10,int);
//预处理替换后为 (int*)malloc(10*sizeof(int));
}
下面就宏和函数的区别,列出了一个表格
命名约定
函数和宏的使用语法很相似,C语言无法帮我们区分,我们平时的命名习惯就是
宏名全部大写
函数名不要全部
3、#undef
用于移除一个宏定义
#undef NAME
如果一个现有的名字要被重新定义,首先要移除旧的名字
示例:
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int c = MAX(3,5);
printf("%d\n", c); // 5
#undef MAX //删除宏
//MAX无法再次调用,除非重新定义
return 0;
}
4、条件编译
在编译程序时,如果想要将一条(一组)语句编译或者放弃时可以使用条件编译指令
1、条件编译
#if 常量表达式
......
#endif
常量表达式由预处理器求值
#if 1 //为真,执行
printf("haha!");
#endif
2、多个分支的条件编译
#if 常量表达式
......
#elif 常量表达式
......
#else
......
#endif
#if 1 //按照顺序进行判断,只会进入一个,然后结束条件编译
printf("1");
#elif 1
printf("2");
#else 1
printf("3");
#endif
3、判断是否被定义
#if defined(N) 是否被定义
#ifdef N
#if !defined(N) 是否没有被定义
#ifndef N
#if defined(N) //N被定义则执行
printf("%d\n", N);
#endif
#ifdef N
//N被定义则执行
printf("%d\n", N);
#endif
#if !defined N //没有定义则执行
printf("NO");
#endif
#ifndef N //没有定义则执行
printf("NO");
#endif
4、嵌套指令
#define N 10
#define M 10
#define O 10
#define P 10
int main()
{//支持嵌套,同一级别分支进入一次之后不会再次进入
#if defined(N)
#ifdef M
printf("yesm");
#endif
#elif defined(O)
#ifdef P
printf("yesp");
#endif
#endif
}
5、文件包含
#include指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样
预处理器先删除这条指令,并用包含文件的内容替换。
头文件被包含的方式
本地文件包含:一般多用于自定义的头文件包含
#include "filename.h"
编译器先在源文件所在目录下查找,如果未找到,还会在C语言的标准路径下查找头文件。
如果找不到就会提示编译错误。
库文件包含:
#include<filename.h>
编译器直接在标准路径下查找,如果找不到就会提示编译错误
这就代表着库文件也可以使用 " " 的方式包含,但是并不建议这样做,这样查找效率会降低,而且不容易区分本地文件和库文件
嵌套文件包含
头文件sou.h被嵌套包含,重复引入。但是也是可以避免这种问题的
在头文件开头写:
#ifndef __TEST_H__
#define __TEST_H__
头文件内容
#endif
或者在每个头文件内加上
#pragram