编译和链接

        C语言由源代码生成可执行文件的过程如下:C源程序——>编译预处理——>编译——>优化程序——>汇编程序——>链接程序——>可执行文件。其中,通常把编译预处理,编译,优化程序,汇编程序这几个阶段统称位编译阶段,而把链接程序生成的可执行文件这一阶段称为链接阶段。

上面这幅图大概就是编译和链接的原理及过程,大家可以好好看看。

        编译的预处理阶段的任务是:读取C源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。比较熟悉的就是我们经常使用的宏,它也是从这开始的,即在正式编译之前,先扫描源代码,对其进行初步的替换。另外,当我们进行注释或者代码中有许许多多的空格的时候,有没有发现对我们的程序其实并没有影响,其实这是因为在编译的时候编译器自动将他们过滤删除了,只留下了有用的代码。

常见的编译预处理指令主要有4类:包含头文件,宏定义及宏展开,条件编译,特殊符号处理。

下面我们就来简单讲讲一些常见的编译预处理指令。

包含头文件

        每当我们想要调用一些函数的时候,总要包含头文件,我们从刚刚学习C语言就认识的printf()和scanf()函数,他们存在于头文件<stdio.h>中,如果不包含它,就无法使用上面两个函数。

        其中头文件的包含方式有两种:

#include <xxx.h>

#include "xxx.h"

首先用<>括号括起来的告诉我们的是编译器自带的头文件或者是外部的头文件;而""双引号括起来的是我们自己创建的头文件。有同学就会问:用不同的符号包含它们有什么好处吗?其实当我们编译器在编译的时候,就会将头文件中所包含的内容全部展开,它需要找到头文件的位置,加上不同的符号,就等于告诉了编译器去哪里寻找,采用两种不同的包含格式使得编译器能够在很多头文件中区别他们的不同,加快程序的效率。如果不这样的话,编译器就会遍历全部的头文件,这就减慢了效率。

宏定义和宏展开

        为了程序的方便,我们在写程序的时候,会定义宏,宏定义了一个代表特定字符串内容的标识符(为了好区分宏定义,我们一般都使用大写来定义宏)。预处理过程会把源代码中出现的宏标识全部都替换成宏后面的内容。宏最常见的用法就是代表某个值,比如当我们想要定义圆周率π的是后,我们及可以使用宏定义#define pi 3.141592。因为π比较长,在写程序时容易写错,而且还可以保证以后的修改。

        宏还可以定义带参数的宏(宏函数),这样的宏可以像函数一样被调用,但他是在调用语句处展开宏,因为我们函数一般都写在main函数前边,而宏定义也一样,当程序编译的时候,会将宏展开,相当于在前面就定义了一个函数。需要特别注意的是,宏定义与宏展开预处理仅是简单的字符串替换,并不进行类型检查和语法出错检测,我们应该保存使用宏时在语法环境中的正确性。

        我们可以尝试着用宏函数写一个加法函数。

#define	GENERIC_MAX(type)			\
		type type##_MAX(type a,type b)  \
		{						   \
			return a>b?a:b;		   \
		}

GENERIC_MAX(int);
//int int_MAX(int a.int b)		//展开后的样子
//{	
//	return a > b ? a : b;
//}

GENERIC_MAX(double);
//double double_MAX(double a.double b)		//展开后的样子
//{
//	return a > b ? a : b;
//}

int main()
{	
	int a = 1;
	int b = 2;
	double c = 1.333;
	double d = 3.444;
	int ret = int_MAX(a, b);
	printf("%d\n", ret);
	double sum = double_MAX(c, d);
	printf("%ff\n", sum);

	return 0;
}

        我们将宏展开,就相当于我们定义的函数,你可以尝试自己展开,宏定义时加的 \ 表示转到想下一行。因为函数严格定义了返回值的类型和形参的类型。假如我们要同时进行整形的加法和浮点型的加法的话我们就必须定义两个加法函数,而当我们使用宏函数的时候,我们只需要定义一个函数,然后修改它所带的参数及行,我们需要什么类型的函数,我们就在给type定义什么类型,当他展开的时候,就是函数。

        我们还需要注意的是宏定义只是简单的将我们main函数中有该宏定义的位置替换而已,所以我们需要严格的检测宏定义回出现的错误。

