c语言基础(一)——预处理详解

目录

1.预处理是什么?

2.预定义符号

3.define标识符

3.1 define 定义标识符

3.2 define定义宏

命名约定

3.3 宏的题目

4.# 和 ##

4.1 #的作用

4.2 ##的作用

5.带副作用的宏参数

6.宏与函数的对比

7.#undef

8.条件编译

9.头文件被包含的方式


1.预处理是什么?

预编译又称为预处理 , 是做些代码文本的替换工作。处理 # 开头的指令 , 比如拷贝 #include 包含的文件代码, #define 宏定义的替换 , 条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。

预处理主要工作:

1.头文件的包含           2.define宏定义的替换       3.条件编译的处理      
 

2.预定义符号

  • __FINE__    //进行编译的源文件
  • __Line__     //文件当前的行号
  • __Date__     //文件被编译的日期
  • __TIME__    //文件被编译的时间
  • __STDC__  //如果编译器遵循ANSI,其值为1,否则则未定;

上面这些是预定义符号都是语言内置的

注意:符号中的左右是两个横杠,是__FINE__,而不是_FINE_,

例如:

	printf("文件:%s\n", __FILE__);
	printf("文件当前的行号:%d\n", __LINE__);
	printf("文件编译的日期:%s\n", __DATE__);
	printf("文件编译的时间:%s\n", __TIME__);

3.define标识符

3.1 define 定义标识符

# define宏定义是个演技非常高超的替身演员,在编译器编译时,真身则会替换替身,在我们代码中我们会经常使用这个替身。

#define MAX 1000

int max = MAX;//在预处理阶段,MAX会替换为1000,相当于int max=1000;

3.2 define定义宏

#defifine 机制包括了一个规定, 允许把参数替换到文本中,这种实现通常称为宏或定义宏
例如我们写一个数字的平方:
#define SQUARE(x) x*x

int main()
{
	printf("%d", SQUARE(5));//输出25
	return 0;
}

当我们传5进去后,在预编译阶段,则SQUARE(5)会被替换为5*5,所以最终输出为25.

	printf("%d", SQUARE(2 + 3));//输出11
    

为什么上面代码会输出11呢?

当我们传2+3进去后,在预编译阶段,则SQUARE(2+3)会被替换为2+3*2+3,在根据优先级计算,即可得出11,我们想要的是(2+3)*(2+3),但输出并没有符号我们预期,我们需要把宏修改一下。

#define SQUARE(x) (x)*(x)
printf("%d", SQUARE(2 + 3));//输出25

那么我们在来定义一个宏,求整数的两倍。

	#define DOUBLE(x) (x)+(x)
    printf("%d", DOUBLE(5));//输出10
	printf("%d", 2*DOUBLE(5));//输出15

 DOUBLE(5)输出10我们还是能够理解,但是2*DOUBLE(5)输出15是怎么回事呢,在预编译阶段,2*DOUBLE(5)则会被替换成2*(5)+(5),我们想要的是输出20的结果,那怎么办呢?那么我们在外边直接加个大括号即可。

#define DOUBLE(x) ((x)+(x))

总结:所以我再写宏定义时尽量给每个变量加个括号,最外层的括号也别省。例如之前#define SQUARE(x) (x)*(x),在外层的括号也加上去,变成

#define SQUARE (x)   ((x)*(x))

命名约定

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

3.3 宏的题目

写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。

代码:

#define SWAP_BIT(n)  n=(((n)&0x55555555)<<1)|(((n)&0xaaaaaaaa)>>1)

思路:先取出整数二进制的奇数位上的数,&上一个01010101010101010101010101010101提取出奇数位,对应的十六进制为0x55555555,在取出整数的二进制的偶数位上的数,&上一个10101010101010101010101010101010提取出偶数位,对应的十六进制为0xaaaaaaaa,然后对奇数位上的数左移一位(<<)就变成偶数位,对偶数位上的数右移一位(>>)就变成奇数位了,在对这两个数进行 | 就可以完成交换。

