C语言进阶——预处理详解

目录

一,#define

1.1 预定义符号

1.2 #define定义标识符

1.3 #define定义宏

1.4 #define替换规则

1.5 #和##

1.5.1 #

1.5.2 ##

1.6 宏和函数对比

二,使用宏时容易犯的错误

2.1 在宏定义后面加分号

2.2 宏定义时不正确使用括号

2.3 带副作用的宏参数

三,#undef

四,命令行定义

五,条件编译

5.1 #if,#else,#endif

5.2 #if defined(),#if !defined()

5.3 #pragma once 

六,文件包含


 

一,#define

1.1 预定义符号

在预处理阶段,编译器提前提供了一些指令以供我们使用,下面就是几个常用的预定义符号

void main1()
{
	printf("%s\n", __FILE__);      //进行编译的源文件地址
	printf("%d\n", __LINE__);      //文件当前的行号
	printf("%s\n", __DATE__);      //文件被编译的日期(年月日)
	printf("%s\n", __TIME__);      //文件被编译的时间(时分秒)
	//printf("%d\n", __STDC__);//当前VS是不支持ANSI C
}

 

1.2 #define定义标识符

#define M 100
#define STR "abc"
#define FOR for(;;)

void main2()
{
	printf("%d\n", M);
	printf("%s\n", STR);
	FOR
	{
		//死循环 
	}
}

预处理后结果为

 

 可以看到#define对应的符号被替换了,所以有人写代码的时候就用偷懒了

#define CASE break;case
void main3()
{
	int d = 0;
	switch (d)
	{
	case 1:break;
	case 2:break;
	case 3:break;
	}
	//懒得写代码,所以可以直接这样写代码
	switch (d)
	{
	case 1:
	CASE 2 :
	CASE 3 : ;
	}
}

预处理后变成这样了

 

同样的,我们可以尝试用#define来打印我们的预定义符号

#define DEBUG_PRINT printf("file:%s\tline:%d\t date:%s\t \
					time:%s\n",__FILE__,__LINE__, \
					__DATE__,__TIME__)
//有时候一行代码太长了不好看需要分行,但是分行后又会出问题
//续航符\,其实就是转义字符,转最后的换行字符,后面不能有空格
void main4()
{
	DEBUG_PRINT;
}

 预编译后变成下面这样子了

1.3 #define定义宏

#define机制包括了一个规定,运行把参数替换到文本中,这种实现通常被称为宏或宏定义,下面是宏的声明方式

#define name(parament-list) stuff
//其中的parament-list是一个由逗号隔开的符号表,他们可能出现在stuff中
//参数列表的左括号必须与name紧邻,不能有任何空格存在

 我们定义一个求平方的宏

#define SQUARE(x) x*x

这个宏接收一个参数x,在声明之后在传入参数

SQUARE(5);

预处理后,就会用下面这个式子替换上面的式子

5*5

 但是这个宏有些许问题,我们后面再讲

1.4 #define替换规则

再程序中扩展#define定义符号和宏时,需要涉及几个步骤

①再调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是先被替换

②替换文本后被插入到程序中原来文本的位置,对应宏,参数名被它们的值替换

③最后再次对文件进行扫描,看它是否包含任何由#define定义的符号,如果是就重复上面的过程

注意

①宏参数和#define定义中可以出现其他#define定义的变量,但是对应宏不能出现递归

②当预处理器搜索#define定义的符号的时候,字符串常量的内容不被搜索

1.5 #和##

1.5.1 #

先看下面的代码

//打印的两种写法
void main8()
{
	printf("hello world\n");  //直接打印字符串
	printf("hello ""world\n");//两个字符串拼在一起打印
}

所以我们可以知道字符串是有自动连接的特点的

所以我们可以这样写宏

#define PRINT(n, format) printf("the value of " n " is " format "\n", n)
void main9()
{
	int a = 20;
	PRINT("a", "%d");
	int b = 15;
	PRINT("b", " % d");
	float f = 4.5f;
	PRINT("f", "%f");
}

