目录
一,预处理
c预处理指令在程序执行前查看程序
1,预处理操作 test.c
- 完成头文件的包含(#include)
- #define 定义的符号和宏的替换(只替换不运算)
- 把文本划分成预处理记号序列、空白序列和注释序列,用空格字符替换注释
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中
2,编译 test.i
- c语言代码转化成汇编代码
- 语法分析,词法分析,语义分析,符号汇总(生成符号表)
编译完成之后就停下来,结果保存在test.s中
3,汇编 test.s
- 把汇编代码转换成二进制代码或机器指令(二进制指令)
- 生成符号表(elf格式)
符号表: 符号 + 地址 ; 地址分为有效地址和无效地址;
有效地址:原生
无效地址:引用外部
汇编完成之后,结果保存在test.o中
注: 文件后缀很重要,文件名是什么根据具体情况
4,link.exe链接(链接器)
- 合并段表
- 符号表的合并和重定位(若有两相同的符号,舍弃无效地址)
5,运行环境 (过程)
- 程序必须载入内存中,在有操作系统的环境中,一般这个都是由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
- 程序的执行便开始,接着调用main()函数
- 开始执行程序代码,这个时候程序将使用一个运行时堆栈存储函数(stack)的局部变量和返回地址。程序同时也可以使用静态内存(static)存储于静态内存中的变量在程序的整个执行过程中一直保持它们的值
- 终止程序,接着终止main()函数,也可能是意外终止
二,#define定义表示符
1,预定义符号
前后都是两个_ 符号
宏 | 含义 |
_ _DATE_ _ | 文件被编译的日期 |
_ _FILE_ _ | 表进行编译的源文件 |
_ _LINE_ _ | 当前源文件的行号 |
_ _STDC_ _ | 如果编译器遵循ANSI C,其值为 1,否则未定义 |
_ _STDC_HOSTED_ _ | 本机环境设置为1,否则设置为0 |
_ _STDC_VERSION_ _ | 支持c99标准设置为:199901L; 支持c11标准设置为:21112L; |
_ _TIME_ _ | 翻译代码的时间,格式为 “ hh:mm:ss” |
printf("file = %s ,line = %d\n",__FILE__ ,__LINE__);
宏前后的 __ 是连一起的,分开写是方便了解需要注意
2,#define 定义标识符
语法: #define name stuff
#define :预处理指令
name : 宏
stuff : 替换体
用宏是只替换不计算的
#define MAX 100 //定义标识符常量
#define REG register ; //给关键字创建一个更简短的名字
#define DO_FOREVER for(;;) //用更形象的符号替换一种实现
#define CASE break;case // 更简短的操作写case语句
如果定义的 stuff 过长,可以分成几行来写,除了最后一行外每行的后面加一个反斜杠(\续行符)
如:
#define DEBUG_PRINT printf("file:%s\nline:%d\n\
data:%s\ntime:%s\n",\
__FILE__,__LINE__,\
__DATE__,__TIME__)
在标识符最后加上分号 ; 容易发生错误导致问题不建议加!
3,在#difine中使用参数(宏)
允许把参数替换到文本中,这种实现通常称为宏或者宏定义
声明方式:
#define name(parameter list) stuff
parameter list : 宏参数
stuff : 替换体
注:
- 参数列表的左括号必须与name紧邻
- 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
定义一个宏并使用:
第一行输出没有问题,为什么第二行就出现错误了呢?
因此要用足够多的圆括号来解决这种情况,确保运算和结合的正确顺序
同时在定义宏时不要使用++,--这种运算符,也容易出现错误情况
4,#define替换规则
- 在调用宏时,首先要对参数进行检查,看看是否包含任何由#define定义的符号,如果是,他们首先被替换
- 替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值替换
- 再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程
注:
- 宏参数和#define定义中可以出现其它#define定义的变量,但是对于宏来说不能出现递归
- 当预处理搜索#define定义的符号时,字符串常量的内容并不被搜索
5,#和##
1,#运算符
使用#把一个宏参数变成对应的字符串
如果 x 是一个宏参数,那么第一次使用宏时 #x 会被替换为 第一次使用时传递的参数
#define A(x) printf("Hello " #x"orld %d\n",((x)*(x)))
int main()
{
int w = 5;
A(w);
A(6);
return 0;
}
//第一次调用时
//Hello world 25 //将#x替换成w
//第二次调用
//Hello 6orld 36 //将#x替换成6
2.##运算符
使用##,可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
当然要注意的是,用##运算符连接必须产生一个合法的标识符,否则就是未定义的
#define NAME(n) x##n
#define USE_NAME(n) printf("x"#n" = %d\n",x##n)
int main()
{
int NAME(1) = 12; //声明x1 x2 x3,并 赋值
int NAME(2) = 13;
int x3 = 14;
//如果没有定义上面的内容直接使用下面的宏会报错
USE_NAME(1); //使用x1 x2 x3的值然后打印
USE_NAME(2);
USE_NAME(3);
return 0;
}
//将会输出
x1 = 12 ;
x2 = 13 ;
x3 = 14 ;
6,变参宏
...(三个点) 和_ _VA_ARGS_ _(实际上前后的_ 是连在一起的,分开是方便阅读)
一些函数接受数量可变的参数,stdvar.h 头文件提供了工具
通过把宏参数列表中最后的参数写成(...)来实现,__VA_ARGS__放在替换体中,如:
#define PR(...) printf(__VA_ARGS__)
int main()
{
float wt = 125.5, sp = 65.5 ;
PR("hello\n");
PR("weight = %f ,shiping = $%.2f\n", wt, sp);
return 0;
}
//第一次调用打印
//hello
//第二次调用打印
//weight = 125.500000 ,shiping = $65.50
使用时要注意,... 只替换最后的宏参数!!!
#define PR(...) printf(__VA_ARGS__)
//这样的是可以的
#define PR(x,y,z,...) printf(__VA_ARGS__)
//这样做也是可以的
#define PR(x,...,y) printf(__VA_ARGS__)
//这样做不行,会报错
7, 移除一个宏定义
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
#undef //移除一个宏定义
//使用
#undef NAME
三,宏和函数的比较
类别 | 宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中,除了非常小的宏外,程序的长度会大幅增加 | 代码出现在一个地方,每次使用都调用同一份代码 |
执行速度 | 快 | 存在调用和返回的额额外开销,相对慢一些 |
操作符优先级 | 参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符优先级可能会产生不可预料的后果 | 参数只在调用的时候求值一次,它的结果值传递给函数,表达式的值更容易预测 |
副作用 | 阐述不可预料的结果 | 更容易被操控 |
参数类型 | 无类型,只要操作合法,就可以使用任何参数 | 与类型有关 |
调试 | 不方便调试 | 可以逐语句调试 |
递归 | 不可以递归 | 可以 |
总结
使用宏时应该注意:
- 宏名字中不可以有空格,但是在替换字符串中可以有空格,
- 用圆括号把宏的参数和整个替换体括起来,这样能确保被括起来的部分在使用时可以正确的展开
- 用大写字母标识宏变量的名称
- 如果打算用宏来加快程序运行的速度,那么首先要确定使用宏和使用函数是否会导致较大的差异;只使用一次的宏无法明显的减少程序运行的时间,在嵌套循环中使用宏更有助于提高效率。