4.# 和 ##

4.1 #的作用

#VALUE会被预处理为“VALUE”

	int a = 0;
	double d = 1.11;
	printf("the value of a is %d\n", a);//输出:the value of a is 0
	printf("the value of d is %f\n", d);//输出:the value of d is 1.110000

那么我们怎样可以写一个宏来替代    

printf("the value of a is %d\n", a);  

printf("the value of d is %f\n", d);这两条语句呢?

首先我们必须先知道:

	printf("hello " "world\n");//输出hello world
字符串是有自动连接的特点的
所以我们可以定义这样一个宏:
#define PRINT(FORMAT,DATA) printf("the value of " #DATA " is " #FORMAT“\n",DATA)

int a=10;
double d=1.11;
PRINT(%d, a);//输出:the value of a is 0
PRINT(%f, d);//输出:the value of a is 1.110000

所以我们就很好通过一个宏就替代上面那两条语句。

注意:宏中的#DATA在预编译时会替换成”DATA"  ,#FORMAT会被替换为“FORMAT"。

4.2 ##的作用

##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符
	#define ADD_TO_SUM_(num,value) sum##num+=value;
    int sum1 = 1;
	int sum2 = 2;
	ADD_TO_SUM_(1, 10);//sum1增加10
	ADD_TO_SUM_(2, 10);//sum2增加10

sum##num为sumnum(num为传进来的宏参数)

5.带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候, 如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险 ,导致不可预测的后果。
例如:
#define MAX(x,y)  ((x)>(y))?(x):(y)

int main()
{
	int a = 0;
	int b = 0;
	MAX(a++, b++);
	printf("%d %d", a, b);//结果:a为1,b为2
	return 0;
}

      为什么运行结果a为1,b为2呢?

    在预编译时,MAX(a++,b++)会被替还为  ((a++)>(b++))?(a++):(b++),

    在 ?判断之前 a跟b都进行了一次++,都变为1,判断a不比b大后,则在运行b++,b在变成2.
所以 a++,和b++这种会改变变量本身的宏参数为带有副作用的宏参数。

6.宏与函数的对比

宏跟函数其实有有很多不同点,那么我们来利用宏和函数来实现找出两个数中的较大值。

#define MAX(x, y) ((x)>(y)?(x):(y))//宏实现

int Max(int a,int b)//函数实现
{
   return a>b?a:b;
}
宏的优势:
1. 宏的参数不需要特定的类型,所以宏的使用可以传任意类型,宏是与类型无关的。而函数的参数必须为特定类型,所以使用的时候只能传特定的参数。

2.宏在预编译阶段中会直接将代码替换,而函数调用需要创建函数栈帧和返回值都需要消耗时间,所以宏的效率比函数要高

的 

宏的劣势:

1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度

2.由于宏是运行时将代码插入到程序中,所以宏是没办法调试

3.宏的参数可以是任意类型的,所以不严谨

4.宏会导致优先级的问题,容易出错。

5.宏是不能递归,函数可以递归

7.#undef

撤销一个宏定义,如果现存的名字需要重新定义,那么需要先移除它的旧名字。

#define MAX 100

int main()
{
	int MAX = 1000;//错误:MAX为宏定义,不能再重新定义
#undef MAX;//移除MAX的宏定义,下面的宏定义就不能用
	int MAX = 1000;//正确
	return 0;
}

宏的生命周期从#define 开始到#undef 结束

8.条件编译

条件编译的功能使得我们可以 按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件

条件编译有三种形式:

方式一:

#ifdef 标识符
   程序段一
#else
   程序段二
#endif 

上面的代码是,如果标识符有被#define定义过,则运行程序段一,如果没有定义过,则运行程序段二,如果没有程序段二(空),则#else可以没有    。 

#define DEBUG 1
int main()
{
#ifdef DEBUG
	printf("hello world\n");
#else
	printf("thank you\n");
#endif 
}    

      例如上面的代码,由于DEBUG被定义,所以在预编译的时候,编译器只会保留

