C 编译预处理和宏

前置知识

0x00 cmd编译运行程序

https://blog.csdn.net/WWIandMC/article/details/106265734

0x01 --save-temps

gcc main.c --save-temps		#save和temps中间是一个减号

在这里插入图片描述

文件后缀(按照生成先后顺序排序)释义
.i编译器将所有的预处理指令替换完之后生成的中间文件
.s汇编代码文件
.o目标代码文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

0x02 tail

cmd中并没有tail命令,可以在git中使用

$ tail main.i			#在终端显示main.i末尾几行
$ tail main.i -n 50		#指明显示main.i末尾50行

在这里插入图片描述在这里插入图片描述

cmd使用tail命令的方法:在System32目录下添加tail.exe,这种方法拓展的tail命令不支持-n选项
https://blog.csdn.net/sishi22/article/details/82285707

预定义符号

#include <stdio.h>

int
main( int argc, char **argv )
{
	printf("%s\n", __FILE__ );	/*源代码文件名*/
	printf("%d\n", __LINE__ );	/*出现这个符号的行号*/
	printf("%s\n", __DATE__ );	/*文件被编译时的系统日期*/
	printf("%s\n", __TIME__ );	/*文件被编译时的系统时间*/
	printf("%d\n", __STDC__ );	/*编译器遵循ANSI C为1*/
	
	return 0;
}

预定义符号的位置会被替换成字符串常量或数值
在这里插入图片描述

宏 Macro

所有用于对数值表达式进行求值的宏定义都应加上括号,避免在使用宏的时候,参数中的操作符或邻近的操作符之间不可预料的相互作用——《C和指针》

为了将函数和宏区分开,一般约定宏的名字全部大写——《C和指针》

0x00 宏的用途

  1. 定义一个常量
#define PI 3.1415926
  1. 给一个经常要使用的表达式一个字面意义
#define SIZE ( sizeof(a) / sizeof(int) )
  1. 执行简单的计算
#define MAX(a,b) ( (a) > (b) ? (a) : (b) )
  1. 改造现有的函数
#define MALLOC( type, size ) ( type* )malloc( sizeof( type ) * size )
  1. 更多…

0x01 宏定义常见错误

  1. 在末尾带了分号
#define PRINT_LOS printf("u lost");
#define PRINT_WIN printf("u win");

	if( a )
		PRINT_LOS;
	else
		PRINT_WIN;

在这里插入图片描述
编译器报错:没有和else级联的if
原因:宏定义中多余的分号,相当于在if和else之间插入了多余的一行

  1. 在定义含参数的宏时,宏名称和括号之间带了空格
#define CUBE (x) ( (x) * (x) * (x) )

在这里插入图片描述
空格的存在让编译器把(x)也当成了要替换进去的内容

3.在带参数的宏中使用++操作符

#define CUBE(x) ( (x) * (x) * (x) )

printf("a ^ 3 = %d\n", CUBE( a++ ) );
printf("a = %d\n", a);

如果把带参数的宏不加区分的当成函数用的话,我们预测将打印出8和3

而实际上打印出了24和5
在这里插入图片描述
查看main.i之后一目了然
在这里插入图片描述

4.更多…

0x02 宏的三个技巧

  1. 当字符串常量作为宏参数时给出时,可以利用相邻字符串的自动连接的特性
#include <stdio.h>

#define MYPRINT( FORMAT, VALUE )	\
		printf( "The value is" FORMAT "\n", VALUE )

int
main( int argc, char **argv )
{
	MYPRINT( "%d", 17);

	return 0;
}

在这里插入图片描述

  1. 使用#让预处理器将一个宏参数转换为一个字符串常量
