【C语言】预处理详解

1、预定义符号


在C语言中有预定义符号,可直接使用

复制代码
__FILE__    //进行编译的源文件
__LINE__    //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSIC,其值为1,否则未定义

举个例子

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main()
{
	printf("%s\n", __FILE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%d\n", __LINE__);
	//printf("%d\n", __STDC__);  //VS编译器不支持
	return 0;
}

2、#define定义常量

基本语法:#define name stuff

举个例子:

复制代码
第一条:
#define MAX 1000
定义后:MAX = 1000

第二条:
#define reg register //为 register这个关键字,创建⼀个简短的名字
定义后:reg = register

第三条:
#define do_forever for(;;) //用更形象的符号来替换⼀种实现
定义后:

第四条:
#define CASE break;case //在写case语句的时候⾃动把 break写上。

例如:
#define DEBUG_PRINT printf("FILE:%s\t line:%d\t date:%s\t time:%s\n",__FILE__,__LINE__,__DATE__,__TIME__)
//如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加⼀个反斜杠(续行符)

可以写成:
#define DEBUG_PRINT printf("FILE:%s\t line:%d\t \
date:%s\t time:%s\n",\
__FILE__,__LINE__,\
__DATE__,__TIME__)

关于第三条for(;;)

  • 初始化部分,调整部分,判断部分都可以省略
  • 省略判断 部分,就造成条件恒为真,造成死循环

如果在define后面加上分号,会发生什么问题?

C 复制代码
#define MAX1 1000;
#define MAX2 1000

建议不要加上分号,这样容易导致问题:

C 复制代码
#include<stdio.h>

int main()
{
	//错误写法:
	//int a = MAX1;  //MAX1 = 1000;
	//会当成:int a = 1000;;(有两个分号)
	
	//错误写法:
	//printf("%d\n",MAX1);
	//会当成:printf("%d\n",1000;);
	
	return 0;
}

3、#define定义宏


#define机制包括了一个规定,允许吧参数替换到文本中,这种实现通常称为宏,下面是宏的申明方式:

复制代码
#define name( parament-list ) stuff

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

  • 参数列表的左括号必须与name紧挨着,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

3.1正常例子:

C 复制代码
#define S(x) x*x

这个宏接受一个参数x,如果在上述声明之后,把S(5);置于程序中,预处理器就会在下面这个表达式替换上面的表达式:5 * 5

在vscode中编译过程中,显示如下所示:

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define Sub(x) 5*5

int main()
{
	int n = 5;
	//Sub(x) = x*x;
	int num = 5*5;
	printf("%d\n", num);

	return 0;
}

3.2错误例子

如果还是同一段代码,把其中宏为

C 复制代码
#define Sub(x) x*x

代码段为

C 复制代码
int n = 5;
int num = Sub(n + 1);

你可能认为打印36,事实上打印11。这是因为替换文本时,参数x被替换成n + 1,所以语句事实上变成了:

C 复制代码
int n = 5;
int num = n + 1 * n + 1;

由于符号的优先级,先执行1 * n,后面再相加求值。

在宏定义上加上两个括号,这个问题便轻松的解决了:

C 复制代码
#define S(x) (x)*(x)

这样预处理之后产生的效果:

C 复制代码
int n = 5;
int num = (a + 1) * (a + 1);

3.3错误例子2

这里还有一个宏定义:

C 复制代码
#define S(x) (x)*(x)

定义中使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误:

C 复制代码
int n = 5;
int num = 10 * Sub(n);

原本我们想让编译器打印100,但实际上打印的是55

替换之后:

C 复制代码
int n = 5;
int num = 10 * (5) + (5);

由于运算符的优先级,所以出现了55.

遇到这个问题,在宏定义表达式两边加上一对括号就可以了。

C 复制代码
#define S(x) ((x) + (x))

提示:

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

4、带有副作用的宏参数

当宏函数在宏的定义中出现超过一次的时候,如果参数带有副作用 ,那么你在使用这和宏的时候就可能额出现危险,导致不可预测的后果。副作用 --> 永久性效果。

例如:

C 复制代码
 x + 1; //不带副作用
 x++;   //带有副作用

4.1正常例子:

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define size(x, y) (x > y) ? x : y

int main()
{
	int a = 3;
	int b = 5;
	int num = size(a, b);
	
	printf("num = %d\n", num);
	printf("a = %d\n", a);
	printf("b = %d\n", b);
	
	return 0;
}

打印出的结果为:

复制代码
num = 5
a = 3
b = 5

在vscode编译器test.i文件中,代码是这样表示的:

C 复制代码
int num = (a > b) ? a : b;

4.2错误例子

若把代码段换成

C 复制代码
int num = size(a++, b++);

在vscode中的test.i会显示:

C 复制代码
int num = (a++ > b++) ? a++ : b++;
  1. a会先与b进行比较,b大于a,返回b,然后a++和b++

  2. 后面返回num = b,此时b = 6

  3. 然后执行后面条件成立的b++,此时b = 7
    打印后:

    num = 6
    a = 4;
    b = 7;

5、宏替换的规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果时,它们首先被替换。
  2. 替换我那本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果时,就重复上述处理过程。
    注意:
  4. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出翔递归
  5. 当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索。

6、宏函数的对比

在实际运算中,宏比函数更有优势些,我们拿这段代码为例子:

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX(a, b) (a > b ? a : b)

int Max(int x, int y)
{
	int z = (x > y ? x : y);
	return z;
}

int main()
{
	int a = 3, b = 5;
	//宏
	int c1 = MAX(a, b);
	//函数
	int c2 = Max(a, b);
	
	printf("%d\n", c1);
	printf("%d\n", c2);

	return 0;
}

原因如下:

  • 函数执行的行数:调用函数11行,执行运算7~8行,函数返回6行
  • 宏执行的行数:执行运算7~8行

宏的优势:

  1. 可见宏执行的行数比函数少了调用函数和函数返回,这让宏在执行时速度更快。
  2. 函数的参数必须声明特定的类型,而宏可以适用不同类型。

宏的劣势:

  1. 每次使用宏的时候,如果宏的代码比较长,可能大幅度增加程序长度。
  2. 宏没法调试观察(因为编译器会替换文本)
  3. 由于宏与类型无关,就不够严谨
  4. 可能带来运算符优先级问题,导致程序容易出现

宏有的可以做到的,函数做不到。比如:宏的参数可以是类型

C 复制代码
#include<stdio.h>
#define malloc(num, type)/
	(type)malloc(num * sizeof(type))
	
int main()
{
	//使用
	malloc(10, int);
	//预处理器替换之后
	(int*)malloc(10 * sizeof(int));
	
	return 0;
}

宏和函数的对比:

属性 #define宏定义 函数
代码长度 多次插入,程序易变长 仅一处定义,多次调用同一份代码
执行速度 更快 因调用/返回开销,相对较慢
操作符优先级 易受上下文操作符优先级影响,建议多写括号 参数求值一次,表达式结果易预测
带副作用参数 参数多次计算易产生不可预料结果 参数仅传参时求值一次,易控制
参数类型 与类型无关,通用 与类型强相关,不同类型需不同函数(任务相同也可能)
调试 不方便 可逐语句调试
递归 不支持 支持

7、###

#运算符

在 C 语言的宏定义里,有一个很实用的#运算符,咱们可以把它叫做字符串化运算符。它的作用很明确,就是把宏的参数转换成字符串字面量,而且只能用在带参数的宏的替换列表里。

咱们举个特别简单的例子来看看。

假设现在有个变量 int num = 20;,我们想打印出 num的值是20 这样的内容。

这时候就可以用#运算符来定义一个宏:

c 复制代码
#define SHOW(n) printf(""#n" 的值是 %d\n", n);

然后咱们调用这个宏:

c 复制代码
SHOW(num);

那这个宏在预处理的时候会变成什么样呢?它会被处理成:

c 复制代码
printf(""num" 的值是 %d\n", num);

当程序运行的时候,就会在屏幕上打印出:

plaintext 复制代码
num 的值是 20

是不是很简单?总结一下,#运算符就像一个 "转换器",把宏的参数变成对应的字符串,这样在打印变量名和变量值的时候就特别方便,不用手动去写字符串了,既节省时间又能减少出错的可能。

##运算符

在 C 语言宏定义里,##运算符可以把它两边的符号合成一个新的标识符 ,也叫记号粘合。但要注意,粘合后的必须是合法标识符,否则结果不确定。

咱们用一个超简单的例子来说明:

比如我们需要为不同数据类型写 "求最大值" 的函数,像这样分开写就很麻烦:

c 复制代码
// 整型求最大值函数
int int_max(int x, int y) {
    return x>y?x:y;
}

// 浮点型求最大值函数
float float_max(float x, float y) {
    return x>y?x:y;
}

这时候##就派上用场了!我们可以定义一个宏来自动生成这些函数:

c 复制代码
#define GENERIC_MAX(type) \
type type##_max(type x, type y) \
{ \
    return (x>y?x:y); \
}

然后调用这个宏,生成不同类型的函数:

c 复制代码
// 生成int_max函数
GENERIC_MAX(int)
// 生成float_max函数
GENERIC_MAX(float)

int main() {
    // 调用整型最大值函数
    int m = int_max(2, 3);
    printf("%d\n", m);
    // 调用浮点型最大值函数
    float fm = float_max(3.5f, 4.5f);
    printf("%f\n", fm);
    return 0;
}

运行后会输出:

plaintext 复制代码
3
4.500000

简单来说,##就像 "胶水",把type_max粘在一起,自动生成了int_maxfloat_max这样的函数名,帮我们减少了重复代码

8、命名约定

  • 宏一般全为大写
  • 函数为开头大写

9、取消宏定义

**当我们定义宏想取消时,就要用到#undef

例如:

C 复制代码
#include<stdio.h>
#define MAX 100
int main()
{
	printf("%d\n", MAX);
	#undef MAX
	printf("%d\n", MAX);//程序报错,宏已被取消
	
	return 0;
}

命令行定义

宏具有很强的灵活性:

  1. 不用宏定义
C 复制代码
int arr[20]; //则数组大小被写死了
  1. 用宏定义
C 复制代码
#include<stdio.h>
#define NUM 20

int main()
{
	arr[NUM];//只需要修改宏就可以改变数组大小
}
  • 对于宏来说,可以让代码更加灵活,实用性更高

条件编译

在编译一个程序时,我们不想把这一条语句删掉,可以选择行编译

单条件

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
#if 0
    printf("%d", MAX);
#endif

    return 0;
}
  • 因为if的后面为0,因为0为假,不符合条件
  • 此时的MAX打印不了

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
#if MAX > 0 //满足条件,则打印
    printf("%d", MAX);