这种方法就是将a,b,c以字符串的形式传给宏参数打印各自的数值,但是打印结果却是这样的

预处理后变成这样了

完全乱套了鸭! 

所以我们可以使用“#”来解决上面的问题

//#可以把宏的参数以字符串的形式插入到字符串里面去
#define PRINT(n, format) printf("the value of " #n " is " format "\n", n)
void main9()
{
	int a = 20;
	PRINT(a, "%d");
	int b = 15;
	PRINT(b, "%d");
	float f = 4.5f;
	PRINT(f, "%f");
}

 

这样打印结果就正确了 

1.5.2 ##

##可以把宏中的两个符号合成为一个符号,运行宏定义从分离的文本片段创建标识符

#define CAT(x,y) x##y
void main10()
{
	int Class110 = 2024;
	printf("%d\n", CAT(Class, 110)); //打印2024
	printf("%d\n", Class110);        //打印2024
}

1.6 宏和函数对比

宏通常被应用于执行简单的运算,比如在两个数中找出较大的那一个

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

 为什么不用函数呢?原因有二:

①用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需时间更多,所以宏比函数在程序的规模和速度方面更优(从反汇编的角度来看,要使用函数首先要调用函数,然后创建该函数栈帧,然后再进行逻辑计算,计算完函数返回,最后还要销毁栈帧)

②C语言不支持模板,所以函数的参数必须声明为特定类型,只能再类型合适的表达式上使用;反之宏可以适用于整形,长整形,浮点型以及结构体成员变量等可以用">"来比较的类型,宏是类型无关的

 当然宏相比函数也有缺点:

①如果宏过长,预处理后会大大增加代码量,代码可读性变差

②宏无法调试

③因为宏是类型无关的,所以安全性较差

④宏可能会带来运算符优先级的问题,导致程序容易出错

 宏也有函数做不到的事情,列入宏的参数可以出现类型,但函数做不到

#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
void main12()
{
	int* p1 = (int*)malloc(126 * sizeof(int));
	//malloc(126, int);
	int* p2 = MALLOC(126, int);//将int作为类型传给宏的参数
	int* p3 = (int*)malloc(126 * sizeof(int));
}

二,使用宏时容易犯的错误

2.1 在宏定义后面加分号

有很多人在宏定义后面喜欢在后面加上分号,虽然大多数情况下没有问题,但是不建议这样做,比如下面的代码

#define M 100;
void main5()
{
	int a = 0, b = 0;
	if (a > 5) 
        b = M;
	else 
        b = -1;
}

我们判断a是否大于5,如果大于就把b赋值为100,如果不大于就把b赋值为-1,乍一看没啥毛病,但是预处理后,程序变成了下面这样 

这个时候错误就来了,if没接{ }时只会默认将后面的一条语句作为主体来执行,宏定义加上分号后,这里就变成了两个分号代表两个语句,这时if就无法和后面的else串联起来,程序编译时就会报错,所以建议宏定义后面不要加上分号 

2.2 宏定义时不正确使用括号

还记得我们前面说过宏求平方那一条吗?我们先看下面代码

/用宏定义求一个平方数
#define SQUARE(x) x*x
void main7()
{
	int a = 3;
	int r = SQUARE(a + 2);
	printf("r = %d\n", r);
	
}

a是3,然后我们把a+2作为参数传给宏,所以按照预期算出来的结果应该是25,但是结果却是

 

这就是不正确使用括号的结果,上面的式子预处理后会变成下面这个 

简单点说,宏就是一个“傻瓜式的替换”,宏不会去关注一些运算的,它只做替换,上面就是3+2*3+2=11。所以有些地方最好加上括号,保证运算优先级

#define SQUARE(x) (x)*(x)
void main7()
{
	int a = 3;
	int r = SQUARE(a + 2);
	printf("r = %d\n", r);
}

