1.翻译环境和运行环境
在 ANSI C 的任何⼀种实现中,存在两个不同的环境。第一种是翻译环境,在这个环境中,源代码被转换为可执行的机器指令(二进制指令);第二种是执行环境,它用于实际执行代码。
1.1翻译环境
翻译环境下,源代码被转换为可执行的机器指令(二进制指令)。
翻译环境由编译和链接俩个大的过程组成。编译又可以分解成:1,预处理(预编译)、2,编译、3,汇编三个过程。
编译:.c文件经过编译器的处理,生成.obj(.o)的目标文件。
链接:目标文件加上链接库在链接器的处理下生成可执行程序.exe。
1.2执行环境
用于实际执行代码。一般指操作系统。以GCC为例子,拆解编译链接的过程:
源代码(.c和.h为后缀的文件)经过预处理.c文件会生成.i为后缀的中间文件;.i为后缀的文件经过编译会生成.s为后缀的中间文件;.s文件经过汇编会生成.o为后缀的目标文件;然后多个目标文件加上链接库经过链接器的处理生成可执行程序.exe。
也就是说有如下过程:
C代码-->预处理-->编译-->汇编-->链接-->可执行程序
____________________________________________________________________
以GCC编译器为例子:所用到的指令
预处理:gcc test.c -E -o test.i //-o是output生成的意思
编译: gcc test.i -S -o test.s 将c代码转换成汇编代码,要进行语义分析...
汇编:gcc test.s -c -o test.o //将汇编代码转换成机器可执行的指令(二进制指令)
2.预处理详解
2.1预定义符号
__FILE__//进⾏编译的源⽂件__LINE__//⽂件当前的⾏号__DATE__//⽂件被编译的⽇期__TIME__//⽂件被编译的时间__STDC__//如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s line:%d\n", __FILE__, __LINE__);2.2#define 定义常量
//语法 #define name stuff //例子 #define MAX 1000 #define reg register //为 register这个关键字,创建⼀个简短的名字 #define do_forever for(;;) //⽤更形象的符号来替换⼀种实现 #define CASE break;case //在写case语句的时候⾃动把 break写上。 // 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏ 符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n" ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )// \为续航符,一行写不完可以用续航符多行2.3#define定义宏
#define 机制包括了⼀个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。宏是纯粹的文本替换。
#define name( parament-list ) stuff /* 其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。 注意: 参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的 ⼀部分。 */ //例子,宏是纯粹的文本替换 //宏实现数的平方 #define SQUARE(x) x * x int main() { int a = 5; int ret = SQUARE(a); printf("%d\n",ret); return 0; }2.4带有副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。
x+1;//不带副作⽤ x++;//带有副作⽤(改变了x的值) //写一个宏求俩个数的较大值 #define MAX(x,y) (x) > (y) ? (x) : (y) int main() { int a = 3; int b = 5; int ret = MAX(a++, b++); //a++中的++为后置++,在三目运算符中,先进行判断再自增。显然3<5,判断完后a,b各自自增一次为4、6 //又y>x,则执行:后表达式 b++,先将b的值(此时为6)赋给ret ,再自增为7 printf("ret = %d\n", ret);//6 printf("a = %d\n", a);//4 printf("b = %d\n", b);//7 return 0; }2.5宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
1,宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2,当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
2.6宏和函数的对比
宏通常被应⽤于执行简单的运算。
//⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。 #define MAX(a, b) ((a)>(b)?(a):(b))那为什么不选择函数完成呢?
1,用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。
2,更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 >来比较的类型。宏的参数是类型无关的。
和函数相比,宏的劣势:
1,每次使用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2,宏是没法调试的。
3,宏由于类型无关,也就不够严谨。
4,宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type)\ (type )malloc(num sizeof(type)) ... //使⽤ MALLOC(10, int);//类型作为参数 //预处理器替换之后: (int *)malloc(10 sizeof(int));3.#和##
#运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为"字符串化"。
#define Print(n,format) printf("the value of "#n " is "format"\n",n) //#运算符 int main() { int a = 1; Print(a,"%d");//the value of a is 1 return 0; }##运算符可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 ##被称为记号粘合。
这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。在实际开发过程中##使⽤的很少,很难举出恰当例子。
#define GENERIC_MAX(type) \ type type##_max(type x,type y) \ { \ return (x>y?x:y); \ } GENERIC_MAX(int) GENERIC_MAX(float) int main() { int m = int_max(2,3); printf("%d\n",m); float fm = float_max(3.5f, 4.5f); printf("%.2f\n",fm); return 0; }4.命名约定
⼀般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分⼆者。那我们平时的⼀个习惯是:
- 把宏名全部大写
- 函数名不要全部大写
这只是大部分情况,也有宏是全小写,如offestof(用来计算结构体成员相较于结构体起始位置的偏移量)
5.#undef
这条指令用于移除⼀个宏定义。
#undef NAME //如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。6.条件编译
在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
//比如调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。 #include <stdio.h> #define __DEBUG__ int main() { int i = 0; int arr[10] = {0}; for(i=0; i<10; i++) { arr[i] = i; #ifdef __DEBUG__ printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 #endif //__DEBUG__ } return 0; }常见条件编译指令:
1. #if 常量表达式 //若if后常量表达式为假,则不执行 //... #endif //常量表达式由预处理器求值。 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif #if 0 //##运算符 #define GENERIC_MAX(type) \ type type##_max(type x,type y) \ { \ return (x>y?x:y); \ } GENERIC_MAX(int) GENERIC_MAX(float) int main() { int m = int_max(2,3); printf("%d\n",m); float fm = float_max(3.5f, 4.5f); printf("%.2f\n",fm); return 0; } #endif 2.多个分⽀的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif 3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol 4.嵌套指令 #if defined(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #endif7.头文件的包含
7.1头文件被包含的方式
- 本地文件包含 : #include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。如果找不到就提示编译错误。
- 库文件包含: #include <filename.h>
查找头文件直接去标准路径下查找,若找不到就提示编译错误。
7.2嵌套文件包含
我们已经知道, #include指令可以使另外⼀个文件被编译。就像它实际出现于#include指令的地方⼀样。
这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。
为了解决这个问题,在头文件中写入下行代码就可避免头文件被重复编译。
#pragma once