C语言 预处理详解

目录

1.预定义符号

2.#define

[2.1#define 定义标识符](#define 定义标识符)

[2.2#define 定义宏](#define 定义宏)

[2.3#define 替换规则](#define 替换规则)

2.4#和##

[2.4.1# 的作用](# 的作用)

[2.4.2## 的作用](## 的作用)

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

2.6宏和函数的对比

对比

**2.7内联函数

2.8命名约定

3.#undef

**4.命令行定义

5.条件编译

常见的条件编译指令

6.头文件包含

6.1头文件被包含的方式

6.2嵌套文件的包含


1.预定义符号

FILE //进行编译的源文件

LINE //文件当前的行号

DATE //文件被编译的日期

TIME //文件被编译的时间

STDC //如果编译器遵循ANSI C,其值为1,否则未定义

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

举个例子:

2.#define

#define是一种预处理指令,他有两种用法:

  1. #define 定义常量(标识符)
  2. #define 定义宏

2.1#define 定义标识符

语法:

#define name stuff

举个例子:

#define 是完全替换,比如

所以在定义的时候,为了强调他是一个整体,需要自己带上括号:

注意 :由于是完全替换,在define定义标识符的时候,不要在最后加 ; 否则替换的时候会将 ; 也替换过去,会导致语法错误

2.2#define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常会被解释为宏(macro)或定义宏(define macro)

下面是宏的声明方式:

#define name( parament-list ) stuff

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

注意:

参数列表的左括号必须与name紧邻

如果两者之间有任何空白存在,参数列表就会被释解释为stuff的一部分

如:

#define定义宏也是完全替换,比如:

为了防止出现失误,我们在声明的时候需要加上括号:

我们在写宏的时候,如果逻辑需要,我们可以加上足够多的括号来使宏变得完整

2.3#define 替换规则

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

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

注意:

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

2.4#和##

2.4.1# 的作用

如何把参数插入到字符串中?、

我们发现字符串是有自动连接的特点的

假设有这样的代码:

我们如何用宏来实现printf的功能呢,这里我们使用#

他的替换是周怎么完成的呢

这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中

使用#,把一个宏参数变成对应的字符串

比如:代码中的**#N** 会被预处理器处理为:"N"

所以**"#N"** 即被处理为**""N""**

2.4.2## 的作用

##可以把位于他两边的符号合成一个符号

他允许宏定义从分离的文本片段创建标识符

注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的

2.5 带有副作用的宏参数

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

x+1;//不带副作用

x++;//带副作用

MAX宏可以证明具有副作用的参数所引起的问题

这段代码输出的结果是什么?

这里我们得知道预处理之后的结果是什么:

这段代码是证明执行的呢?

2.6宏和函数的对比

宏通常被应用于执行简单的运算

比如在两个数中找出较大的一个

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

那为什么不用函数来完成这个任务?

原因有二:

  1. 用于调用函数和从函数返回的代码可能实际执行这个小型计算工作所需要的时间更多
    所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型
    所以函数只能在类型合适的表达式上使用
    宏是类型无关的

**宏的缺点:**当然和函数相比,宏也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是没法调试的
  3. 宏由于类型无关,也就不够严谨
  4. 宏可能会带来运算符优先级的问题,导致过程容易出现错误

宏有时候可以做函数做不到的事情,比如:宏的参数可以出现类型,但是函数做不到

对比

建议:

如果逻辑比较简单,可以使用宏来实现

如果计算逻辑比较负责,那么就使用函数实现

**2.7内联函数

C99之后,C++引入了内联函数的概念 inline关键字

内联函数具有函数和宏的双重优点:

  1. 内联函数是函数
  2. 内联函数又像宏一样,在调用的地方展开

2.8命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者

那我们平时的一个习惯是:

  • 把宏名全部大写 //MAX
  • 函数名不要全部大写 //Max

3.#undef

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

**4.命令行定义

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

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处

(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一 个机器内存大写,我们需要一个数组能够大写。)

5.条件编译

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

条件编译就是:满足条件就编译,不满足条件就不编译

比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译

常见的条件编译指令

1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值
如:
#define DEBUG 1
#if DEBUG
//..
#endif

表达式为真则编译,为假则不编译

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

只会选择以一个#if或者#elif执行

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

判断(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

注意:#if 与 #endif 是配套使用的,同时出现,同时消失

6.头文件包含

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

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换

这样一个源文件被包含10次,那就实际被编译10次

6.1头文件被包含的方式

头文件的包含一般有两种方式:

1.包含本地文件(自己的.h文件)

#include "xxx.h"(用双引号)

2.包含标准库中的文件

#include <xxx.h> (用尖括号)

查找策略:

先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件

#include包括""和<>这两种情况

""是在用户工作目录下寻找(用户的工作目录是通过编译器指定的)
<>是找系统标准库函数,通过系统环境变量指定系统库目录

如果找不到就提示编译错误

6.2嵌套文件的包含

如果出现这样的场景

comm.h和comm.c是公共模块

test1.h和test1.c使用了公共模块

test2.h和test2.c使用了公共模块

test.h和test.c使用了test1模块和test2模块。

这样最终程序中就会出现两份comm.h 的内容

这样就造成了文件内容的重复

我们可以用条件编译解决这个问题

每个头文件的开头写:

#ifndef TEST_H
#define TEST_H
//头文件的内容
#endif //TEST_H

或者

#pragma once

就可以避免头文件的重复引入

相关推荐
万物得其道者成11 分钟前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
学步_技术16 分钟前
Python编码系列—Python抽象工厂模式:构建复杂对象家族的蓝图
开发语言·python·抽象工厂模式
wn53140 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
Hello-Mr.Wang1 小时前
vue3中开发引导页的方法
开发语言·前端·javascript
救救孩子把1 小时前
Java基础之IO流
java·开发语言
WG_171 小时前
C++多态
开发语言·c++·面试
宇卿.1 小时前
Java键盘输入语句
java·开发语言
Amo Xiang1 小时前
2024 Python3.10 系统入门+进阶(十五):文件及目录操作
开发语言·python
friklogff1 小时前
【C#生态园】提升C#开发效率:深入了解自然语言处理库与工具
开发语言·c#·区块链
重生之我在20年代敲代码3 小时前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记