预处理后就是

 

2.3 带副作用的宏参数

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

x+1;//不带副作用
x++;//带副作用
#define MAX(x, y) ((x)>(y)?(x):(y))
int Max(int x, int y)
{
	return (x > y ? x : y);
}
void main11()
{
	int a = 5;
	int b = 6;
	int c1 = MAX(a++, b++);
	//int c = ((a++) > (b++)) ? (a++) : (b++)
	printf("a = %d\n", a);  //6
	printf("b = %d\n", b);  //8
	printf("c1 = %d\n", c1);//7
	printf("\n");

	a = 5, b = 6;
	int c2 = Max(a++, b++);
	printf("a = %d\n", a);  //6
	printf("b = %d\n", b);  //7
	printf("c2 = %d\n", c2);//6
}

三,#undef

这条指令用于移除一个宏定义

#define MAX(x, y) ((x)>(y)?(x):(y))
void main14()
{
	int c = MAX(3, 5);
	printf("%d\n", c);
#undef MAX
	//c = MAX(5, -5);  无法使用MAX了
	printf("%d\n", c);
}

四,命令行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。

(下面操作均在Linux系统下进行)

这里我没有对arr数组长度进行定义,我们通过命令行来给SZ赋值

gcc test.c -D SZ=100 -o test

 最后我们执行test程序,成功打印0到100的数字

五,条件编译

在编译一个程序的时候我们如果要将一条语句或一组语句保留或者放弃是很方便的,因为我们有条件编译指令。

(以下代码也是在Linux系统下操作)

5.1 #if,#else,#endif

#define M 2
void main1()
{
#if M==1
	printf("hehe\n");
#elif M==2
	printf("haha\n");
#else
	printf("heihei\n");//也是预处理阶段搞的,如果满足条件,在编译的时候直接删了
#endif
}

预处理后

可以看到不符合条件的在预处理后直接删掉了

5.2 #if defined(),#if !defined()

 #if defined() 表示括号里的标识符如果定义了该咋办咋办

#if !defined() 标识括号里的标识符如果没被定义该怎么样怎么样

#define WIN 1
void main2()
{
#if defined(WIN) //表示如果WIN被定义了该什么什么
	printf("windows");
#endif
	//#if defined 和 #ifdef效果一样
#ifdef WIN
	printf("windows");
#endif
	///
#if !defined(WIN) //表示如果没有定义就什么什么
	printf("windows\n");
#endif
	//效果一样
#ifndef WIN
	printf("windows\n");
#endif
}

预处理后,变成下面这样

 

5.3 #pragma once 

有人可能会这样写代码:

//test.h
int Add(int x,int y)
{
    return x+y;
}

//test.c
#include<stdio.h>
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
void main3()
{
	int a=10,b=20;
	int c=Add(a,b);
}

int main()
{
	main3();
	return 0;
}

一次性包含很多头文件,预处理后变成下面这样

 

造成代码大量重复,影响各方面

这时候我们可以在头文件加上#pragma once表示该头文件只包含一次,现在高版本的VS编译器在创建头文件的时候都会带上这条语句

//test.h
#pragma once
int Add(int x,int y)
{
    return x+y;
}

//test.c
#include<stdio.h>
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
void main3()
{
	int a=10,b=20;
	int c=Add(a,b);
}

int main()
{
	main3();
	return 0;
}

预处理后变成这样

 

六,文件包含

 我们在代码开头都包含一些头文件,库文件一般用<>,我们自己写的头文件一般用“ ”

查找策略:先在源文件所在的目录下找,如果头文件未被找到,编译器就会去标准库函数的文件路径下去找,<>表示去标准库目录下去找," "表示在当前目录下找

Linux环境标准头文件路径为:

/usr/include

windows环境下的标准头文件路径为 

C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include

这个依据自己VS的按照目录来确定

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值