#endif

    return 0;
}
  • 此时if后为MAX > 0,满足宏定义的条件
  • 此时的MAX可打印

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
#if MAX < 0 //不符合条件
    printf("%d", MAX);
#endif

    return 0;
}
  • 此时if后面MAX < 0,不满足宏定义的条件
  • 此时的MAX不打印

多条件

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
//条件不满足,则不打印
#if MAX == 50
    printf("%d", MAX);

//条件满足,打印
#elif MAX == 100
    printf("%d", MAX);

#endif

    return 0;
}
  • 当多有编译条件时,则下一个条件些成elif,与if语句相似
  • 因为elif后满足宏定义的条件,则执行语句

只定义就满足条件

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
#if defined(MAX)
   printf("hehe\n");
#endif
   return 0;
}

注意:

  • 写的defined是define的过去式
  • 只要定义了就满足打印条件
    以下是另外一种写法:
C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
#ifdef MAX
   printf("hehe\n");
#endif
   return 0;
}

注意:

  • 不能对MAX加上括号
  • 与上面的形式形同
C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
//defined前面加上了!
//只要没有宏定义的MAX则打印
#if !defined(MAX)
    printf("hehe\n");
#endif
    return 0;
}
  • defined前面可以加上!,表示为假(相反)
    以下是另外一种写法:
