- 编译和运行
- 预处理
- 编译
- 汇编
- 链接
- 运行阶段
- 预定义符号
- #define的使用
- #define定义标识符
- #define不要加上分号
- #define实现宏
- #和##的使用
- 宏和函数的对比
- 命令行定义
- #undef
- 条件编译
- #if和#endif
- #ifdef和#ifndef(no define)
- #elif
- #include中""和<>的区别
- 如何避免重复包含头文件
我们都知道计算机是只认识二进制的,当我们写下这种文本代码时,计算机是怎么将这些文本转换成二进制让机器看的懂的。
经过两个大阶段:编译和运行
编译又分为了四个小阶段,分别为:预处理(预编译),编译,汇编,链接
这些过程在linux上可以清晰的看到会发生什么。
大概用文字描述一下:
在预处理阶段程序会做三件事情
- 把程序中的注释全部删除
- 头文件展开,比如stdio.h等等
- 把#define进行替换
编译阶段干的最明显的事情就是把高级语言代码转换成汇编代码
包括:语法分析,词法分析,语义分析,符号汇总等等。这都是编译原理里面的内容。
这里简单说一下符号汇总,汇总的都是全局的符号,比如全局变量,函数等等。汇总的原因是为了在汇编阶段形成符号表(符号表是记录全局符号的地址的表格,相当于地图吧)
汇编生成了目标文件
我们经常看见的.obj后缀的文件就是在这个阶段生成的
大致做两件事情
- 把汇编代码转换成机器语言(二进制)
- 形成符号表
下面这些就是符号表,就是记录符号的地址
把所有目标文件全部通过链接器变成一个可执行程序(后缀是.exe)。如果目标文件里面含有库里面使用的函数,或者是程序员自己写的函数,也会在链接库里面链接起来。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
一句话总结:函数在链接阶段被链接器把它与目标文件结合。
另外要做的两件事情:
- 合并段表
- 合并符号表
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同
时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。 - 终止程序。正常终止main函数;也有可能是意外终止
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
int main()
{
printf("%s", __FILE__);
return 0;
}
比如__FILE__就可以代表代码的文件名
常见用法是这样的:
#define MAX 1000
其实它还可以替换名字,例如:
#define INT int #define do_forever for(;;)
只要替换后程序没有错误,就可以替换。
#define不要加上分号如果加上了分号,可能会造成程序报错。
例如下面的例子:
#define MAX 1000;
printf("%d",MAX);
#define实现宏
宏的定义:
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
例如:
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
注意:宏的实现不要吝啬括号的使用,因为会很容易造成错误。具体例子就不举了。
#和##的使用这两个符号很少见,也很有趣。
通过一个问题来引出#的使用方法
如何把参数插入到字符串中?
我们怎么让parametre是可变的呢?在第一次printf时代表a,在第二次printf时代表b。
int main()
{
int a = 10;
int b = 20;
printf("the value of the parametre is %d", a);
printf("the value of the parametre is %d", b);
return 0;
}
这时候我们就要在宏里面使用#来解决这个问题。
使用 # ,把一个宏参数变成对应的字符串。
这么写就可以了。#x把宏参数x变成了字符串x。
#define PRINT(x) printf("the value of the "#x" is %dn",x);
注:字符串是可以自动拼接的。
如:
printf("hello ""world");
这行代码会打印出hello world。
我们还可以用这个符号让宏更加美观
这样写会更加清晰。
#define PRINT(x)
printf("the value of the "#x" is %dn",x);
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
这个符号可以是任何类型的
比如下面这个例子:
#define CAT(a,b) a##b
int helloworld = 2021;
printf("%d",CAT(hello, world));
hello和world会被拼接成helloworld,这个变量刚好是等于2021.
甚至这样都可以
#define CAT(a,b) a##b
printf("%d", CAT(1,2));
//会打印12
宏和函数的对比
这个东西不用背,理解理解就好了。
- 宏: 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。函数:函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
- 宏:执行速度更快,因为它在预处理时就替换好了,不需要额外的开销。函数:存在函数的调用和返回的额外开销,所以相对慢一些
- 宏:宏要考虑优先级优先,要加很多括号。函数:函数不需要额外考虑优先级问题,结果容易预测。
- 宏:宏的参数是任意的,甚至可以传入类型。函数:函数的参数是固定的
- 宏:宏不可以调试。因为调试已经在程序运行阶段了,这时候宏早就消失了。函数:函数可以调试
- 宏:宏不可以递归。函数:函数可以递归。
#undef可以消除一个宏定义
这段代码会报错,因为PRINT这个宏已经被消除了。
#define PRINT(x)
printf("the value of the "#x" is %dn",x);
int main()
{
#undef PRINT(x)
PRINT(a);
PRINT(b);
}
条件编译
#if和#endif
这个和if语句使用方法一样。
如果满足条件就进入,不满足就不进入。
下面这段代码就是不会编译第一个main函数,因此程序不会报错。
#if 0
int main()
{
return 0;
}
#endif
int main()
{
return 0;
}
注:#if后面接的表达式一定要是常量表达式,不可以是变量
#ifdef和#ifndef(no define)#ifdef代表的意思是,如果这个被定义了,就进入编译
#ifndef代表的意思是,如果这个没有被定义,就进入编译
定义了这个符号,因此就进入了main函数
#define __DEBUG__ 1
#ifdef __DEBUG__
int main()
{
return 0;
}
#endif
没有定义这个符号,因此也进入main函数(这里是#ifndef)
#ifndef __DEBUG__
int main()
{
return 0;
}
#endif
#elif
如下面这段代码,只会打印一个1
int main()
{
#if 1
printf("1");
#elif 0
printf("1");
#endif
}
#include中""和<>的区别
很简单,直接上结论。
双引号:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。
尖括号:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含? 答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
有两种方法:
第一种,这种比较简单
#pragma once
第二种
#ifdef __TEST.H__ #define __TEST.H__ #endif
原理:第一次包含头文件的时候,__TEST.H__没有被定义,因此进入编译,包含头文件。第二次包含头文件的时候,__TEST.H__已经被定义过了,这时候就不进入编译。因此不会重复包含头文件。
注:由于头文件在预编译阶段已经全部展开了,因此#define也会被展开几份,因此这么写是没有问题的。



