马行无力皆因瘦,人不风流只为贫
作者:Mylvzi
文章主要内容:程序环境和预处理
前言:
c程序的实现并不是简单的在vs上写好代码再调试执行的过程,vs是一种集成开发环境,简单来说,集成开发环境就是一个智能软件,它帮助人们进行代码的编写,执行,但背后具体的细节往往被忽略,了解这些细节有助于我们加深对计算机底层原理的认知,也能提高我们的代码理解能力
任何一个ANSI C(标准C语言)的实现都需要经过两个“环境”-->翻译环境和执行环境
翻译环境:将所写代码(文本信息)转变为计算机语言(机械指令,二进制语言)形成目标文件并与链接库链接(给的任务看不懂,翻译为机器能看懂的语言)
-->将中文翻译成英文,并给你一些注释(链接库)
执行环境:执行计算机指令(得到任务,开始干活)
编译:将文本信息翻译为计算机语言
编译分为三个过程:
1.预编译(预处理)-->文本操作,处理预处理指令(头文件包含,#define的替换,注释删除)
#include #define的替换都叫做预处理指令
2.编译-->(将C语言代码翻译为汇编指令)
3.汇编-->(将汇编指令转变为机械指令)
在LINUX环境中,利用gcc编译器查看编译过程的每一步:
LINUX:目标文件和可执行程序是通过ELF这种文件格式来存放计算机指令
readelf:读取elf的文件格式:
test.o的这种文件是按照段的形式存储的,他的信息被一个一个段划分存储
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
预编译阶段会处理所有的预处理指令,预处理指令有很多,比如:头文件的包含,#define的替换等等,下面将详细讲解预编译过程中都会处理那些预处理指令
预定义符号:是语言内置的,需要使用的时候就直接打印就行(就是为了方便)
在预处理阶段会直接被替换为相应的文本
//#define定义的标识符
#define M 100
#define REG register
#define FOR 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__ )
注意:在使用#define定义的标识符时,不要在末尾添加“ ;”,否则可能出现两个分号的情况
宏:就是一种替换文本的工具,和#define定义的标识符作用类似
注意:
1.成员列表(parament-list)的左括号必须与name相连
2.stuff的每个参数都必须单独使用()括起来,且整体也要使用()括起来,避免优先级的错误
//宏
#define SQUARE(x) ((x)*(x))
int main()
{
int a = SQUARE(5);
printf("%d\n", a);//输出5*5=25
return 0;
}
#define SQUARE(x) x*x //不添加括号,存在优先级的错误
//宏只是一种“形式”上的替换,他根本不会计算表达式的值
int main()
{
int a = SQUARE(3+2);
printf("%d\n", a);//3+2*3+2=11
return 0;
}
#define DOUBLE(x) (x) + (x)
int main()
{
int a = 5;//会输出100吗
printf("%d\n", 10 * DOUBLE(a));//10*(5)+5 == 55
return 0;
}
#define 替换规则 在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意:#define定义过程中可以嵌套,但定义的宏无法递归使用
如果定义的标识符常量在字符串中出现,在预处理阶段并不会被替换
//我希望在字符串中添加宏参数,并不能直接写一个参数,因为在预处理时,字符串的内容并不会被扫描
//解决方法:就是让参数以字符串的形式出现-->在参数前添加#
#define PRINT(n, format) printf("the value of " #n " is " format "\n", n)
//format不需要再单独添加“”,因为传递的参数自带“”
int main()
{
int a = 20;
//printf("the value of a is %d\n", a);
PRINT(a, "%d");
int b = 15;
//printf("the value of b is %d\n", b);
PRINT(b, "%d");
float f = 4.5f;
//printf("the value of f is %f\n", f);
PRINT(f, "%f");
return 0;
}
#define CAT(x,y) x##y
int main()
{
int ab = 10;
printf("%d\n", CAT(a,b));//10
printf("%d\n", ab);//10
return 0;
}
注意:连接后的标识符必须是一个合法的,可以被识别的标识符,否则就无意义
如果参数具有“副作用”,可能会出现意想不到的结果,要避免宏的参数具有副作用
//带副作用的宏参数
int main()
{
int a = 10;
//int b = a+1;//b=11, a=10 无副作用
//int b = ++a;//b=11, a=11 具有副作用
return 0;
}
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);//带有副作用的参数
//替换:((5++) > (8++) ? (5++) : (8++))
// 先使用值,5<8 --> (8++) -->所以z=8+1=9
//((5++) > (8++) 值使用完之后在进行副作用,x=x+1=6,y=y+1=9
//最后的(8++)使用完后,执行y=y+1=9+1=10
//x=6,y=10,z=9
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
6 10 9
return 0;
}
宏是小聪明,好看,好用,方便,但是不严谨,只能适用于简单的工作
函数是大智慧,使用不方便(耗时长),消耗的资源多,但是严谨,适用于大型工作
1.宏无法调试,而函数可以调试,可以发现计算过程中的错误
2.宏是形式上的替换,在预编译阶段直接替换,会增加源代码长度,如果大量使用,会导致代码长度过长;而函数是随用随找,不会增加代码长度(使用就进入)
3.宏不需要声明类型,无类型限制(不太严谨),函数有严格的类型限制
4.宏在传递参数时不会计算结果,可能出现副作用;而函数在传递时,会先计算值,不会有歧义
5.宏的参数可以出现类型,但函数不能(把类型当作参数)
6.宏的参数必须严格添加(),避免优先级的错误!
7.宏的执行速度更快,函数因为函数调用,函数栈帧开辟以及函数返回所需额外时间更多,所以执行效率较低(但如果是大型计算过程,相较于计算过程所需的时间,函数额外使用的时间可以忽略不计)
8.宏无法递归,函数可以递归使用
9.宏名一般全部大写,函数名一般首字母大写
//宏的参数可以是类型
#define MALLOC(num,type) (type*)malloc(sizeof(type)*num)
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);//我觉得这样开辟太麻烦,使用宏替换
int* p = MALLOC(10,int);//这样就实现了相同的功能
//宏的参数可以出现类型,而函数不能
return 0;
}
内联函数: 集合宏的优点(计算速度快)和函数的优点(方便调试)的一种特殊类型的函数
1.可以被调试,可以观察每一步计算的具体过程
2.不需要进行函数栈帧的开辟,函数返回,节省时间
3.内联函数还是由编译器处理的,宏是在预处理阶段进行的形式替换
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
#undef用于去除已有的宏定义
格式:#undef NAME
在预编译过程中执行
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个 程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器 内存大些,我们需要一个数组能够大些。)
所谓的条件编译就是在不同条件下编译不同的代码,既不用删除代码,也不需要编写多份代码
#include指令可以被另一个文件编译,会被编译的结果替换
替换方式很简单:
1.#include出现在哪里,就在哪里替换
2.出现一次,就替换一次;出现十次,就替换十次(相当于被编译了10次)
C语言中的头文件包含有两种方式:
1.《 》包含 《》包含一般应用于库文件的包含,如:#include
2.“”包含 " "包含一般应用于程序员本身头文件的包含,如:#include "test.h"
注意:“包含”其实是一种搜寻的过程,即检查路径之下是否包含被编译的文件;
“”的搜寻路径有两种,会现在源文件所在目录下搜寻,如果未找到,去库文件路径下搜寻;而《》的搜寻路径只有库文件路径
使用《》包含库文件,效率更高!
#include的替换方式是出现一次,就替换一次,就编译一次;在使用的过程中可能会出现嵌套文件的包含,那么同一文件有可能在预处理的过程中被编译多次,大大增加代码量,要解决这个问题,有两种解决方式(通过条件编译解决)
#error 是一种在程序中提醒和报错的工具
#pragma
#line
... ... ...
比如:#pragma pack(8) 修改默认对齐数为8
本文主要讲解了c程序实现过程中的两个环境:翻译环境和执行环境,具体讲解了翻译环境中的编译和链接的过程;又详细的讲解了编译过程中的预编译过程;尽管如今的集成开发环境已经如此便利,我们还是要去学习背后的基本逻辑与代码,这样才能加深我们对代码的理解能力
推荐书籍:
1.《程序员的自我修养》(强烈推荐)
2.《高质量c/c++编程指南》
3.《编译原理》
4.《C语言深度剖析》