程序环境和预处理

1.程序的编译环境和执行环境

在ANSI C(标准C)的任何一种实现中,存在两个不同的环境。

第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第二种是执行环境,它用于实际执行代码。

2.详解编译和链接

我们在一个工程里写的每一个源文件(.c)都要单独经过编译器生成目标文件(.obj),再通过链接器的处理把这些目标文件和链接库合成一个可执行程序(.exe)。

编译又分为三个小的阶段:预编译(预处理),编译,汇编。

预编译

预编译不会修改原来的.c文件,而是会生成一个新的文件(.i),预编译会完成以下的一些操作:

1.头文件的内容全部包含到文件中,头文件的包含在实际中就是复制头文件的内容。

2.会把 #define 定义的符号(标识符和宏)进行替换,并且会把#define的这一行代码删除。

3.会把注释删除……

在预编译期间做的都是一些文本操作

编译

编译之后会生成一个.s文件,编译阶段做的事情就是把C语言代码转换成汇编代码,在这个过程中进行了语法分析,词法分析,符号汇总,语义分析,这里面的符号汇总会把我们写的代码中的全局的符号汇总起来,比如全局变量名,函数名等。

汇编

汇编之后会生成一个 .o 文件(.obj目标文件),汇编期间会对编译生成的汇编代码进行处理,把汇编代码转换成二进制指令,同时会形成一个符号表,上面我们讲到编译期间会汇总符号,在汇编期间会形成符号表,形成符号表的同时会给这些符号关联一个地址,比如函数名和全局变量名。 如果一个函数在这个源文件中只进行了声明,定义写在了其他的文件中,那么在这个文件的符号表中会给函数名分配(关联)一个无效的地址,而在定义的那个文件的符号表中关联一个有效的地址。在汇编期间是不用考虑函数重名的,因为函数如果重名那么在编译期间汇总符号的时候就已经报错了。

链接

汇编生成目标文件后会进行链接操作,而链接期间主要做的是:1.合并段表

                                                                                                     2.符号表的合并和重定位

符号表合并的时候两个相同的符号会进行筛选,选出有意义的地址,合并后的符号表中该符号最终关联的就是这个有效的地址。合并符号表就是为了链接期间能够跨文件找到函数。



3.预处理详解

3.1预定义符号

下面的一些与定义符号是C语言内置的。

__FILE__  //进行编译的源文件
__LINE__  //文件当前的行号
__DATE__  //文件被编译的日期
__TIME__  //文件被编译的时间
__STDC__  //如果编译器村寻ANSI C ,其值为1,否则未定义 

我们可以将这些值打印出来观察。

首先我们可以发现vs编译器中STDC是未定义的,这说明vs不遵循标准C。

我们将其他的值打印出来就能很清楚的知道他们所代表的信息。FILE是指示当前的文件,注意,你打印的FILE在哪个文件,他表示的就是哪个文件,比如在test,c中调用一次打印FILE的函数,则这次调用打印的就是test.c,在main.c中打印的FILE表示的就是main.c。另外三个就是分别表示LINE所在的行号,编译的日期和时间。

3.2#define

#define定义标识符

在讲解枚举的时候我们就详细讲了#define定义的标识符常量和枚举常量的区别,我们在使用这个时唯一要注意的就是在#define定义的这一行代码后面不要加分号,如果加了分号,#define在进行替换的时候就会把这个分号也当作要替换进去的内容的一部分,这是一个非常坑的事情。

#define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常被称为宏或定义宏。

宏的申明方式:

#define name(parament_list) stuff

其中name是宏名,papament_list是一个由逗号隔开的符号表,类似函数的参数,但是没有类型,stuff是宏体,符号表中的符号可能会出现在stuff中。

要注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

其次,宏的参数是替换进去的,不会经过任何的运算,所以在定义宏的时候多加括号逻辑会更加清晰

错误示范


#define MUL(x,y) x*y

int main()
{
	int sum = MUL(2 + 3, 6 + 4);
	printf("%d\n", sum);

	return 0;
}

如果我们注意定义宏的话,那么将参数替换进去的话就是 2+3*6+4,得到的结果就是24,这与我们的预期是不相符的。

而正确而做法是多加括号,在参数部分加上括号,返回的结果也加上括号,这样能避免很多的麻烦

#define MUL(x,y) ((x)*(y))

这样定义的话就能得到5和10的积。

#define 替换规则

1.在调用宏是,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果有,它们首先进行替换。

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

3.卒子后,看看它是否包含任何由#define定义的符号,如果有,就重复上面的过程。

注意:

1.宏的参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不能出现递归。

