在ANSIC的任何一种实现中,都会存在两个不同的环境。第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,第2种是执行环境,它用于实际执行代码。如下图所示:
翻译环境会分几个步骤:
由上我们便可以知道翻译环境分成编译器、链接器两个部分分别完成相应的功能,那么编译器这一部分又可以分为预编译(预处理)、编译、汇编这三个部分,如下图:
预编译:预编译过程主要处理的是那些源代码文件中以"#"开始的预编译指令,如#include、#define等,具体有哪些如下:
编译:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析、生成相应的符号汇总及优化后生产相应的汇编代码文件。
汇编:汇编就是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令,以及生成符号表。
符号汇总,符号表: 编译中的符号汇总就是会把源文件当中类似于全局变量,函数名等汇总起来,局部变量不会总因为局部变量只有在程序执行时才会定义,生命周期短。 汇编当中的符号表就是把各原文件当中所汇总的符号整合到一起,把全局变量,以及函数等它们的真正地址都整合起来,一边在链接器链接时找到它们的真正位置。具体如下所示:
链接(链接器):把多个目标文件和连接库进行链接的,主要进行合并段表和符号表的合并与重定位 。
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
所谓预定义就是在我们预处理之前就已经定义好了,可以直接使用,这些能用来干什么呢?一般可以用来我们在写代码的时候用作标记,当工程比较复杂的时候,我们可以在其中穿插这样的代码,类似于写日志,写入文件当中,以便编译时发现其中的错误。如下代码所示:
int main()
{
FILE* pf = fopen("log.txt", "a+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
fprintf(pf, "%s %d %s %s\n ", __FILE__, __LINE__, __DATE__, __TIME__);
}
fclose(pf);
pf = NULL;
return 0;
}
#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__ )
#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。 语法:#define name(parament-list) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。需要注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。看如下代码:
//#define SQUARE( x ) x * x
//#define SQUARE( x ) (x) + (x)
#define SQUARE( x ) ((x) + (x))
int main()
{
printf("%d \n", SQUARE(5));//25
printf("%d \n", SQUARE(2 + 3));//11实际上printf("%d \n", 2+3*2+3)
//改正#define SQUARE( x ) (x) * (x)
//但又会遇到新的问题如下
printf("%d \n", 10*SQUARE(2 + 3));//想要的结果是10*(5+5)=100
//实际上10*5+5=55
//因此#define定义宏括号要尽量加上
return 0;
}
所以需要注意:用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
#define替换规则
#define定义的符号和宏,替换的时候会涉及如下几个步骤:
需要注意:
上面我们知道,字符串常量的话,#define所定义的符号它是不会替换的,那怎么样让它去替换呢?下面我们讲述比较奇妙的知识点,#和##。
#define PRINT(V, FORMAT) printf("the value of "#V" is " FORMAT"\n", V )
int main()
{
//如果我们想要打印the value of a is 0
//the value of b is 1,the value of c is 2
//定义函数的话,其中的abc是不是不能以一个通用的方式去替换
int a = 0;
PRINT(a, "%d");
float b = 1;
PRINT(b, "%f");
return 0;
}
#define VALUE(x, y) x##y
int main()
{
int value111 = 100;
printf("%d\n", VALUE(value, 111));//100
return 0;
}
#的含义就是把宏定义的参数转换成所对应的字符串,之后才插入字符串之中。##就是把两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。需要注意:
带有副作用的宏参数和上述加不加括号所带来的影响是不同的。具体的就是宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么就有可能会出现危险。副作用就是表达式求值的时候出现的永久性效果。例如:
x+1//正常的加法运算没有副作用
x++//这会产生副作用,表达式使用的是x原本的值,但是执行之后x+1了不再是原来的值
产生的副作用看下例:
#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);//结果是不是我们想的6 9 8呢?
//事实上结果是6 10 9
}
事实上的结果与我们所想的大相径庭,这是为什么呢?这就是由于副作用的表达式在宏的定义中出现的不止一次,对最后的结果产生了较大的影响,最终的运算见下图: MAX(x++, y++) ( (x++) > (y++) ? (x++) : (y++) )
一定程度下我们是不是会感觉宏和函数的功能差不多,但是呢它们还是有一定的区别,区别是:
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
int main()
{
MALLOC(10, int);//类型作为参数
//预处理器替换之后:(int*)malloc(10 * sizeof(int));
return 0;
}
当然,有时候优点也是缺点,是一把双刃剑和函数相比它的缺点如下:
这里有一个不成文的规定, 一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者,因此呢C语言程序员的一个习惯就是:把宏名全部大写,函数名不要全部大写。我们也尽量这样做,这样会显得高级!
这个指令就和#define正好相反,前者定义一个符号或宏,后者取消定义,如果在代码中我们想让这个符号重新被定义,就应该先把之前的移除。
什么意思呢?就是说我们可以在命令行中进行定义,应用场景也比较有限,一般就是在一些内存空间比较有限,会根据不同的机器我们去定义一个不同大小的变量,gcc编译环境下就可以实现这一的一个功能。
顾名思义就是满足条件就编译,不满足就不编译,比如一些调试性代码删除可惜,保留又碍事,所以我们可以选择性的编译。具体有以下几种条件编译指令:
//1
#if 0
printf("张三");//真就执行,假就不执行
#endif
return 0;
//2
#if 0
printf("张三");//真就执行,假就不执行
#elif 1
printf("李四");//真就执行,假就不执行,但如果第一条执行了这个即使是真也不执行
#elif 0
printf("王二");//真就执行,假就不执行,但如果前两条执行了这个即使是真也不执行
#endif
//3
#define NAME 0
int main()
{
#ifdef NAME // 等价于#if defined (NAME)
printf("张三");//检测的是否被定义,即使定义的为假也执行
#endif
#ifndef NAME//==#if !defined (NAME)
printf("张三");//检测的是否被定义不定义才执行
#endif
return 0;
}
//4嵌套,类似于判断语句条件编译指令也是支持嵌套的
我们在写C语言代码时常常会写一句#include<stdio.h>,什么意思呢?其实这条语句的意思就是包含头文件 stdio.h,在预处理阶段这条代码就会被替换成我们所包含文件的代码。在前面所介绍的通讯录代码中是不是还有另外一种包含头文件的方式#include"",那种方式我们用来包含我们自己定义的头文件。
所以头文件包含有两种方式:#include<>和#include""。它们的区别就是查找策略不同,如下:
那我们对于库函数下的头文件可不可以用#include""进行包含呢?当然也是可以的,但是它是不是会先在我们的源文件的目录下去查找,再去标准位置去查找,这样效率也就低了。因此我们要按照标准去编写我们的代码,这样我们的代码会显得高级!
通过对预处理的了解,你想没想到过这样的场景呢?
可能我们并没有在意这些,但当我们仔细去观察就会发现,在一些时候我们还是写出了这样的代码的, comm.h和comm.c是公共模块,add.h和add.c使用了公共模块,sub.h和sub.c使用了公共模块,test.h和test.c使用了add模块和sub模块。那我们可想而知在预处理之后,common的代码是不是就在test中出现了两次,一旦这些代码足够的长,那效率就会降下来了。怎么样预防这样问题的出现呢?有两种解决方式:
//第一种是在头文件中写入
#pragma once
//第二种
#ifndef __TEST_H__
#define __TEST_H__
//endif之前是头文件的内容
#endif
//这三条语句的意思就是1.ifndef __TEST_H__不定义就执行
//因此#define __TEST_H__和头文件内容就会插入在程序当中
//当有第二个头文件要插入时遇到了#define __TEST_H__
//因为定义了#ifndef __TEST_H__就不会再执行