#define A 2+2
#define B 3+3
#define C A*B
int main()
{
	printf("%d\n", C);
  return 0;
}

        当我们看到上面这幅代码时,你肯定认为C的值是24对不对。其实它的值为11。我们将A,B带到宏C中,它展开的方式就是2+2*3+3。得到的结果就是11。怎样解决这个方法呢? 我们只需要在定义宏C的时候将它定义为这样的形式,#define C (A)*(B),添加括号改变运算符的优先级,就能很好的解决这些问题,所谓说当我们在使用宏写某些简单的函数的时候,一定不要吝啬括号的使用。

带参数的宏和函数的区别:一是宏只是在编译前进行简单的字符串替换,并不进行相应的类型检查之类的语法检查,所以说很容易出现我们上面所讲的错误,而函数定义的时候会在编译的时候进行检查,现在的编译器带我们写代码的时候就已经报出很多语法上的错误了。

二是宏并不想函数一样存在给形参分配存储空间,完成实参向形参的传递,流程控制转移以及函数返回值,这样的宏调用比函数调用的执行效率高,使用函数的时候,还要去栈上开辟空间,效率较低。但由于带参数的宏很容易发生副作用,所以并不推荐大家使用带参数的宏。

"#"运算符

        出现在宏定义中的"#"运算符把跟在其后的参数转换成一个字符串。有时把这种用法的"#"称为字符串化运算符。例如:

#define SAYHTLLO(x) "hello,"#x

#undef运算符

        用#define定义的宏的正常作用范围为定义出到源程序结束,但可以用#undef来取消前面定义过的宏定义,从这开始后面出现的宏定义将不再被替换。

可以看到程序出现了报错,HI是未定义标识符。我们将后面两串代码删掉或注释掉就可以编译运行。

条件编译

#if指令检测后面的常量表达式。如果表达式为真,则编译后面的代码,直到出现#else,#leif或#endif位置,否则就不编译。我们可以用这个方法来注释我们所写的代码。

可以看到,当我们在程序前面使用#if指令时,并将表达式置为0,它其实就显得比较暗,无法运行。如果将0置为1 则就可以正常运行。

#endif 用于终止#if预处理指令

#else指令用于在某个#if指令之后,如果#if条件部位真时,就编译#else后面的代码。这其实和我们在条件判断语句里的if语句使用差不多。

#elif预处理指令综合了#else#if指令的作用,用于嵌套条件判断。

#ifdef 指令用于检测后面跟的宏是否有定义,如果有定义,则编译后面的代码,否则不编译。

#ifdef和#ifndef

这二者主要用于防止重复包含头文件。一般在XXX.h的头文件中前面有这么一段:

#ifndef  _xxx_h_                //如果没有定义该文件

#define _xxx_h_                //就定义该文件

#endif                

我们一般在头文件前面都会看到#pragma once 表示该头文件只被包含一次的意思。

特殊符号的处理

        编译预处理程序可以识别一些特殊符号,并对在源程序中出现的这些符号将用合适的值进行替换,从而实现某种程度上的编译控制。常见的定义好的供编译预处理程序识别和处理的特殊符号有如下几个。

__FILE__:包含当前文件名的字符串。
__LINE__:表示当前行号。

__DATE__:包含当前日期的字符串。

__TIME__:包含当前时间的字符串。

下面我们就来试着用一下。

可以看到文件的相关信息。

链接

        从开始的那副图中可以看到,通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。

以上就是关于编译和链接的一些知识,如果哪里讲的不对希望大家能够指出,大家一起学习进步,如果该文章对你有所帮助的话,那将是我最大的荣幸。还请大家点点赞+收藏+关注。

  • 32
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值