2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。(字符串中如果出现#define的标识符,它们并不会被当作标识符而被替换,而只是字符串的内容)。 

 #和##的用法

首先我们要知道C语言的printf函数的一个特性,一个printf内,多个字符串会被合并成一个字符串输出。比如下面的代码

	printf("hello world\n");
	printf("he""llo"" ""world""\n");

这两次printf函数的的结果是一样的,因为下面的printf把几个字符串合并成了一个字符串输出。

# 的作用
	int a = 10;
	printf("the value of a = %d\n", a);
	int b = 20;
	printf("the calue of b = %d\n", b);

当我们有时候需要打印楚某个数的值时,为了更清晰的表述清楚符号名和值,我们会用上面这样的方式将其打印出来,但是我们发现a和b都是整型类型的值,那我们能不能写一个函数来实现既打印符号名有打印他的值的功能呢?答案是不能的,函数是实现不了这样的功能的,因为传的参数传的是值,我们无法知道传的实参的符号。但是我们用宏却可以实现这样的功能。

当我们这样定义宏的时候

#define PRINT(N) printf("the value of N is %d\n",N)

打印出来的却是这样的结果

问题就出在了前面的 N 是在字符串中,他不会预处理器搜索,不会被替换。那么我们又想到结合刚刚讲的printf函数的特性,是不是把N写在两个字符串之间就行了呢?于是我们又写出了下面的宏定义

#define PRINT(N) printf("the value of "N" is %d\n",N)

如果想的这么简单,那么编译器马上就会给你泼一盆冷水,有没有想过,如果这样写的话,N就被替换成参数传过来的值了,而不是字符串了,这时候就不再是printf函数的特性了,而会报错。

这时候又会想把N也加上双引号,但是这样的话不就又跟第一种情况一样了吗?

这时候我们就需要一种方法,来使传过来的参数被替换成参数的字符串,而不是参数的值,# 的作用就是这样的。如果在参数N前面加上# , N就会被替换成参数的字符串,而不是参数的值, 那么我们就可以写出下面的代码

#define PRINT(N) printf("the value of "#N" is %d\n",N)

这样定义宏的时候,第一个N由于被#修饰,他会转换为参数对印的字符串。当我们用这个宏来打印,就能够实现我们想要的功能

	int a = 10;
	int b = 20;
	PRINT(a);
	PRINT(b);

## 的作用

## 可以把位于他两边的符号合并成一个符号,它允许宏定义从分离的文本片段创建标识符。

比如当我们想要实现一个宏,传过去两个参数 class 和 1 ,我们想要将他们合并得到一个 class1 的变量名,这样我们就能使用它生成多个班级信息的变量名,或者用这个宏得到的结果来修改班级的信息,这时候我们就可以使用 ## 这样定义宏。


#define CAT(CLASS,NUM) CLASS##NUM
	int i = 0;
	int class1 = 0;
	int class2 = 0;
	int class3 = 0;
	CAT(class, 1) = 10;
	CAT(class, 2) = 20;
	CAT(class, 3) = 30;

注意:这样的连接必须产生一个合法的标识符,否则其结果是未定义的

带有副作用的宏参数

副作用是什么?前面我们学习操作符时,当我们使用++ 或者-- 这样的操作符来对其他变量赋值时,操作符的作用对象自身的值也被修改了,这就可以说是带有副作用。

当我们的宏参数中也带有++ --这样的操作符时,他也会有副作用。比如下面的一个代码

#define MAX(M,N) (((M)>(N))?(M):(N))

int main()
{
	int a = 4;
	int b = 3;
	int c = MAX(a++, b++);
	printf("a=%d,b=%d,c=%d\n",a,b,c );

	return 0;
}

在上面的代码中,我们在宏定义中已经很小心的用括号把所有的可能出问题的地方都括起来了,但是还是防不住参数是个老六。

上面这段代码怎么理解呢?首先进行了替换 c=(((a++)>(b++))?(a++):(b++))。这无非就是一个三目操作符,首先判断条件,(a++)>(b++),因为是后置++,所以会先使用a和b的值,再自加(这里的括号并不影响后置++使用后再自加的特点,因为这里的括号没有对a和b进行操作,只是将括号的多个表达式当成一个表达式的作用),而a的值是4,b的值是3 ,所以表达式为真,判断完之后,a和b都自增,a的值变成了5,b的值变成了4,而三目运算符的鸡国就是a++ ,也就是 c=a++。这时候也是先赋值,a再自增,所以c的值是5,a自增之后变成了6.

 当宏参数在宏的定义中出现不止一次时,如果参数带有副作用,那么你在使用这个宏的时候就可能会有危险,导致不可预测的后果,副作用就是表达式求值的时候出现永久性效果。

所以在设计宏和传参的时候,一定要注意他会不会带来副作用。

宏和函数

看到这里我们也许就会发现宏和函数有很多类似的地方,都有名(宏名、函数名)和定义(宏体、函数体)。当我们以求最大值为例来比较宏和函数时:

宏:

#define MAX(M,N) (((M)>(N))?(M):(N))

函数:

int Max(int x, int y)
{
	return (x > y ? x : y);
}

我们发现,函数的参数和返回值都是有类型限制的,而宏只是完成替换,不会对类型进行检查,上面的函数只适用于整型之间的比较,如果换成其他类型就得重新写一个函数,而上面的宏却无论什么类型都能比较。而且,函数在调用的时候会有一系列的动作来开辟和销毁栈帧,存在系统开销,而宏由于在预处理期间就替换了,不会在执行的时候起作用,而且宏的代码只有一行,而函数却有两行,代码量也是很宏的更少,这样好像看起来宏似乎比函数要更好用?其实不是的,对于这种代码量少的计算宏确实比函数更有优势,但函数的很多优点宏也是不具备的。

宏相较函数的优点:

宏的参数可以出现类型,而函数做不到。比如我们用calloc的参数传给malloc实现malloc。

#define MALLOC(DATATYPE,COUNT) malloc(sizeof(DATATYPE)*count)

DATATYPE是可以由宏的参数指定的,而函数则无法将类型当作参数。

同时,由于宏是替换的,所以他的执行速度是要比函数更快的。

而宏的缺点也很明显:

1.每次使用宏的时候,一份宏代码就插入(替换)到程序中,除非宏非常短,否则可能大幅度增加程序的长度

2.宏是无法调试的,而函数我们能够逐语句进行调试

3.宏由于类型无关,可能不够严谨,所以说宏不检查类型的特性其实是一把双刃剑。

4.宏可能带来运算符优先级的问题,导致程序容易出现错误。

命名约定

我们一般宏名全部大写,而函数名一般每个单词的第一个字母大写或者使用下划线。

#undef

用于移除一个宏定义或者标识符。当我们想要这个标识符或者宏只在代码的一部分有效时,我们就可以在使用完之后用这样的一条指令来移除#define 的定义。

#define MAX 100

int main()
{
	int a = MAX;
#undef MAX
	int b = MAX;

	return 0;
}

命令行定义

在gcc这样的编译器上,我们是可以以命令行的形式在程序运行时对某些变量进行赋值的。

条件编译

条件编译顾名思义,就是有条件的编译,对于某些语句,如果满足条件就留下来进入编译阶段,不满足条件的话就跟注释一样在预处理的时候就删除了。

常见的条件编译指令

1.

#if 常量表达式
/*
要进行条件编译的代码
*/
#endif

首先是最基础的#if 和#endif的条件编译,这就很像我们的条件语句的 if ,如果常量表达式为真,后面的语句就就进入编译,如果为假,就不编译。一定要记住在后面要有 #endif 来划定要条件编译的范围,#if和#endif就跟左括号和右括号一样,是成对出现的,不能缺少其中一个。

例如我们可以用条件编译来定义一个全局变量,如果我们不想要这个变量的时候,就把#define那行代码注释掉。

#define __DEBUG__ 1

#if __DEBUG__
int a = 0;
/*
要进行条件编译的代码
*/
#endif

2.

多个分支的条件编译

#if 常量表达式

…………

#elif 常量表达式

…………

#else

…………

#endif

这个用法也可以参考if else语句来理解,但是一定要注意 最后面要用#endif 来结束条件编译。

3. 判断是否被定义

#if defined (symbol)
…………
#endif

或者

#ifdef symbol
…………
#endif

这两种条件编译的形式是一样的,如果前面定义了symbol,代码就编译,注意后面也要有#endif

4.判断是否未被定义

这一条跟上面的就是逻辑反过来的。

#if !defined (symbol)
…………
#endif

   或者

#ifndef symbol
…………
#endif

如果symbol未被定义就编译。

嵌套指令                

这些条件编译指令也是可以嵌套的,与if语句是非常相似的,要区分的就是每一层嵌套都必须有#endif,要记住#if 和#endif的匹配。

文件包含

#include

#include指令会在预处理期间把这一行代码删除,然后把这个要包含的文件的内容全部复制进来。

当我们重复包含某个头文件时,如果头文件代码量过大,会导致我们的程序的代码量也很大。为了防止头文件被重复包含,我们可以用条件编译指令将整个头文件的内容进行条件编译,来防止重复包含。

#ifndef TEST_H
#define TEST_H

//
//头文件内容
//

#endif

像这样,第一次包含这个头文件时就会定义TEST_H,后面再重复包含的话就不会满足这个条件编译的条件。

我们也可以在头文件最上方写下这样一行代码 

#pragma once

这样也可以避免头文件被重复包含。

在之前我们用到过#include< > 和#include" " 这两种方式去包含头文件,那么这两种有什么区别呢?

两者的查找策略不一样。

如果用< >的话,编译器会直接去库目录底下去查找头文件。

而如果用 " " 的话,查找策略有两步,首先会在代码的路径底下去查找,如果找不到,再去库目录底下查找。

所以我们一般包含库文件用<>.而包含自己的头文件用" " ,当然库文件也可以用" "来包含,只是这样的话就要查找两次了,效率比不上使用< >,不建议库文件用 " "。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值