C语言--预处理


前言

在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。


一、翻译环境

在这里插入图片描述

1.编译

预编译(.i)

预处理指令

  1. #include头文件的包含
  2. 注释删除,使用空格替换注释
  3. #define替换

编译(.s)

把C语言代码翻译成汇编代码

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总

汇编(.o)

  1. 把汇编指令转换成二进制指令
  2. 形成符号表

例子

add.c源文件中
int Add(int x, int y)
{
	reutrn x + y;
}
在linux生成add.o文件,在vs中生成add.obj文件,目标文件
编译时会进行符号汇总 Add
汇编时形成符号表 Add 0x111(地址)
test.c源文件中
#include<stdio.h>
extern int Add(int x,int y);
int main()
{
	int a = 10;
	int b = 20;
	int c =Add(a,b);
	printf("%d\n",c);
	return 0;
}
在linux生成test.o,在vs中生成test.obj文件,目标文件
编译时进行符号汇总 Add 和 main
汇编时形成符号表	Add   	0x000    只是声明,找不到确切的地址,因此放入一个无意义的地址
    			main 	0x200

2.链接

链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库
每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序
将其需要的函数也链接到程序中

  1. 合并段表
  2. 符号表的合并和符号表的重定位
  3. 两个Add名字有冲突的时候,用有效的地址填入,合并和重定位

二、执行环境

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止main函数;也有可能是意外终止。

三、预处理符号

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

这些预定义符号都是语言内置的,例子:

printf("file:%s line:%d\n",__FILE__,__LINE__);

四、#define(定义标识符)

1.语法

  1. #define name stuff
  2. 续行符号 \
  3. 不加分号
#define MAX 100
#define STR "hehe"
#define reg registerregister这个关键字创建一个简短的名字 reg
#define do_forever for(;;)	用do_forever代表for(;;)
#define CASE break;case		再写case语句的时候自动把break写上
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
							date:%s\ttime:%s\m", \
							__FILE__,__LINE__,\
							__DATE__,__TIME__)
如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠  \(续航符)
不加分号的原因
#define MAX 100 ;
int main()
{
	int a = MAX;
	printf("%d\n", a);
	return 0;
}
在进行预处理时会导致问题
预处理后
#define MAX 100;
int main()
{
	int a = 100; ; 会多出一个分号导致出现问题
	printf("%d\n", 100;); 会多出一个分号导致出现问题
	return 0;
}

2.定义宏

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

声明方式

  1. #define name(parament-list) stuff其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
  2. 参数列表的左括号必须与name相邻,如果两者之间由任何空白存在,参数列表就会被解释为stuff的一部分,用于对数值表达式进行求值的宏定义都因该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
#define SQUARE(x) x*x        替换
int main()
{
	int ret = SQUARE(5);
	预处理 int ret = 5 * 5
	int ret = SQUARE(5+1);
	预处理 int ret = 5 + 1 * 5 + 1
	return 0;
}

改进后
#define SQUARE(x) (x)*(x)
int main()
{
	int ret = SQUARE(5+1);
	预处理int ret = (5+1)*(5+1)
	return 0;
}

#define DOUBLE(x) x+x
int main()
{
	int a = 5;
	int ret = 10 * DOUBLE(a);
	预处理int ret = 10 * 5 + 5
	return 0;
}

改进后
#define DOUBLE(x) (x+x)
int main()
{
	int a = 5;
	int ret = 10 * DOUBLE(a);
	预处理int ret = 10 * (5 + 5)
	return 0;
}
注意:用于对数值表达式进行求值的宏定义都因该用这种方式加上括号
避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

替换规则

在程序中扩展#define定义符号和宏时,需要设计几个步骤

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,他们首先被替换
  2. 替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值替换
  3. 最后再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是就重复上述步骤

注意:

  1. 宏参数和#define定义中可以出现其它#define定义的变量(可套娃使用),但是对于宏,不能出现递归
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

带副作用的宏参数

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

x+1    不带有副作用
x++    带有副作用
#define MAX(X,Y)( (X) > (Y) ? (X) : (Y)
int main()
{
	int a = 10;
	int b = 11;
	int max = MAX(a++, b++);
	预处理int max = ((a++) > (b++) ? (a++) : (b++));
	printf("%d\n", max); 12
	printf("%d\n", a); 11
	printf("%d\n", b); 13
	return 0;
}

3.#和##

#把宏的参数插入到字符中