#define MYPRINT( FORMAT, VALUE )	\
	printf("The value of" #VALUE "is" FORMAT "\n", VALUE )
	
	MYPRINT( "%d", x +3);	/*特地在+后面不留空格*/

在这里插入图片描述

  1. 使用##连接两个相邻的符号
#include <stdio.h>

#define ADD_TO_SUM( sum_number, value )	\
	sum ## sum_number += value

int
main( int argc, char **argv )
{
	int sum5 = 3;
	ADD_TO_SUM( 5, 10 );
	printf("%d", sum5);

	return 0;
}

在这里插入图片描述
注意:##是单纯的字符连接,即使sum_number是一个有值的int变量,也只是连接变量名而已

#include <stdio.h>

#define ADD_TO_SUM( sum_number, value )	\
	sum ## sum_number += value

int
main( int argc, char **argv )
{
	int sum5 = 3;
	int i = 5;
	ADD_TO_SUM( i, 10 );
	printf("%d", sum5);

	return 0;
}

在这里插入图片描述
在这里插入图片描述

0x03 删除宏定义

#undef DEBUG		

重新定义某个已定义的宏之前,必须要用#undef移除旧定义

函数和宏的对比

0x00 执行效率对比

#include <stdio.h>

#define MAX(a,b) ( (a) > (b) ? (a) : (b) )

int
main( int argc, char **argv )
{
	int a = 1;
	int b = 2;
	printf("%d", MAX( a, b ) );

	return 0;
}

宏版本对应的反汇编代码

00F4144E  mov         dword ptr [a],1						;[]取地址符, dword ptr 指向双字的指针
  
00F41455  mov         dword ptr [b],2  

00F4145C  mov         eax,dword ptr [a]  					;将a的内容送入eax寄存器
00F4145F  cmp         eax,dword ptr [b]						;b的内容和eax的内容做比较, 比较结果作用于下一条jle指令
00F41462  jle         main+3Fh (0F4146Fh)					;a大于b为假则跳转到0F4146F

00F41464  mov         ecx,dword ptr [a]		 
00F41467  mov         dword ptr [ebp-0DCh],ecx				;将ecx的内容送入内存地址为ebp-0DCh的双字单元
00F4146D  jmp         main+48h (0F41478h)					;无条件跳转到0F41478

00F4146F  mov         edx,dword ptr [b]  
00F41472  mov         dword ptr [ebp-0DCh],edx  
00F41478  mov         esi,esp  								;esi存放源数据串的偏移地址, esp存放指向当前堆栈的栈顶偏移地址
00F4147A  mov         eax,dword ptr [ebp-0DCh] 				
00F41480  push        eax									;eax内容入栈
00F41481  push        offset string "%d" (0F45A00h)  		;内存地址0x0F45A00的格式字符串入栈
00F41486  call        dword ptr [__imp__printf (0F482B0h)]	;调用printf函数
#include <stdio.h>

int
max( int a, int b );

int
main( int argc, char **argv )
{
	int a = 1;
	int b = 2;
	printf("%d", max( a, b ) );

	return 0;
}

int
max( int a, int b )
{
	return ( (a) > (b) ? (a) : (b) );
}

函数版本对应的反汇编代码

010F10EB  jmp         max (10F1420h)				;跳转到函数体部分

...

010F13AE  mov         dword ptr [a],1
010F13B5  mov         dword ptr [b],2  

010F13BC  mov         eax,dword ptr [b]  
010F13BF  push        eax
010F13C0  mov         ecx,dword ptr [a]  
010F13C3  push        ecx  
010F13C4  call        @ILT+230(_max) (10F10EBh)  
010F13C9  add         esp,8  
010F13CC  mov         esi,esp  
010F13CE  push        eax  
010F13CF  push        offset string "%d" (10F573Ch)  
010F13D4  call        dword ptr [__imp__printf (10F82B0h)]

...

010F1420  push        ebp  							
010F1421  mov         ebp,esp  						
010F1423  sub         esp,0C4h  					
010F1429  push        ebx							
010F142A  push        esi							
010F142B  push        edi  							
010F142C  lea         edi,[ebp-0C4h]				
010F1432  mov         ecx,31h  
010F1437  mov         eax,0CCCCCCCCh  
010F143C  rep stos    dword ptr es:[edi]  

010F143E  mov         eax,dword ptr [a]  			;准备完毕开始执行
010F1441  cmp         eax,dword ptr [b]  
010F1444  jle         max+31h (10F1451h)
  
010F1446  mov         ecx,dword ptr [a]  
010F1449  mov         dword ptr [ebp-0C4h],ecx  
010F144F  jmp         max+3Ah (10F145Ah)
 
010F1451  mov         edx,dword ptr [b]  
010F1454  mov         dword ptr [ebp-0C4h],edx  
010F145A  mov         eax,dword ptr [ebp-0C4h]		;将结果存入eax寄存器

010F1460  pop         edi							;pop出栈操作
010F1461  pop         esi  
010F1462  pop         ebx  
010F1463  mov         esp,ebp 						
010F1465  pop         ebp  	
010F1466  ret

从宏版本的汇编代码和函数版本的汇编代码的长度对比来看,用宏来执行一些简单的计算,所需的开销比函数小

0x01 参数使用对比

在前面常见错误部分,我们讲到在宏当中使用++是有风险的。我们的本意是想在完成所有操作之后再让参数++, 但实际的执行结果是++执行了很多次。其原因是宏进行简单的文本替换,++作为参数的一部分参与文本替换,有多个参数时就会有多个++

而对于函数来说,函数获得的是参数的拷贝而不是参数本身。函数结束之后,后缀++才作用于参数。

0x02 参数类型对比

宏执行的操作是文本的替换,它与类型无关,我们可以把int, float这些关键字作为参数进行传递,如下面这个例子

#define MALLOC( type, size ) ( type* )malloc( sizeof( type ) * size )

a = MALLOC( int, 10 );

而函数的参数与类型是有关的。如果参数类型不同就需要不同的函数,即使代码一模一样

0x03 代码长度对比

每次使用宏时,宏代码都被插入到代码中。会增加代码的长度。函数则不存在长度变化的问题。

0x04 总结

属性函数
执行效率较慢
参数类型无关有关
代码长度增加不变
++等操作符的副作用存在不存在

条件编译

0x00 #if

#if 常量
	语句
#endif 

和if-else一样,当常量为1时,语句正常编译;如果常量为0,编译器则将它们删除。
为什么要强调是常量呢?来看一个例子

#include <stdio.h>

#define OP +
int
main( int argc, char *argv[] )
{
	int option;
	scanf("%d", &option);

#if option

#undef OP
#define OP -

#endif
	
	printf("%d", 2 OP 1 );
	return 0;
}

现在我们在#if后面跟了一个变量,我们希望option赋值为1之后,OP能换成减号,从而输出1。编译运行
在这里插入图片描述
两次结果都是3。查看.i文件
在这里插入图片描述
在生成可执行文件之前需要进行编译预处理,在执行程序之前就已经完成了宏文本的替换,而对option赋值的操作是在执行程序时。所以在#if使用变量是没有意义的。

如果要让程序不执行某些语句,又不想把它们从源文件中删除,除了注释之外,还可以使用#if

#define DEBUG 0

#if DEBUG

#endif

#if也支持类似else if的级联结构

#if DEBUG1

#elif DEBUG2

#else

#endif

0x01 #ifdef和#ifndef

#ifdef和#ifndef的字面意思就是 if defined 和 if not defined

#ifdef DEBUG

#endif

#ifndef DEBUG

#endif
#define DEBUG	/*此时DEBUG被定义为一个空字符串*/
#ifdef DEBUG

#endif

0x02 命令行定义

gcc main.c -DDEBUG		# -D后面是宏的名字
gcc main.c -DDEBUG=3	# DEBUG定义为3
gcc main.c -Dmian=main	# 把main替换为main 这是一个历史悠久的bug
gcc main.c -UDEBUG		# 忽略DEBUG的初始定义
int
fact( int n )
{
	int fact = 1;
	while( n > 1 ){
		fact *= n--;
#ifdef DEBUG
		printf("%d\n", fact );
#endif
	}
	
	return fact;
}

在这里插入图片描述
在这里插入图片描述

文件包含

0x00 #include做了什么

创建一个头文件head.h

/*
**	head.h
*/
void f( void );

在main.c添加include

/*
**	main.c
*/
#include "head.h"

在这里插入图片描述
head.h的内容插入了main.c,可见#incldue和#define一样,都是做文本替换的工作

在.i文件中,#表示此行是注释的意思

0x01 三种#include方式

#include <stdio.h>

编译器在默认的“函数库”中查找头文件

#include "head.h"
#include "stdio.h"

编译器在源文件所在目录查找,如果找不到回到默认的函数库再找一遍,这也是为什么"stdio.h"能通过编译的原因

#include "C:\Users\Administrator\Desktop\folder\head.h"

给出头文件的路径

0x02 嵌套文件包含

现在有两个头文件a.h和b.h

#include "a.h"
#include "b.h"

其中b.h中#include了a.h

/*
**b.h
*/
#include "a.h"

这就意味着实际上a.h被替换了两次,这种嵌套的文件包含会影响编译的速度

在C++中,如果头文件里有class的声明,嵌套文件包含引起的class重复声明,将会引起编译错误

禁止套娃的办法就是使用#ifndef

#ifndef HEAD_H_
#define HEAD_H_

#endif

0x03 一道思考题

在这里插入图片描述
ppt内容
在这里插入图片描述

其他

0x00 #error

用于生成错误信息

#ifdef OPTION1
	#define OP1 
#elif OPTION2
	#define OP2
#else
	#error No option select!
#endif

在这里插入图片描述

0x01 #line

			#line 100 "head.h"
/*100*/![在这里插入图片描述](https://img-blog.csdnimg.cn/20200522180321585.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1dXSWFuZE1D,size_16,color_FFFFFF,t_70)
/*101*/
/*102*/		#include <stdio.h>
/*103*/
/*104*/		int
/*105*/		main( int argc, char *argv[] )
/*106*/		{
/*107*/			printf("%d\n", __LINE__);
				printf("%s\n", __FILE__);
				
				return 0;
			}

在这里插入图片描述
#line会修改__LINE__和__FILE__的值,其中#line会指定下一行为指定的行号

参考链接

https://www.icourse163.org/learn/ZJU-9001?tid=9001#/learn/content?type=detail&id=176002&sm=1
https://study.163.com/course/courseLearn.htm?courseId=271005#/learn/video?lessonId=380125&courseId=271005
https://www.icourse163.org/learn/ZJU-9001?tid=9001#/learn/content?type=detail&id=175002&cid=191088

posted @ 2020-05-17 15:16  LanceHansen  阅读(72)  评论(0编辑  收藏  举报