int main()
{
    printf("hello world\n");
}

这段代码,其它代码被编译过滤掉,最终输出结果为hello world。

如果DEBUG没有定义,则编译器则会保留    
int main()
{
    printf("thank you\n");
}   

这段代码,其它代码被编译过滤掉,最终输出结果为 thank you。

方式二:

       

#ifndef 标识符
程序段 1
#else
程序段 2
#endif

    与第一种形式差不多,只是将#ifdef改为#ifndef,如果标识符没有被定义,则运行程序段1,有被定义则运行程序段 2,与方式一差不多,只是逻辑相反,所以笔者我就不再举例子了。

方式三:

#if 常量表达式1
	程序段 1
#elif 常量表达式2
	程序段 2
#else
	程序段 3
#endif

如果 常量表达式1的结果不为0,则为真,则只运行程序段 1,如果常量表达式结果为0(假),常量表达式为2不为0,则只运行程序段 2,如果常量表达式为1和常量表达式 2的结果都为0,则运行程序段 3。如果没有程序段 2和程序段 3(为空),则#elif和# else可以没有


int main()
{
#if 10
	printf("hello world\n");
#elif 10
	printf("you are very handsome\n");
#else
	printf("thank you\n");
#endif
	return 0;
}

对于上面的代码,由于常量表达式不为0,所以在预编译的时候,编译器只保留


int main()
{
    printf("hello world\n");

     return 0;
}
这段代码,其它代码被过滤掉。最终运行结果为 hello world ,如果常量表达式1的结果为0,则运行结果为 you are very handsome ,如果常量表达式1和常量表达2都为0,则运行结果为thank you。

假设我们有一个Add.h头文件,里面包含着几个函数的声明:

int Add_Int(int x, int y);
double Add_Double(double x, double y);
float Add_Float(float x, float y);

我们在test.c上引入Add.h的头文件,如下:

#include"Add.h"
int main()
{
	int a = 0, b = 0;
	Add_int(a, b);
	return 0;
}

在预编译阶段时,则编译器会发生替换,替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。

则test.c文件预编译后会变成:
 

int Add_Int(int x, int y);
double Add_Double(double x, double y);
float Add_Float(float x, float y);

int main()
{
	int a = 0, b = 0;
	Add_int(a, b);
	return 0;
}

但是如果我们的test.c文件多次包含头文件Add.h后,则预编译时会将Add.h的内容会包含多次,这就会导致test.c文件中的内容重复。

那么如何解决头文件的重复包含?答案是:条件编译

我们可以对Add.h头文件就行修改

#ifndef ADD_H
#define ADD_H 1
	int Add_Int(int x, int y);
	double Add_Double(double x, double y);
	float Add_Float(float x, float y);
#endif

如果test.c引入一次Add.h头文件,由于没有定义#define Add_H,则预编译时会定义一次#define Add_H,并且将头文件中的内容给包含进去,如果在引入一次Add.h头文件,因为ADD_H已经被定义,那么条件编译不会将头文件的内容包含进来,所以这就很巧妙的解决这些问题。

或者我们有一种现代写法:

#pragma once
int Add_Int(int x, int y);
double Add_Double(double x, double y);
float Add_Float(float x, float y);

上面的两种方式都能避免我们的头文件的重复引用。

9.头文件被包含的方式

c语言提供了两种头文件的包含方式,它实际是宏替换的延伸,有两种格式:

格式一:

#include<stdio.h> 

stdio为包含文件的名称,用尖括号括,预处理时会直接去库函数的头文件中查找,找不到则编译错误。

格式二:

#include"file.h"

 ”file.h为包含文件的名称,直接在当前目录下查找名为file.h的头文件,找不到在按系统指定的路径信息,搜索其他目录。找到文件后,用

文件内容替换该语句。

 点个赞呗~

  • 34
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值