【C语言篇】编译和链接以及预处理介绍(上篇)1:https://developer.aliyun.com/article/1617221
运行环境
- 程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序 的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成。
- 程序的执⾏便开始。接着便调⽤
main
函数。 - 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(
stack
),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static
)内存,存储于静态内存中的变量在程序的整个执⾏过程 ⼀直保留他们的值。 - 终⽌程序。正常终⽌
main
函数;也有可能是意外终⽌。
预处理(预编译)详解
在上面的翻译环境简单介绍了预处理的一些操作,在接下来将会比较详细的讲解一下:
预定义符号
__FILE__ //进⾏编译的源⽂件 __LINE__ //⽂件当前的⾏号 __DATE__ //⽂件被编译的⽇期 __TIME__ //⽂件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
在gcc上举例:
int main() { printf("%s\n", __FILE__); printf("%d\n", __LINE__); printf("%s\n", __DATE__); printf("%s\n", __TIME__); printf("%d\n", __STDC__);//在gcc上打印出1,说明gcc完全遵循ANSI C(如果在VS上则是未定义,说明VS上不完全遵循ANSI C) return 0; }
#define定义常量
基本语法:
#define name stuff
举个例子:
#define MAX 1000 #define reg register //为 register这个关键字,创建⼀个简短的名字 #define do_forever for(;;) //⽤更形象的符号来替换⼀种实现 #define CASE break;case //在写case语句的时候⾃动把 break写上。 例: int main() { int n = 0; scanf("%d", &n); switch (n) { case 1: CASE 2: CASE 3: ······· } return 0; }//不建议这么搞,可读性差 // 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符) 符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n" ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )
在定义标识符的时候,不要在最后加上;
比如:
#difine a 5; int main() { int flag=0; if(a)//预处理替换后就变为if(5;) flag=1; return 0; }
直接报错
#define定义宏
#define
机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro
)或定义宏 (define macro
)。
下⾯是宏的申明⽅式:
#define name( parament-list ) stuff
其中的 parament-list
是⼀个由逗号隔开的符号表,它们可能出现在stuff
中。
注意:
参数列表的左括号必须与name
紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff
的 ⼀部分。
举例:
//实现一个宏,求平方 #define SQUARE(x) x * x int main { int m = SQUARE(5); return 0; }
这个宏接收⼀个参数 x
.如果在上述声明之后,把 SQUARE( 5 )
置于程序中,预处理器就会⽤下⾯这个表达式替换上⾯的表达式: 5 * 5
这就是在求5的平方
警告:
这个宏存在一个问题:
问题:
#define SQUARE(x) x * x int main { int m = SQUARE(5+1); return 0; }
预处理器替换后:
int main { int m =5+1*5+1; return 0; }
我们本来要求6的平方,结果替换后先算乘法,算出来为11
改进:
#define SQUARE(x) (x) * (x)
这里我们认识到,定义宏单个参数要加括号
这里还有一个宏:
//写一个代码求一个数的2倍 #define DOUBLE(x) (x)+(x)
这个宏也存在一个问题:
问题:
//写一个代码求一个数的2倍 #define DOUBLE(x) (x)+(x) int main() { int ret = 10 * DOUBLE(5); return 0; }
预处理器替换后:
int main() { int ret = 10 *(5)+(5); return 0; }
我们本来要求10*10,结果算出来是55
改进:
#define DOUBLE(x) ((x)+(x))
这里我们认识到,宏定义参数列表外部也需要加括号
综上:
所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,简单来说就是内部括号+外部括号,避免在使⽤宏时由于参数中的操作符或邻近操作符之间不可预料的相互作⽤。
带有副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。
例如:
x+1;//不带副作⽤ x++;//带有副作⽤
MAX宏可以证明具有副作⽤的参数所引起的问题。
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) ... x = 5; y = 8; z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
这⾥我们得知道预处理器处理之后的结果是什么:
z = ( (x++) > (y++) ? (x++) : (y++));
所以输出的结果:
x=6; y=10; z=9;
可以看到,在使用++
操作符时,让x和y的值发生了多次改变,从而出现了不可预料的结果,这就是带有副作用的宏参数
宏替换的规则
在程序中扩展#define
定义符号和宏时,需要涉及⼏个步骤。
- 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由
#define
定义的符号。如果是,它们⾸先被替换。 - 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由
#define
定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和
#define
定义中可以出现其他#define
定义的符号。但是对于宏,不能出现递归。 - 当预处理器搜索
#define
定义的符号的时候,字符串常量的内容并不被搜索。
例子:
#include <stdio.h> #define M 100 #define N M+2 #define MAX(x, y) ((x)>(y)?(x):(y))//不能出现递归 int main() { int m = MAX(M, 15);//两次替换 printf("MAX(M, 15)");//不被搜索 return 0; }
宏和函数的对比
宏通常被应⽤于执⾏简单的运算。
⽐如在两个数中找出较⼤的⼀个时,写成宏,更有优势⼀些。
//宏 #define MAX(x, y) ((x)>(y)?(x):(y)) //函数 int Max(int x, int y) { return x > y ? x : y; } int main() { int a = 10; int b = 20; int m = MAX(10, 20); //直接替换:int m = ((10) > (20) ? (10) : (20)); printf("%d\n", m); int m = Max(10, 20); printf("%d\n", m); return 0; }
那为什么不⽤函数来完成这个任务? 原因有⼆:
- ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹。
二者在执行核心运算指令差不多,但函数多了调用准备工作和返回的指令
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之, 这个宏怎可以适⽤于
整形
、⻓整型
、浮点型
等可以⽤于>
来⽐较的类型。宏的参数是类型⽆关的。
宏有时候可以做函数做不到的事情。
⽐如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(n, type) (type*)malloc(n*sizeof(type)) int main() { //int *p = (int*)malloc(10*sizeof(int)); //更加方便如下: int *p = MALLOC(10, int); //int* p = (int*)malloc(10 * sizeof(int)); return 0; }
但是但是:
和函数相⽐宏的劣势:
- 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
- 宏是没法调试的。 (调试时程序已经运行起来了,但宏在编译阶段的预编译就完成了)
- 宏由于类型⽆关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程序求值等计算容易出现错。
综上我们来对比一下:
写在最后
关于编译链接具体过程十分复杂,本篇只是笼统地介绍了一下,在下一篇博客中将会把预处理阶段其它的知识进行比较详细的介绍,敬请期待喔😘
以上就是编译和链接以及预处理介绍(上篇)内容啦,各位大佬有什么问题欢迎在评论区指正,您的支持是我创作的最大动力!❤️