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 宏的用途
- 定义一个常量
#define PI 3.1415926
- 给一个经常要使用的表达式一个字面意义
#define SIZE ( sizeof(a) / sizeof(int) )
- 执行简单的计算
#define MAX(a,b) ( (a) > (b) ? (a) : (b) )
- 改造现有的函数
#define MALLOC( type, size ) ( type* )malloc( sizeof( type ) * size )
- 更多…
0x01 宏定义常见错误
- 在末尾带了分号
#define PRINT_LOS printf("u lost");
#define PRINT_WIN printf("u win");
	if( a )
		PRINT_LOS;
	else
		PRINT_WIN;

 编译器报错:没有和else级联的if
 原因:宏定义中多余的分号,相当于在if和else之间插入了多余的一行
- 在定义含参数的宏时,宏名称和括号之间带了空格
#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 宏的三个技巧
- 当字符串常量作为宏参数时给出时,可以利用相邻字符串的自动连接的特性
#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;
}

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

- 使用##连接两个相邻的符号
#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*/
/*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

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号