C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100

int main()
{
#ifndef MAX
    printf("hehe\n");
#endif
    return 0;
}
  • 此时idndef中的n代表否定,与!类似

嵌套指令

C 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX 100
#define A 1
#define B 2

int main()
{
#if defined MAX(100)
	#ifdef A
		printf("A");
	#endif

	#elif B
		printf("B");
	#endif
#endif
}

头文件的包含

头文件被包含的方式

本地文件包含

C 复制代码
#include "game"

查找策略:先在源文件所在的目录下查找,如果该头文件未找到,编译器就会到标准库找头文件。

如果找不到就提示编译错误。
Linux汉奸的标准头文件的路径:

复制代码
/usr/include

VS环境的标准头文件的路径:

\ 复制代码
//这是VS2013的默认路径
C:/program Files (x86)/Microsoft Visual Studio 12.0/VC/include

注意按照直接的按照路径去找

库文件包含

C 复制代码
#include<filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误

  • 如果在使用标准头文件时用" "来查找,但是效率会低
  • 不容易区分时库文件还是本地文件

嵌套文件包含

我们已经知道,#define指令可以使另外一个文件被编译。就像它实际出现于#define指令的

地方⼀样。

这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。

一个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大

C 复制代码
//test.c
#include "tset.h"
#include "tset.h"
#include "tset.h"
#include "tset.h"
#include "tset.h"

int mian()
{
	 return 0;
}
C 复制代码
//test.h
void test();
struct Stu
{
	int id;
	char name[20];
};
  • test.h文件将会拷贝5次
  • 预处理代码量增大
    如果我们要控制头文件的量就要每个文件开头写:
C 复制代码
#ifndef __TESET_H__
#define __TEST_H__
//头文件内容
#endif //__TEST_H__

或者

C 复制代码
#pragma once

这样就避免头文件的重复引入。

相关推荐
合作小小程序员小小店5 小时前
web网页开发,在线%考试,教资,题库%系统demo,基于vue,html,css,python,flask,随机分配,多角色,前后端分离,mysql数据库
前端·vue.js·后端·前端框架·flask
顾漂亮5 小时前
Redis深度探索
java·redis·后端·spring·缓存
努力也学不会java5 小时前
【Spring】Spring事务和事务传播机制
java·开发语言·人工智能·spring boot·后端·spring
小立爱学习5 小时前
Linux 内存 --- get_user_pages/pin_user_pages函数
linux·c语言
小姐姐味道5 小时前
Claude Skills:被过度吹嘘的的概念翻新!
后端·github·claude
新青年5795 小时前
Go语言项目打包上线流程
开发语言·后端·golang
陈随易5 小时前
PaddleOCR-VL可太强了,图片识别转文字的巅峰之作
前端·后端·程序员
Ray665 小时前
Delete vs Truncate vs Drop
后端