1,编译环境
首先,要说明的是,计算机只能识别二进制指令,在编写程序时,无论是何种语言,系统根本就识别不了,要想让机器来识别我们用的语言,就必须要通过编译器来进行编译,形成二进制指令的目标文件(.obj)。
在源程序中,通常有很多个源文件,在一个源程序的所有源文件中,当程序运行时,组成一个程序的每个源文件都要通过编译过程分别转换成可识别的目标文件,然后系统才会将这些目标文件进行下一步处理。所以,这也就是为什么在一个源程序中不能运行两个不同的主程序,这样可能会出现系统崩溃。
接下来我们来详细研究一下编译环境。编译环境也细分为编译和链接,编译又可细分为预编译,编译,汇编。
预编译:预编译也叫预处理。在预编译中,系统会将代码的注释删除,宏的符号进行替换,以头文件的包含所运用。在文本操作中,所有的预处理指令都是在预处理阶段处理的。
编译:在预处理完毕后就进入了编译环节,在编译环节中,系统会把C代码翻译成汇编指令,即形成汇编代码,在此环节中,会一系列进行语法分析,词法分析,语义分析,符号汇总等。
汇编:编译过后就进入了汇编环节,在汇编中,系统根据汇编指令将会形成二进制的目标文件,在这些目标文件中都是一些人为看不懂的二进制指令。除此之外,还会形成以一个符号表,在符号表中存放了一些汇总的符号和相关的地址。
链接:在链接中,系统会合并段表,并进行符号表的合并和重定位。
具体形式如下图所示:
2,执行环境
在编译结束后,系统已经形成了一系列的二进制的目标文件。在执行环境中,系统执行的就是这些目标文件0,每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序,在此过程中,链接器同时也会引入标准C(ANSI C)函数库中任何被该程序所用的函数,而且它可以搜索程序员个人程序库,将其需要的函数也链接到程序中。
程序的执行与编译导图
预处理是系统首要处理代码的环节,系统内置的符号是预处理典型处理的符号。
首先,我们先了解下预定义符号。在C程序中,预定义符号都是语言内置的,而这些预定义符号,在预编译时就进行预处理,其中,预定义符号有以下几种:
__FILE__:用来显示进行编译源文件的来源
__LINE__:显示文件当前的行数
__DATE__:显示文件被编译的日期
__TIME__:显示文件被编译的时间
__STDC__:此符号遵循ANSI C(标准C),如若当前编译器也遵循ANSI C(标准C),其值为1,否则此符号未定义,将会报错。
具体例子代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
fprintf(stdout, "%s\n", __FILE__);//__FILE__代表被编译环境的名字
fprintf(stdout, "%d\n", __LINE__);//__LINE__代表此时的行数
fprintf(stdout, "%s\n", __TIME__);//此时运行的时间
fprintf(stdout, "%s\n", __DATE__);//此时的日期
//fprintf(stdout, "%s\n", __STDC__);输出错误
//因为在VS中是不支持ANSI C,即标准C,任何编译器都要在此基础上扩充。若支持,则值为1,否则未定义
return 0;
}
运行图:
1,#define的标识符
在#define的运用中,最简单的用途就是用#define来定义标识符,形式为#define x y。经#define定义后,如同换了个名字,即把x的名字改为了y。两者之间的作用效果还是一样的。代码如下:
#define name 5
#define type int
2,#define的宏定义
#define除了用来标识,还可以定义用来定义宏。在#define机制中,允许把参数替换到文本中,这种类似于函数的实现被称为宏或定义宏,而预处理过程中,宏的定义将会被程序替换,但在替换过程也存在优先级,形式代码和注意要素如下:
#include
//定义一个数的平方宏
#define num(x) x*x
#define arr(x) (x)*(x)
int main()
{
int a = 3;
int r = num(a + 2);
int t = arr(a + 2);
//经过预编译后r=a+2*a+2=3+2*3+2=11
fprintf(stdout, "%d\n", r);//输出11
//因为加上了括号有了优先级,即经过预编译后t=(a+2)*(a+2)=5*5=25
fprintf(stdout, "%d\n", t);//输出25
return 0;
}
宏的代码替换如图:
3,’#‘和’##‘的宏作用
在宏的使用过程中,还可以使用‘#’和‘##’的运用,‘#’用来进行字符或字符串的参数替换,‘##’用来进行参数合并,而在宏的输出中,用format可以进行任意类型的输出,具体形式代码如下:
#include
//在其中用"#n"将会把字符串参数进行替换
#define num(n) fprintf(stdout,"Hello "#n" World %d\n", n)
//在宏定义参数后面加上format后可输出任意类型,具体格式如下
#define arr(n,format) fprintf(stdout,"Hello "#n" World "format"\n", n)//"format"会将其值代入
int main()
{
int abc = 10, bcd = 20;
float pp = 1.56f;
int pa = 15;
num(abc);//用"#参数"将会替换字符串,输出Hello abc World 10
num(bcd);//用"#参数"将会替换字符串,输出Hello bcd World 20
//用format形式将会输入任何类型的参数
arr(pp, "%f");
arr(pa, "%d");
return 0;
}
在运行‘##’代码之前,我们要先明白,宏的替换中若是替换成了类型变量,则在输出的时候会以变量类型的形式输出,请看以下例子:
#include
#define m a
int main()
{
int a = 5;
fprintf(stdout, "%d", m);//输出5,因为m变成了a,而a是变量,值为5
return 0;
}
‘##’的合并就是根据以上为基础的,’##‘是将宏的参数进行合并在一起,代码形式如下,
#include
//##可以将两个数进行合并
#define CTA(a,b) a##b//相当于#define CTA(a,b) ab
int main()
{
int ab = 2023;
//在##的作用宏中,a##b相当于ab,##是合成符,将分离的片段合成一个片段
fprintf(stdout, "%d\n", CTA(a, b));//输出2023
fprintf(stdout, "%d\n", ab);
return 0;
}
4,宏参数的副作用
宏在使用过程中也要进行传参,然而,在这里需提醒的是如若在传参的过程中参数会发生变化,就会形成副作用的变化,即宏数值逻辑的变化,代码例子如下。
#include
#define max(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 5, b = 6, c = max(a++,b++);
//在这里,c先被预处理替换成c=((a++)>(b++)?(a++):(b++))
//前缀++,在表达式中先给值再++,所有经过c后a=6,b=8(b经过了两次++)
fprintf(stdout, "%d %d %d\n", a, b, c);//输出6 8 7
return 0;
}
以上代码在宏传参的时候参数就在不断发生数值变化,形成了宏的副作用。
5,#undef的移除宏的操作
在宏定义后,如若不想在使用某个宏,我们可以使用#undef来进行移除操作,用#undef移除后的宏就等于直接被删除了,如若在使用被此宏将会报错未定义,代码如下:
#include
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 10, b = 20;
int c = MAX(a, b);
fprintf(stdout, "%d", c);
#undef MAX//将宏全部移除,没有此宏了
int d = MAX(a, c);//会报错,因为宏已经被移除了
fprintf(stdout, "%d", d);
return 0;
}
6,宏于函数的对比
经过宏的几个运用我们大概明白,宏与函数有着相似的作用,但两者之间却各有千秋。当执行简单的运算时,我们通常使用宏来完成,因为用于调用函数和从函数返回的代码可能比实际执行这个小类型计算工作所需要的时间更多,在小的运算中宏比函数在程序的规模和速度方面更胜一筹。除此之外,函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之,宏可以适用于任何类型。
然而,宏也有自身的缺点,宏的缺点如下:
1,每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。
2,宏是没办法调试,每次出错的时候必须人为观察。
3,宏由于类型无关,也就不够严谨。
在编译一个程序的时候我们如果将一条语句(即一组语句)编译或者放弃是很方便的。因为我们有条件编译指令,这个指令的编译相当于就是条件编译,而条件编译在预编译时会被处理。
有关条件编译的指令#if,#elif,#else,#endif,#if defined等。
1,条件语句的判断
用来条件语句的判断有四种:#if,#elif,#else,#endif。
#if语句于条件判断语句if相似,如若#if判断的语句值为0,则后面的代码将会在预编译过程被系统删除,如若值为1则执行后面的代码。
#elif于条件语句中else if条件语句详细,它是根据#if后面的又一次判断
#else与条件语句else作用也是相同的,在条件编译不满足时执行。
#endif是条件编译指令的结束标志,#endif是与#if或#elif共同使用的,倘若出现#if或#elif就必须有#endif,否则系统会报错。
具体代码如下:
#include
#define M 0
int main()
{
//当条件成立,即值为1时,执行下面的语句,若不成立,在预处理时代码就被删除了
#if M == 1
//跟if条件语句不同的是,if语句每次执行时都有此代码,#if中是直接把代码删除
fprintf(stdout, "%d", M);
#elif M == 2//多种条件编译的判断
fprintf(stdout, "Hello World");
#else//跟条件语句中的else作用同理
fprintf(stdout, "兔子\n");
#endif//终止#if的判断指令,在#endif后的语句仍可执行
fprintf(stdout, "%d", 5);
return 0;
}
2,条件宏的判断
通常使用#if defined的形式来判断宏。#if defined的作用是条件判断是否定义宏,与#if不同的是当用#if defined判断的宏存在时才执行后面的代码,当不存在时系统在预编译时将会自动删除后面的代码。具体例子如下:
#include
#define MAX 5
int main()
{
#if defined(MAX)//如若存在MAX这个宏,则下面的代码执行,否则在预处理时删除以下代码
//其中#if defined(MAX)也可写成#ifdef MAX,两者之间效果一样
fprintf(stdout, "%d", MAX);
#endif
return 0;
}
3,条件编译的嵌套使用
条件编译与条件语句存在类似功能,条件编译跟条件语句一样,也存在嵌套的使用。嵌套的使用在平常的代码中其实也常见,在系统运维或跨平台使用时,经常多次使用这些条件编译的嵌套使用来实现预想的目的。有关嵌套的使用代码如下:
#include
int main()
{
//在多次嵌套的使用在跨平台时的使用较多,在跨平台时会大量的使用宏的嵌套
#if defined(WIN)
#ifdef LIUNX
Wliunx();
#endif
#ifdef UNIX
opper();
#endif
#elif defined(OPP)
#ifdef NUIO
liunx();
#endif
#endif
return 0;
}//在用#if和#endif条件预处理时相当于注释,在预编译过程中判断是否直接将其删除