printf特性
int main()
{
	printf("hello world\n");
	printf("hello" "world\n");
	printf("hel"lo" "world\n");
		三个打印结果相同
		return 0;
}
#define PRINT(X) printf("the value of "#X" is %d\n",X)
int main()
{
	int a = 10;
	int b = 20;
	PRINT(a);
	打印结果为the value of a is 10
	PRINT(b);
	打印结果为the value of b is 20
	return 0;
}

##(把位于它两边的符号合成一个符号)

可以把位于它两边的符号拼成一个符号
注意:这样的链接必须产生一个合法的标识符,否则其结果就是未定义的

它允许宏定义从分离的文本片段常见标识符
#define CAT(X,Y) X##Y
int main()
{
	int Class11 = 2019;
	printf("%d\n", CAT(Class, 11);
	预处理printf("%d\n", Class##11);
	等价于printf("%d\n", Class84);
	return 0;
}

4.宏和函数的对比

在这里插入图片描述

使用宏的原因

  1. 宏通常被应用与执行简单的运算,比如在两个数中找出较大的一个
  2. 调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹
  3. 更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之宏可以适用于整型,长整型,浮点型等可以用来比较的类型,宏与类型无关
#define MAX(X,Y)    ((X)>(Y)?(X):(Y))
int Max1(int x,int y)
{
	return (x>y?x:y);
}

float Max2(float x,float y)
{
	return (x>y?x:y);
}

int main()
{
	int a = 10;
	int b = 20;
	float c = 3.0f;
	float d = 4.0f;
	int max1 = Max1(a,b);    函数在调用的时候,会有函数调用和返回开销
	float max2 = Max2(c,d);    过程更复杂繁琐,且要再写一个函数来计算float类型
	printf("%d\n",max1);
	printf("%f\n",max2);
	max1 = MAX(a,b);        预处理阶段就完成了替换
	max2 = MAX(c,d);        没有函数的调用和返回的开销
	预处理max1 = ((a) > (b)?(a):(b);
	预处理max2 = ((c) > (d)?(c):(d);
	printf("%d\n",max1);
	printf("%f\n",max2);
	return 0;
}

宏相比函数 劣势的地方

  1. 每次使用宏时,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是不能调试的
  3. 宏由于与类型无关,也不够严
  4. 宏可能会带来运算符优先级的问题,导致程序容易出错
#define TEST(x,y) printf("test\n")
int main()
{
	TEST();
	TEST();
	TEST();
	......		几十份,会使代码大幅度的增加,而函数则不会

宏相比函数 优势的地方

  1. 宏的参数可以出现类型,但是函数做不到
#define SIZEOF(type) sizeof(type)
int main()
{
	int ret = SIZEOF(int);
	预处理int ret = sizeof(int);
	return 0;
}

命名约定

一般来讲函数和宏的使用语法很相似,语言本身没法帮我们区分二者
平时的习惯是:
把宏名全部大写 函数名不要全部大写

五、#under(移除一个宏定义)

用于移除一个宏定义

#include<stdio.h>
#define MAX 100
int main()
{
	printf("MAX = "%d\n",MAX);
#undef MAX
	printf("MAX = "%d\n",MAX);
	会报错,将无法运行
	return 0;
}

六、命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另 外一个机器内存大些,我们需要一个数组能够大些。)

#include <stdio.h> 
int main() 
{ 
	 int array [ARRAY_SIZE]; 
	 int i = 0; 
	 for(i = 0; i< ARRAY_SIZE; i ++) 
	 { 
		 array[i] = i; 
	 } 
	 for(i = 0; i< ARRAY_SIZE; i ++) 
	 { 
		 printf("%d " ,array[i]); 
	 } 
	 printf("\n" ); 
	 return 0; 
} 

七、条件编译

在编译一个程序的时候,如果我们将一条语句(一组语句)编译或者放弃是很方便的

#include<stdio.h>
#define DEBUG
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int i = 0;
	for(i = 0;i < 10;i++)
	{
		arr[i] = 0;
#ifdef DEBUG        如果DEBUG被定义过,下面这条语句就参与编译
	如果DEBUG没有被定义过,在预编译时,下面这条语句就会被删除
	printf("%d ",arr[i]);
#endif
	}
	return 0;
}

1.#if

常量表达式(为真参与编译,为假不参与编译)

#if	常量表达式
	........
#endif

2.多个分支的条件编译

常量表达式(为真参与编译,为假不参与编译)

#if    常量表达式
......
#elif    常量表达式
......
#elif    常量表达式
......
#endif

3.判断是否被定义

上下两个等价
#if define(symbol)
#ifdef symbol

比如:
#define DEBUG
int main()					int main()
{							{
#if define(DEBUG)			#ifdef DEBUG		是否定义DEBUG,真执行,假不执行
	printf("hehe\n");			printf("hehe\n");
#endif						#endif
	return 0;						return 0;
}							}

与之对立的
#if !defined(symbol)
#ifndef symbol

比如
int main()					int main()
{							{
#if !define(DEBUG)			#ifndef DEBUG		是否定义DEBUG,真执行,假不执行
	printf("hehe\n");			printf("hehe\n");	与上面为相反的逻辑
#endif						#endif
	return 0;					return 0;
}							}

4.嵌套指令

#if defined(OS_UNIX)
	#ifdef OPTION1
		unix_version_option1();
	#endif
	#ifdef OPTION2
		unix_version_option2()
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
		msdos_version_option2();
	#endif
#endif

八、文件包含

#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。

头文件被包含的方式

1.本地文件包含

#include "filename"

查找策略:

  1. 先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件
  2. 如果找不到就提示编译错误,
  3. linux环境的标准头文件的路径:/usr/include
  4. VS环境的标准头文件的路径:C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include

2.库文件包含

#include<filename>

查找策略:

  1. 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
  2. 对于库文件也可以使用 “ ” 的形式包含,但这样做查找的效率就低些,也不容易区分使库文件还是本地文件

3.嵌套文件包含

在这里插入图片描述
comm.h和comm.c是公共模块。 test1.h和test1.c使用了公共模块。 test2.h和test2.c使用了公共模块。 test.h和test.c使用了test1模块和test2模块。 这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

第一种写法(古老的写法)
test.h头文件中
#ifndef __ADD_H__        如果没有定义过的话
#define __ TEST_H__        定义

int Add(int x,int y);

#endif
第一次被引用后会定义
第二次被引用后由于已经被定义了,所以不会重新定义,使得头文件不会被重复多次包含,节省空间
第二种写法
test.h头文件中
#pragma once        避免头文件的重复引入
int Add(int x,int y);

九、其它预处理指令

#error
#pragma
#line
....

参考《C语言深度解刨》学习

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值