前言
我们已经写了这么多的代码,那我们是不是应该了解一下代码是运行的呢?
1. 翻译环境和运行环境
翻译环境将源代码转换为二进制指令。
运行环境用于执行实际代码
2. 翻译环境
翻译环境主要由编译和链接两个大过程组成,而编译又可以分解成:预处理/预编译,编译,汇编三个过程。
2.1 预编译
预编译阶段,源文件和头文件被处理成为以.i为后缀的文件。
该阶段主要处理文件中以#开始的预编译指令,例如#include,#define,处理规则:
-
将所有的#define删除,展开所以的宏定义。
-
处理所有的条件编译指令,如#if、#endif、#ifdef 、#elif、#else。
-
删除所有注释
-
处理#include指令,将所包含的头文件的内容插入到预编指令位置,这个过程递归,即头文件可能包含其他文件。
-
添加行号和文件号标识,方便后续编译器生成调试信息等。
-
保留所有的#pragma的编译指令,编译器后续会使用。
2.1.1 宏定义
2.1.1.1 #define定义常量
基本语法:
cs
#define name stuff
举例:
cs
#define M 100
#define A 50
#define CASE break;case
#define DEBUG_PRINT printf("file:%S\tline:%d\t\
date:%s\ttime:%s\n",\
__FILE__,__LINE__,\
__DATE__,__TIME__)
当定义的stuff过长,可以分成几行写,除了最后一行外,每行后面加一个反斜杠(续行符)。
通过第三行的代码可以以抽象的方式实现switch-case语句
cs
int main()
{
int n =0;
scanf("%d ",&n);
switch(n)
{
case :1;
CASE :2;
CASE :3;
//.......
}
}
思考:在define定义标识符的时候,要不要在最后加上;?
有些情况不能加;,有些情况可以加,建议是不加。例如下面的情况不能加
比如:
cs
#define MAX 10000
#define MAX 10000;
cs
if (condition)
max =MAX;
else
max = 0;
当替换之后,会出现两个分号,当没有大括号时,if后面只允许有一个语句,就会报错。
2.1.1.2 #define定义宏
申明方式:
cpp
#define name(parament-list) stuff
其中parament-list是由逗哈隔开的符号表,他们可能出现在stuff中。
注意:左括号必须和name紧密相连,否则参数列表会被解释为stuff中的一部分。
宏定义举例:
cs
#define SQUARE(X) X*X
当使用SQUARE(5) 时,预处理器将用 5*5替换SQUARE(5)。
警告:
上面的其实是有问题的,例如当遇到下面情况时:
cs
int a = 5;
printf("%d ",SQUARE(a+1));
输出的结果不是36,而是11。
计算过程:5+1*5+1 = 11.
原因:宏定义代换时不会进行运算操作,只会进行简单代换。
如何修改正确呢?
cs
#define SQUARE(x) ((x)*(x))
总结:当宏定义的时候尽量将每个参数都进行加括号操作,避免造成不可预估的结果。
2.1.1.3 带有副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,那么使用的时候可能出现危险,导致不可预测的结果。
例如:
cs
x+1;//不带有副作用
x++;//带有副作用
证明:
cs
#define MAX(a,b) ((a)>(b)?(a):(b))
x = 5;
y = 8;
Z = MAX(x++,y++);
预处理后:
cs
#define MAX(a,b) ((a)>(b)?(a):(b))
Z = ((x++)>(y++)?(x++):(y++));
进行运算后:x = 6, y = 10,z = 9。
2.1.1.4 宏替换规则
-
在调用宏时,首先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
-
替换文本随后被插⼊到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
-
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:对于宏不能出现递归。
2.1.1.5 宏与函数的对比
宏通常被用于执行简单的运算。
如在找较大数时,写成宏更有优势。
原因:
-
对于小型计算,宏不需要要调用函数栈帧等操作,节约时间。所以宏比函数在程序的规模和速度方面更胜一筹。
-
宏的参数是类型无关的,什么类型都可以。
对比与宏的劣势:
-
每次使用宏时,都需要插入代码,除非宏比较短,否则可能大幅度增加程序长度。
-
宏没有办法调试。
-
宏与类型无关,不够严谨。
-
宏会带来运算符优先级的问题,导致容易出错。
宏的参数可以出现类型,但是函数不可以。
例如:
cs
#define MALLOC(num,type) (type*) malloc (num * sizeof(type))
运用:
cs
MALLOC(10,int);
预处理后:
cs
(int *)malloc(10*sizeof(int));
宏和函数的对比
2.1.1.6 #和##
1. #运算符
当我们想写函数作为打印下列代码时,普通函数可能不是很好实现。因为这里面的n是在字符串里面的,带入参数时不好修改,而且如果打印时是其他参数呢?如%f等。那我们这里就可以运用宏来实现。
cs
printf("the value of n is %d",n);
cs
#define PRINT(format,n) printf("the value of ""#n" "is"format,n);
这里的format是数据的类型,如果传的是%f,就会输出浮点数类型。%d就是整型类型。
这里的#n用来输出参数n的名称,而不是参数n的数据。
#执行的操作就是字符串化。
2. ##运算符
##运算符的作用是将两边的符号合成一个符号。##被称为记号粘合。
当求两个数较大数时,一个函数不能应用于多种类型,所以我们需要写不同的函数,那我们这里就可以用宏来实现。
cs
#define GENERATE_MAX(type)\
type type##_max(type x,type y)\
{
return (x>y?x:y);
}
运用:
cs
GENERATE_MAX(int)
GENERATE_MAX(float)
int main()
{
int m = int_max(2,3);
printf("%d\n",m);
return 0;
}
2.1.1.7 命名约定
我们通常将宏全部大写,函数名不全部大写。
2.1.1.8 #undef
#undef用于移除宏定义
格式:
cs
#undef name
2.1.1.9 条件编译
当编译时,我们可以通过条件编译指令进行编译或放弃编译。
常见条件编译指令:
cs
#if
//。。。。
#endif
如:
#define __DEBUG__ 1
#if __DEBUG__
//......
#endif
- 多分支条件编译
cs
#if
#elif
#else
#endif
- 判断是否被定义
cs
#if defined(symbol//写法1
#ifdef symbol//写法2
#if !defined(symbol)
#ifndef symbol
2.1.1.10 头文件包含
1. 本地文件包含
cs
#include"filename"
先在源文件目录查找,再到标准位置查找。
2. 库文件包含
cs
#include<filename.h>
到标准位置查找。
总结:查找文件都可以用""方式查找,但这样效率更低,且不容易区分是库文件还是本地文件。
3. 嵌套文件包含
当包含了多次同一文件时,为了避免造成资源的浪费,我们通过条件编译解决该问题。
方法1:
cs
#ifndef __TEST_H__
#define __TEST_H__
//....
//endif
分析:先判断是否已经被定义,没被定义再定义,定义了则跳过。
方法2:
cs
#pragma once
分析:这个表示只能被引入一次。
2.2 编译
将预处理后的文件进行一系列的:词法分析,语法分析,语义分析及优化,生成汇编代码。
2.3 汇编
将汇编代码转换为二进制代码。
2.4 链接
链接是一个复杂的过程,链接的时候需要将一堆文件链接在一起才生成可执行程序。
链接解决的是一个项目中多文件多模块之间相互调用的问题。