【C语言进阶】预处理详解

引言

对预处理的相关知识进行详细的介绍

猪巴戒个人主页✨

所属专栏《C语言进阶》

🎈跟着猪巴戒,一起学习C语言🎈

目录

引言

预定义符号

#define定义常量

#define定义宏

带有副作用的宏参数

宏替换的规则

宏函数的对比

#和##

#运算符

##运算符

命名约定

#undef

条件编译

头文件的包含

嵌套文件的包含


预处理又叫预编译,预处理是编译过程中的第一个步骤,主要是处理以#开头的预编译指令。

预定义符号

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

cs 复制代码
__FILE__    //进行编译的源文件的文件名
__LINE__    //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器崔寻ANSI C,其值为1,否则就是未定义。
cs 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __DATE__);

	return 0;
}

#define定义常量

cs 复制代码
#define name stuff

在预编译的过程中,会用stuff来代替name。

比如下面的MAX就是要被替换的name,在预处理的阶段,MAX会被替换成100。

cs 复制代码
#define MAX 100
int main()
{
	printf("%d\n", MAX);
	return 0;
}

后面的stuff不仅可以是数字,也可以是一个长句。

cs 复制代码
#define PRINT printf("hello world!\n")
int main()
{
	PRINT;
	return 0;
}

#define定义宏

#define定义宏,不仅可以完成语句的替换,还可以将参数传进去。

cs 复制代码
#define name(parament_list) stuff

parament_list表示的就是参数,有了参数的加入,stuff的表达更加丰富。

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

比如说:

cs 复制代码
#define SQUARE(x) x+x
int main()
{
	int a = 10;
	printf("%d\n", SQUARE(a));
	return 0;
}

这里的SQUARE(x)中的 x 就是参数,将参数a代替x,然后带入后面的表达式。

这里的SQUARE(a),经过替换就是 a+a.

但是这种定义宏的写法是存在瑕疵的,#define定义宏的书写一定要在各个参数上加上括号,在整体的表达式加上括号。

下面的书写方式才是正确的。

cs 复制代码
#define SQUARE(x) ((x)+(x))

接下来看两个错误的示范:

cs 复制代码
#define SQUARE(x) x*x
int main()
{
	int a = 10;
	printf("%d\n", SQUARE(a+1));
	return 0;
}

这里的本来预想的结果是11*11=121,但是预处理并不是首先将参数进行计算,而是简单地将参数进行替换,SQUARE(a+1)会被替换成a+1*a+1(10+1*10+10),所以最后地结果是21。

解决办法就是

cs 复制代码
#define SQUARE(x) (x)*(x)
cs 复制代码
#define SQUARE(x) x+x
int main()
{
	int a = 10;
	printf("%d\n", 10*SQUARE(a));
	return 0;
}

这里本来预想的结果是200,但是将参数进行替换,10*SQUARE(a)->10*a+a,结果就是110.

解决办法就是

cs 复制代码
#define SQUARE(x) (x+x)

总结:

为了避免宏定义中,参数中操作符或临近操作符之间不可预料的相互作用。我们应该毫不吝啬地在参数和参数表达式整体加上括号。

带有副作用的宏参数

刚刚讲了定义宏是什么,接下来是带有副作用的宏参数。如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

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

多次使用宏定义,那就会导致x的值发生变化,最终导致的结果就会大相径庭。

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

这里x++,y++作为参数,MAX(x++,y++)会被替换成((x++)>(y++)?(x++):(y++)),首先x++和y++比较大小,x的值会变成5,y的值会变成6,那么后面的y++表达式就会被执行,因为是前置++,先执行后++,所以结果是6,但是y的值变成了7。

宏替换的规则

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

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

注意:

宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

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

宏函数的对比

|----------|------------------------------------------------------------------------|------------------------------------|
| 属性 | #define定义宏 | 函数 |
| 代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现与一个地方,每次执行函数调用的都是同一个地方 |
| 执行速度 | 更快 | 存在函数的调用和返回函数栈帧的时间,速度会慢一些 |
| 操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候直接将参数的值传递给函数。 |
| 带有副作用的参数 | 参数可能被代替到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候对参数进行求值 |
| 参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用与任何参数类型 | 函数的参数是与类型有关的,不同的参数类型就要不同的函数,所以比较严格 |
| 调试 | 宏是不方便调试的 | 函数是可以逐语句调试 |
| 递归 | 宏是不能递归的 | 函数是可以递归的 |

#和##

#运算符

#运算符将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中。

#运算符所所执行的操作可以理解为"字符串化"。

cs 复制代码
#define PRINT(n) printf("the value of "#n"is %d",n)

