C语言——预处理详解

目录

[1 预定义符号](#1 预定义符号)

2 #define定义常量

3 #define定义宏

[4 带有副作用的宏参数](#4 带有副作用的宏参数)

[5 宏替换的规则](#5 宏替换的规则)

[6 宏函数的对比](#6 宏函数的对比)

7 #和##

[8 命名约定](#8 命名约定)

9 #undef

[10 条件编译](#10 条件编译)

[11 头文件的包含](#11 头文件的包含)

[11.1 本地文件包含](#11.1 本地文件包含)

[11.2 库文件包含](#11.2 库文件包含)

[11.3 嵌套文件包含](#11.3 嵌套文件包含)


1 预定义符号

C语言设置了一些预定义符号,可以直接使用,预定义符号是在预处理期间处理的。

1 _ FILE _//进行编译的源文件

2 _ LINE _//文件当前的行号

3 _ DATE _//文件被编译的日期

4 _ TIME _//文件被编译的时间

5 _ _ STDC_ _//如果编译器遵循ANSIC,其值为1,否则未定义

举个例子:

复制代码
#include <stdio.h>
int main()
{
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%d\n", __LINE__);
	return 0;
}

结果显示日期,时间和行号:

2 #define定义常量

基本语法:

复制代码
#define name stuff

一些使用例子:

复制代码
#define MAX 1000;
#define reg register //为register关键字创建一个名字reg
#define do_forever for(;;)//for(;;)是死循环语句,可以用do_forever这种更形象的符号替代
#define CASE break;case//在写case语句的时候自动把break加上
//如果定义的stuff过长,可以分开几行写,除了最后一行外,每行的后面都加一个\(续航符)
#define DEBUG_PRINT printf("line:%d\t \
                            date:%s\t \
                            time:%s\n",\
                            __LINE__,\
                            __DATE__,\
                            __TIME__)

在define定义标识符,要不要加上分号(;)?

例如:

复制代码
#define MAX 1000
#define MAX 1000;

在define定义标识符时,最好不要加;,容易产生问题。例如:

复制代码
#include <stdio.h>
#define MAX 1000;
int main()
{
    int max = 0;
    if (1)
        max = MAX;
    else
        max = 1;
    return 0;
}

如果加了分号,替换后为:max=1000;; 这就是两条语句,没有大括号时,if后只能有一条语句,会出现语法错误,所以define定义标识符时,最好不要加分号。

3 #define定义宏

#define机制允许把参数替换到文本中,这种实现方式通常称为宏(macro) 或者定义宏(define macro)。下面是宏的申明方式:

#define name(parament-list) stuff

其中的parament-list是一个由逗号分开的符号表,它们可能出现在stuff中。

注意:参数列表的左括号必须与name紧邻,如果有任何空白存在,参数列表就会被解释为stuff的一部分。

举例:

#define SQUARE (x) x*x

这个宏接受一个参数x,假设x=5,将SQUARE(5)用在程序中,预处理器就会用5*5替换SQUARE(5)。

这个宏在使用时可能会存在一个问题,如果代码这么写:

复制代码
#include <stdio.h>
#define SQURE(x) x*x
int main()
{
	int a = 5;
	printf("%d\n", SQURE(a + 1));
	return 0;
}

乍一看这个代码输出结果时36,但是实际是11,**这是因为参数x被替换成a+1,预处理时会将SQURE(a+1)替换成a+1*a+1,根据算数优先级,结果就为11。**所以如果我们在宏定义上加两个括号,这个问题就解决了:

复制代码
#define SQUARE(x) (x)*(x)

再举一个例子:

复制代码
#define DOUBLE(x) (x)+(x)

这回加了括号,想避免之前的问题,但是这个宏在使用时可能还会出现错误,举个例子:

复制代码
#include <stdio.h>
#define DOUBLE(x) (x)+(x)
int main()
{
	int a = 5;
	printf("%d\n", 10*DOUBLE(a));
	return 0;
}

结果好像是打印100,但是实际结果是55。**这是因为10*DOUBLE(a)在预处理时替换成10*(5)+(5),结果是55,**解决办法就是在宏定义表达式两边加上一对括号就可以了。

复制代码
#define DOUBLE(x) ((x)+(x))

所以对于数值表达式进行求值的宏定义都应该用这种方式加上括号。避免临近操作符之间的不可预料的作用。

4 带有副作用的宏参数

当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,使用宏时就有可能造成不可预料的结果。副作用就是表达式求值时出现的永久性效果。

例如:

x+1;//不带副作用

x++;//带有副作用

复制代码
#include <stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n",x,y,z);
	return 0;
}

所以结果为x=6 y=10 z=9

5 宏替换的规则

在程序中#define定义符号和宏时,需要有如下几个步骤:

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

注意:

  • 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  • 当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索。

举个例子:

复制代码
#include <stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
#define M 10
int main()
{
	int x = 5;
	int z = MAX(x, MAX(2,3));
	printf("x=%d M=%d z=%d\n", x, M, z);
	return 0;
}

输出结果:

其中"M=%d"中的M不会被替换。

6 宏函数的对比

宏常常被应用于简单的运算。

比如求两个数较大的一个数,写成宏更有优势一些:

复制代码
#define MAX(a,b) ((a)>(b)?(a):(b))

举个例子:

复制代码
#include <stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
	int a = 10;
	int b = 11;
	printf("%d\n", MAX(a, b));//输出11
	return 0;
}

为什么不用函数来完成呢?有两个原因:

  • 函数在调用和返回值时需要时间,所以宏比函数在程序的简洁性和速度更胜一筹
  • 再者函数参数需要特定的类型,只能在符合这个类型的数上使用,宏参数是类型无关的,整型,长整型,短整型,浮点型等同时适用。

宏的参数可以出现类型,函数做不到:

复制代码
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
	int* ptr=MALLOC(5,int);
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	free(ptr);
	ptr = NULL;
	return 0;
}

和函数相比,宏的劣势:

  • 每次使用宏时,一份宏定义的代码将插入到程序中。可能会大幅度增加宏的长度
  • 宏是没办法调试的
  • 宏由于类型无关,所以不够严谨
  • 宏可能会带来运算符优先级的问题

总结一下宏和函数的对比:

|---------------|--------------------------------|---------------------|
| 属性 | #define定义宏 | 函数 |
| 代码长度 | 除了非常小的宏外,程序长度大幅度增长 | 函数代码只出现在那一个地方 |
| 执行速度 | 更快 | 比宏慢一些 |
| 操作符优先级 | 可能会因为操作符优先级造成不可预料的后果 | 表达式的结果更容易预测 |
| 带有副作用的宏参数 | 如果宏被多次计算,带有副作用参数求值可能会造成不可预料的结果 | 函数只在传参时求值一次,结果更容易控制 |
| 参数类型 | 可以使用任何参数类型 | 参数类型不同,就要使用不同的函数 |
| 调试 | 不方便调试 | 可以逐语句调试 |
| 递归 | 不能递归 | 可以递归 |

7 #和##

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。

写一个函数求两个数的最大值时,不同的数据类型就得写不同的函数。例如:

复制代码
int int_max(int x, int y)
{
	return x > y ? x : y;
}
float float_max(float x, float y)
{
	return x > y ? x : y;
}

这样写起来比较繁琐,如果用宏定义:

复制代码
#include <stdio.h>
#define NUM_MAX(type)      \
type type##_max (type x,type y)\
{                              \
  return (x>y)?x:y;\
}

NUM_MAX(int)
NUM_MAX(float)

int main()
{
	int m = int_max(2, 3);
	printf("%d\n", m);
	float n = float_max(3.4f, 5.4f);
	printf("%f\n", n);
	return 0;
}

这样使用宏内部的int##_max和float##_max生成int_max和float_max做函数名。

8 命名约定

一般来说函数的宏的使用语法相似,语言本身没法区分二者,所以尽可能:

  • 宏名全部大写
  • 函数名不要全大写

9 #undef

#undef用来移除一个宏定义

语法:

#undef NAME

一个名字被重新定义,旧名字就要移除

10 条件编译

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

常见的条件编译指令

#if 常量表达式

语句

#endif

//表达式为真,执行语句,否则不执行

类似的,多分支条件编译:

#if 常量表达式

//

#elif 常量表达式

//

#else

//

#endif

判断是否被定义:

#ifdef symbol 如果宏定义了symbol ,执行语句

#ifndef symbol 如果没有宏定义symbol ,执行语句

11 头文件的包含

11.1 本地文件包含

#include "filename"

查找策略:先在源文件所在根目录下查找,如果找不到,就像查找库函数头文件一样在标准位置查找头文件。

11.2 库文件包含

#include <filename.h>

查找头文件直接去标准位置查找,如果找不到就提示错误。

11.3 嵌套文件包含

一个头文件包含几次就编译几次,如果重复包含,编译的压力就比较大。

例如:

复制代码
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
	return 0;
}

如果直接这样写,test.h里的文件就会拷贝5次放在源文件中,如果工程比较大,会造成严重后果。所以引入条件编译来避免头文件重复引用:

复制代码
#ifndef __TEST_H__
#define __TEST_H__
//头文件内容
#endif

//或者
#pragma once

以上就是有关预处理的常见知识了,如果这篇文章对你有用,可以点点赞哦,你的支持就是我写下去的动力,后续会不断地分享知识。

相关推荐
Boilermaker19921 小时前
【Java EE】Mybatis-Plus
java·开发语言·java-ee
aramae1 小时前
C++ -- STL -- vector
开发语言·c++·笔记·后端·visual studio
Tony小周1 小时前
实现一个点击输入框可以弹出的数字软键盘控件 qt 5.12
开发语言·数据库·qt
lixzest2 小时前
C++ Lambda 表达式详解
服务器·开发语言·c++·算法
沉默媛2 小时前
如何安装python以及jupyter notebook
开发语言·python·jupyter
_Chipen3 小时前
C++基础问题
开发语言·c++
止观止3 小时前
JavaScript对象创建9大核心技术解析
开发语言·javascript·ecmascript
阿捏利4 小时前
C Primer Plus 第6版 编程练习——第7章(上)
c语言·编程题·c primer plus
screenCui4 小时前
macOS运行python程序遇libiomp5.dylib库冲突错误解决方案
开发语言·python·macos
linux kernel5 小时前
第七讲:C++中的string类
开发语言·c++