文件的编译链接和预处理
- 程序的翻译环境和执行环境
- 翻译环境
- 预处理(预编译)过程
- 编译过程
- 汇编过程
- 链接过程
- 运行环境
- 预处理详解
- 预处理符号
- 预处理指令
- #define
- #define定义标识符
- #define定义宏
- #define替换规则
- #与##
- #的使用
- ##的使用
- 带有副作用的宏参数
- 宏与函数的对比
- 宏的优势
- 函数的优势
- 宏与函数的命名约定
- undef
- 命令行定义
- 条件编译
程序的翻译环境和执行环境
在ANSIC的任何一种实现中,存在俩个不同的环境:
1.翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)
2.执行环境,用于执行代码
- 计算机只能执行二进制的指令
翻译环境
在程序编译过程:
- 组成一个程序的每一个源文件通过编译过程分别转换为目标代码
- 每个目标文件有链接器捆绑在一起,形成一个单一而完整的可执行程序
- 链接器同时也会引入标准C函数中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将器所需要的函数也链接到其中
- 每一个源文件会单独被编译器处理为目标文件
预处理(预编译)过程
1.#include头文件的包含
2.#define定义符号的替换和删除
3.注释的删除
- 预处理(预编译)过程属于文本操作过程
编译过程
将C语言代码翻译成汇编代码
- 语法分析
- 词法分析
- 语义分析
- 符号分析
汇编过程
1.将汇编代码翻译成二进制指令(存放目标文件)
2.形成符号表
链接过程
1.合并段表
2.符号表的合并和符号表的重定义
【注意】把gcc移植到windows环境,编译产生的程序想在windows上运行,就得按照windows的可执行程序的格式进行
运行环境
在程序执行的过程:
1.程序必须载入内存中,在有操作系统的环境中:一般由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存完成
2.程序的执行便开始。接着便调用main函数
3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4.终止程序。正常终止main函数;也有可能是意外终止。
预处理详解
预处理符号
int main(void)
{
printf("%s\n",__FILE__);
printf("%s\n",__LINE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
printf("%s\n",__STDC__);
printf("%s\n",__FUNCTION__);
return 0;
}
- 这些预定义符号都是语言内置
预处理指令
1.#define
2.#include
3.#pragma
4.#error
5.#line
…
#define
#define定义标识符
#define name staff
- define 定义一个数字
#define MAX 100
#define min 5
- define 创建一个新名字
#define reg register
- define 使用另外一个符号替换实现
#define do_forever for(;;)
注意:一般使用define定义标识符时,不使用;
#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者定义宏
- 宏的声明方式:
#define name(parament-list) stuff
其中的parament-list是一个由逗号隔开的符号表,可能出现在stuff中。
【注意】参数列表的左括号必须与name紧邻,如果俩种之间由任何空白存在,参数列表就会被解释为stuff的一部分
- 宏可以看作是一种替换
#define ADD(x,y) x+y
int main(void)
{
int a = 10;
int b = 20;
printf("%d",ADD(a,b));
return 0;
}
【注意】所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#与##
#的使用
将宏的参数变成对应的字符串
#define PRINTF(format,value) printf("the value of "#value"is"format"\n",value)
int main(void)
{
int i = 10;
PRINT("%d",i);
return 0;
}
##的使用
可以把位于它俩边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符
#define F(x,y) x##E##y
#include<stdio.h>
int main(void)
{
printf("%f",F(2,-5));
return 0;
}
带有副作用的宏参数
#define ADD(x,y) x+y
int main(void)
{
int b = ++a;
printf("%d",ADD(b,b));
return 0;
}
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
宏与函数的对比
宏的优势
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
函数的优势
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的
- 宏由于类型无关,也就不够严谨
- 宏可能会带来运算符优先级的问题,导致程序出错
属 性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销, 所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
宏与函数的命名约定
- 宏的名字全部大写
- 函数名的每个英文单词首字母大写
undef
#undef用于移除一个宏定义
#define ADD(x,y) x+y
#undef ADD
命令行定义
在命令行中定义符号。用于启动编译过程。
条件编译
在编译一个程序的时候如果一条语句(一组语句)编译或者放弃是很方便的
- 这种情况适合于:调试型代码,删除可惜,保留多余
#if 常量表达式
//...
#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
#endif