之所以通过#运算符进行字符串化,是因为在字符串中,#define是不会进行替换的。

cs 复制代码
printf("the value of ""a""is %d",a);

##运算符

##运算符可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创立标识符。##被称为记号粘连。

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

float float_max(float x, float y)
{
	return x > y ? x : y;
}

由于类型的不同,这个比较大小的功能必须分为两个函数进行实现。

下面是一个定义宏,通过下面的定义宏将类型作为参数,替换函数中的类型。

其中\是换行符,type_max如果直接书写就是函数名称了,分开书写,然后用##来粘合,就可以实现替换函数名。

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

GENERIC_MAX(int)
GENERIC_MAX(float)//等同于上面两个函数的代码

命名约定

一般来说函数和宏语法很相似,所以语言本身没法帮助我们区分二者。

那我们平时的一个习惯是

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

#undef

我们直到#define name就是定义一个常量,#undef就是取消宏定义。

cs 复制代码
#define MAX 100
#undef MAX     //这里往后MAX就取消宏定义,不能直接使用MAX了

条件编译

条件编译指令有

cs 复制代码
#ifdef #if #endif #elif #else #ifndef

#if 表达式 语句: 后面加表达式,和if的意思一样,但是这个步骤是在预处理的时候完成的,如果表达式为假,就不会出现在后面的文件中。

#elif 表达式 语句:和else if的意思一样,和#if搭配使用。

#else 语句:和else的意思一样。

#endif :不同的是我们在使用完条件编译指令后,要加上#endif,表示预处理完成。

#ifdef : 意思:如果后面被定义,就执行后面的语句。

#ifndef : 意思:如果后面没被定义,就执行后面的语句。

cs 复制代码
int main()
{
    #ifdef MAX
        printf("haha\n");
    #endif
    return 0;
}
cs 复制代码
#if 常量表达式
        //...
#endif
//常量表达式由预处理器求值。
如:

#define __DEBUG__ 1
#if __DEBUG__
            //..
#endif

2.多个分⽀的条件编译
#if 常量表达式
            //...
#elif 常量表达式
            //...
#else
            //...
#endif

3.判断是否被定义
#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol

4.嵌套指令
#if defined(OS_UNIX)
        #ifdef OPTION1
            unix_version_option1();
        #endif
        #ifdef OPTION2
            unix_version_option2();
        #endif
#elif defined(OS_MSDOS)
        #ifdef OPTION2
            msdos_version_option2();
        #endif
#endif

头文件的包含

cs 复制代码
#incldue <filename>

库函数通过<>包含,会在指定的标准头文件位置查找头文件。

cs 复制代码
#include "filename"

通过""包含,会先在源文件所在目录下查找,如果头文件未找到,编译器就会像查找库函数文件的方式一样去标准的头文件位置查找。

所以库函数也可以用""来包含,但是这样查找的效率就慢,也不容易区分库文件和本地头文件。

嵌套文件的包含

#include指令会将头文件进行包含,在预处理阶段实现,但是如果是多个源文件的交互合作,那么程序会对同一个头文件多次包含,那么就会在大大增加程序包含的代码量。

所以我们通过条件编译解决头文件多次引用问题:

cs 复制代码
ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif 

//或者
#pragma once

当头文件被第一次引用时,就会定义__TEST_H__,那么在第二次打开的时候,__TEST_H__就已经被定义,因为#ifndef,头文件就不会再次包含。

相关推荐
cooldream20095 分钟前
Linux性能调优技巧
linux
_.Switch20 分钟前
Python机器学习模型的部署与维护:版本管理、监控与更新策略
开发语言·人工智能·python·算法·机器学习
QMCY_jason1 小时前
Ubuntu 安装RUST
linux·ubuntu·rust
慕雪华年1 小时前
【WSL】wsl中ubuntu无法通过useradd添加用户
linux·ubuntu·elasticsearch
苦逼IT运维1 小时前
YUM 源与 APT 源的详解及使用指南
linux·运维·ubuntu·centos·devops
长天一色1 小时前
C语言日志类库 zlog 使用指南(第五章 配置文件)
c语言·开发语言
仍有未知等待探索1 小时前
Linux 传输层UDP
linux·运维·udp
whltaoin1 小时前
【408计算机考研课程】-C语言认知
c语言·考研
一般清意味……2 小时前
快速上手C语言【上】(非常详细!!!)
c语言·开发语言
zeruns8022 小时前
如何搭建自己的域名邮箱服务器?Poste.io邮箱服务器搭建教程,Linux+Docker搭建邮件服务器的教程
linux·运维·服务器·docker·网站