程序环境和预处理
一、程序环境
在ANSI C的任何一种实现中,存在两个不同的环境。分别是翻译环境和执行环境
1. 翻译环境
在这个环境,源代码被转换成可执行的机器指令(二进制指令)。
①编译和链接
隔离编译,一起链接
- 组成一个程序的每一个源文件通过编译这个过程分别转换成目标代码(object code)。
- 每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的库函数也链接到程序中。
注:头文件直接包含在源文件中,所以头文件在编译时是不存在的
②编译+链接的四个阶段
前三个阶段属于编译,最后进行连接
-
预编译阶段:
-
编译阶段:
-
汇编阶段:
-
链接阶段:
注:链接阶段会通过符号表,查找跨文件函数、全局变量…。
链接时发生错误,则报错:无法解析的外部符号。
2. 执行环境
执行环境就是执行代码
程序执行的过程:
- 程序载入内存,在有操作系统的环境中,由操作系统完成。在独立的环境中,程序的载入由手动控制,也可能是通过可执行代码置入只读内存来完成。
- 接着就是调用main函数。
- 开始执行程序代码,这时程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也使用静态(static)内存,存储在静态内存的变量在程序的整个执行过程一直保留他们的值。
- 终止程序,正常结束main函数,也能会出现别的情况,意外终止。
二、预处理
1. 预定义符号
这些预定义符号都是语言内置的。
eg:
2. #define
①#define定义标识符
语法:
#define name stuff
eg:
#define MAX 100
#define reg register //为register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号替换一种实现
#define CASE break;case //在写case语句时自动把break写上
//如果定义的stuff过长,可以写成几行,除了最后一行,
//每行的后面都要加一个反斜杠(续行符)。在vs2019上可加可不加
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n",\
__FILE__,__LINE__, \
__DATE__,__TIME__);
注:在define定义标识符的时候,最好不要加上 ; 这样容易出问题,因为再替换的时候他会把这个符号也替换上去
②#define 定义宏
- 宏的定义:
#define机制包括一个规定,允许把参数替换到文本中,这种实现通常被称为宏或定义宏- 宏的声明方式:
#define name(parament-list) stuff
其中parament-list是一个由逗号隔开的符号表
注:参数列表的左括号必须与name紧邻,否则参数列表就会被解释为stuff的一部分。
eg:
#include <stdio.h>
#define SQUARE(x) x*x
#define DOUBLE(x) x+x
int main()
{
printf("%d\n", SQUARE(5)); //output:25
printf("%d\n", SQUARE(5 + 1)); //output:11
printf("%d\n", 10 * DOUBLE(5));//output:55
return 0;
}
注:因为宏是整体替换,所以可能受操作符优先级等作用影响,所以尽量加一个**()**
eg:对数值表达式进行求值的宏定义加上()以免出问题
#define DOUBLE(x) ((x) + (x))
③#define 替换规则
注:程序中#define替换涉及的步骤
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果有,则他们首先被替换。
- 替换文本后被插入程序中原来文本的位置,对于宏,参数名被他们的值替换。
- 最后,再对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果有就重述上述处理过程。
注:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
- 当处理器搜索#define定义的符号时,字符常量的内容并不被搜索
eg:
#define M 100
“MDHJAK”,中的M是不会被替换的
④#和##
#的作用
eg1:字符串有自动连接的特点
#include <stdio.h>
int main()
{
char* p = "hello ""bit\n";
printf("hello ""bit\n");
printf("%s", p);
return 0;
}
//output:
//hello bit
//hello bit
eg2:
#include <stdio.h>
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
int main()
{
PRINT("%d", 10);
//转换:
printf("the value is ""%d""\n", 10);
//这可以看成"the value is "和"%d"和"\n"
return 0;
}
//output:
//the value is 10
//the value is 10
eg3:使用#,把一个宏参数变成对应的字符串。代码中的#VALUE会被预处理处理为"VALUE"
#include <stdio.h>
#define PRINT(FORMAT, VALUE)\
printf("the value of "#VALUE" is "FORMAT"\n",VALUE);
int i = 10;
int main()
{
PRINT("%d", i + 3);
return 0;
}
//output:the value of i + 3 is 13
##的作用
- ##可以将位于其两边的符号合成一个符号,
- 它允许宏定义从分离的文本片段创建一个标识符。
eg:
#include <stdio.h>
#define ADD_TO_SUM(num,value) \
sum##num +=value;
int main()
{
int sum5 = 0;
int num = 0;
ADD_TO_SUM(5, 10);
printf("%d\n", sum5);
return 0;
}
//output:10
简单来说,就是把sum和num连接在一起。
注:在主函数中定义连接后的一个标识符sum5,让sum和num连接变得合法。否则其结果就是未定义。
⑤带副作用的的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么我们在使用这个参数的时候就带有危险,导致出乎意料的结果。副作用就是表达式求值出现的永久性效果,如i++;
eg:
#include <stdio.h>
// 这里a、b参数在定义时就不止出现一次
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x = %d y = %d z = %d\n", x, y, z);
return 0;
}
//output:x = 6 y = 10 z = 9
通过上面例子的结果,发现结果不是我们所想的。所以在使用宏的时候一定要注意它的参数。
⑥宏和函数的对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,否则程序的长度会大幅增长 。 | 函数的代码只出现于一个地方;每次使用这个函数,同调用那个地方的同一份代码。 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对较慢 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近的操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多一些括号。 | 函数参数只在调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只有在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,他就可以使用任何参数类型。(因此宏也不够严谨) | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使它们执行的任务是相同的。 |
调试 | 宏是不方便调试的。(在预处理阶段,宏就会被替换成内容相对应的代码) | 函数是可以逐语句调试的。 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
当然还有别的不同。例如:宏的参数可以出现类型,但是函数做不到
例子:
#define MALLOC(num,type)\
(type*)malloc(num*sizeof(type))
int main()
{
//....
//使用
MALLOC(10, int);
//替换后
(int*)malloc(10 * sizeof(int));
return 0;
}
小结:逻辑简单用宏,相反用函数
⑦宏和函数的命名约定
一般来说,函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者
所以按照大多数程序员习惯,
把宏名全部大写
把函数名不全部大写
3. #undef
这条指令用于移除一个宏定义
语法:
#undef NAME
如果现存的一个名字需要重新被定义,那么首先要移除它的旧名字
eg:
#include <stdio.h>
#define MAX 100
int main()
{
printf("%d\n", MAX);//ok
#undef MAX
printf("%d\n", MAX);//err,已经取消了MAX定义
return 0;
}
4. 命令行定义
一些C编译器提供一种能力,允许在命令行定义符号。
根据一个源文件编译出一个程序的不同版本的时候,例如:数组大小,当机器内存有限,我们要一个小的数组,当内存大的时候,我们希望数组大些。
eg:
#include <stdio.h>
int main()
{
int arr[ARR_SIZE];
int i = 0;
for (i = 0; i < ARR_SIZE; i++)
{
arr[i] = i;
}
for (i = 0; i < ARR_SIZE; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
linux 环境演示
gcc -D ARR_SIZE=10 programe.c
5. 条件编译
在编译一个程序的时候,如果我们要将一条或者一组语句编译或者放弃是很方便的,使用条件编译指令。
例如:调试性代码,删除可惜,保留碍事,这时就可以使用条件编译
eg:
#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__ //判断__DEBUG__是否被定义,如果定义执行下一条,否则不执行一直到#endif
printf("%d\n", arr[i]);
#endif //结束
}
return 0;
}
常见的条件编译指令:
2.多分支的条件编译
3.判断是否被定义
嵌套指令
6. 文件包含
#include指令可以使另外一个文件被编译,就如同所写文件实际出现在#include指令的地方。
替换方式:
1.预处理器先删除这条指令,并用包含文件的内容替换
2.这样一个文件被包含10次,那实际就被编译10次
①头文件包含方式
本地文件包含
#include “filename”
查找次序:
先在源文件目录下寻找,如果头文件未找到,编译器就像查找库函数头文件一样在标准位置查找文件。都找不到就会出现编译错误。
Linux环境的标准头文件路径:
/usr/include
vs环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft visual Studio 12.0\VC\include
根据自己的安装路径寻找
库文件包含
#include <filename.h>
查找头文件直接去标准路径下去寻找,如果找不到直接编译错误。
不建议查找库文件也用" "这个形式包含,这样查找会让效率变低,也不容易区分是库文件还是本地文件。
②嵌套文件包含
comm.h和comm.c是公共模块
test1.h和test1.c使用了公共模块
test2.h和test2.c使用了公共模块
test.h和test.c使用了test1和test2模块
这样最终链接在一起就会出现两次comm.h的内容,造成文件内容重复
解决:条件编译
具体:两种方法
在每一个头文件的开头写