c语言的常用的预处理指令和条件编译
预处理详解
最早接触预处理,还是在一篇文章看懂c语言_如何看懂c语言代码-CSDN博客中介绍了c语言中#define
定义的符号在编译阶段会被替换的行为。
这里将我所了解的预处理符号简单做个收集。预处理指令肯定不止这些,曾经遇到过的:
#pragma pack()
也是预处理指令。
详细参考《c语言深度剖析》。
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
输出这些符号可以知道代码的信息。
#include <stdio.h>
#include <windows.h>int main()
{printf("%s\n%d\n%s\n%s\n", __FILE__, __LINE__, __DATE__, __TIME__);printf("%s", __STDC__);//vs下未定义,说明vs不是严格遵循ANSI C标准return 0;
}
在某一个的Devc++5.11的输出:
D:\aDarkwanderor\_01ComputerLearn\_01C_Language_and_C++\_4CppProjectDebug\testC.c
6
Apr 25 2025
21:39:22
#define
#define 定义标识符
#define
的用法:
#define name stuff
定义阶段会将name
替换成stuff
。stuff
可以是数字,可以是关键字,还可以是语句。
如果定义的 stuff
过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define MAX 1000//为 register这个关键字,创建一个简短的名字
#define reg register//用更形象的符号来替换一种实现
#define do_forever for(;;)//在写case语句的时候自动把 break写上
#define CASE break;case // 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ )
注意staff
加;
需要谨慎,因为符号用到的地方可能是某个语句的一部分。
#define 定义宏
#define
机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list
是一个由逗号隔开的符号表,它们可能出现在stuff
中。
注意:
-
参数列表的左括号必须与
name
紧邻。 -
如果两者之间有任何空白存在,参数列表就会被解释为
stuff
的一部分。 -
对每个出现的参数和最终的宏,尽量用括号
()
括起来。
简单使用宏:
#include <stdio.h>#define MUL(x) x*x
#define MUL2(x) (x)*(x)
#define ADD(x) (x)+(x)int main() {int a = 5;printf("%d\n", MUL(a));printf("%d\n", MUL(a + 1));printf("%d\n", MUL2(a + 1));printf("%d\n", ADD(a) * 6);return 0;
}
输出:
25
11
36
35
但我们想要的预期输出很明显是{25,36,36,60}
,在MUL
和ADD
两个宏上出现了问题。
将宏的符号替换(预编译之后),并省略stdio.h
展开后的所有代码,main
函数变成这个样子:
int main() {int a = 5;printf("%d\n", a * a);printf("%d\n", a + 1 * a + 1);printf("%d\n", (a + 1) * (a + 1));printf("%d\n", a + a * 6);return 0;
}
第4行的宏替换后,因为*
的优先级高,故先计算1*a
,使得结果错误,第6行的宏也是如此。因此对每个出现的参数和最终的宏,尽量用括号()
括起来。
对#define
替换规则进行总结:
在程序中扩展#define
定义符号和宏时,需要涉及几个步骤。
-
在调用宏时,首先对参数(或源码)进行检查,看看是否包含任何由
#define
定义的符号。如果是,它们首先被替换。 -
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
-
最后,再次对结果文件进行扫描,看看它是否包含任何由
#define
定义的符号。如果是,就重复1、2。
注意:
-
宏参数和
#define
定义中可以出现其他#define
定义的符号。但是对于宏,不能出现递归。 -
当预处理器搜索
#define
定义的符号的时候,字符串常量的内容并不被搜索。
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;//不带副作用
x++;//带有副作用
这里的MAX
宏可以证明具有副作用的参数所引起的问题。
#include <stdio.h>#define MAX(a, b) ( (a) > (b) ? (a) : (b) )int main() {int x = 5,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
宏和函数的对比
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个:
#define MAX(a, b) ((a)>(b)?(a):(b))
不用函数来完成这个任务的原因有二:
-
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多(就是函数费时,宏省时)。所以宏比函数在程序的规模和速度方面更胜一筹。
-
更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于>
来比较的类型。即宏是类型无关的。
此外,宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
宏的缺点:和函数相比宏也有劣势的地方:
-
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
-
宏是没法调试的。
-
宏由于类型无关,也就不够严谨。
-
宏可能会带来运算符优先级的问题,导致程容易出现错。
-
宏无法实现递归。
-
参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。
用一张表列出二者的差别:
对比项 | #define 定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
#define命名约定和#undef移除宏
函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:
-
把宏名全部大写。
-
函数名不要全部大写。
此外如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。可以用#undef
移除宏。
#define NAME stuff
//...
#undef NAME
# 和 ## 参数插入字符串
字符串的自动连接
案例:
#include <stdio.h>int main() {char* p = "hello ""world\n";char* p2 = "hello"\" wor"\"ld\n";printf("hello"" world\n");printf("%s", p);printf("%s", p2);return 0;
}
输出:
hello world
hello world
hello world
这说明,字符串是有自动连接的特点的。
因此可以利用这个特点继续实现表现更丰富的宏。
#宏参数
在这之前先介绍#宏参数
,这里的#宏参数
是指把宏参数转换成字符串。
例如:
#include <stdio.h>#define STR(x) #xint main() {printf(STR(aasfsdgsg));return 0;
}
输出:
aasfsdgsg
因此利用#宏参数
和字符串的自动连接特性,完善的宏如下:
#include <stdio.h>#define PRINT(FORMAT, VALUE) \printf("the value is "FORMAT"\n", VALUE)#define PRINT2(FORMAT, VALUE) \printf("the value of " #VALUE " is "FORMAT "\n", VALUE);int main() {int i = 10;PRINT("%d", 10);//利用字符串的自动连接特性PRINT2("%d", i + 3);//利用#宏参数和字符串的自动连接特性return 0;
}
输出:
the value is 10
the value of i + 3 is 13
##
可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
例如:
#include <stdio.h>#define CAT(x,y) x##yint main() {int a = 2025;int b = CAT(a, -3);//将a和-3连接在一起变成a-3printf("%d", b);return 0;
}
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
命令行是一种通过输入文本命令来与计算机系统(特别是操作系统)进行交互的界面,它与图形用户界面(GUI)相对应,具有高效、灵活和强大的特点,在系统管理、软件开发等领域应用广泛。
c语言代码通过编译最终变成的可执行程序,只要满足条件是可以直接运行在操作系统上的。从代码到可执行程序,在一些编译器可以通过输入命令干涉编译过程。
当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。
举个例子,假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。
例如这个代码:
#include <stdio.h>
int main()
{int array[ARRAY_SIZE];int i = 0;for (i = 0; i < ARRAY_SIZE; i++){array[i] = i;}for (i = 0; i < ARRAY_SIZE; i++){printf("%d ", array[i]);}printf("\n");return 0;
}
在封装严密的 IDE 比如Devc++、vs2019等,这个代码可能直接报错。
但如果是在vscode,则可以通过命令行在编译阶段指定ARRAY_SIZE
变成我们想要的数值,从而生成不同功能的程序。
例如通过命令gcc -D ARRAY_SIZE=10 testc.c
可以指定ARRAY_SIZE
变成指定数值10,然后生成可执行程序 a.exe 。再输入.\a.exe
即可执行它。
也可在原命令的基础上加-o ProgramName.exe
来指定生成的可执行程序的程序名。这里让生成的可执行程序名和c语言的代码名保持一致。
条件编译
在编译一个程序的时候若要将一条语句(一组语句)编译或者放弃参与编译,可通过条件
编译指令实现。
这些调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
常见的条件编译指令如下。
#if和#endif
#if 常量表达式//...
#endif
常量表达式由预处理器求值。
例如:
#include <stdio.h>
int main()
{
#if 0printf("asdfghjkl\n");
#endifprintf("qwertyyiop");return 0;
}
输出:
qwertyyiop
在模块化编程中的某个头文件用#define
定义某个符号,这个符号也能通过#if
进行条件编译。
例如,这里将#define
和c语言代码放在一起:
#include <stdio.h>#define ABC 1int main()
{
#if ABCprintf("asdfghjkl\n");
#endifprintf("qwertyyiop");return 0;
}
输出:
asdfghjkl
qwertyyiop
多分支条件编译#if、#elif、#else和#endif
#if
实现多分支编译的用法:
#if 常量表达式1//...
#elif 常量表达式2//...
#else//...
#endif
其中#elif
不可放在#else
之后,一般#else
是作为所有常量表达式都不满足的情况下,作为最后的选择。
例如:
#include <stdio.h>#define A 1
#define B 2
#define C 3void f1() {
#if A==1&&B!=2printf("1\n");
#elif A==1&&B==2printf("2\n");
#endif
}void f2() {
#if A==1&&B!=2printf("1\n");
#elseprintf("2\n");
#endif
}void f3() {
#if A==1&&B!=2printf("1\n");
#elif A==1&&B==2&&C!=3printf("2\n");
#elseprintf("3\n");
#endif
}int main()
{//f1();//f2();f3();printf("six");return 0;
}
#ifdef和#ifndef 判断某符号是否定义
若定义了某个符号就执行相关语句:
#if defined(symbol)
//...
#endif#ifdef symbol
//...
#endif
若没定义某个符号就执行相关与:
#if !defined(symbol)
//...
#endif#ifndef symbol
//...
#endif
其中symbol
表示某一标识符,这个标识符通常由#define
预定义。
一般更喜欢用#ifdef
和#ifndef
,因为可以少敲一个单词。
例如,#ifdef
的用法:
#include<stdio.h>#define A 1int main()
{
#ifdef Aprintf("A\n");
#endif
#ifdef Bprintf("B\n");
#endifprintf("six");return 0;
}
#ifndef
:
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS 1
#endif#include <stdio.h>int main()
{int a;scanf("%d", &a);//在vs定义了_CRT_SECURE_NO_WARNINGS,才能正常使用scanf和其他别的函数printf("%d", a);return 0;
}
一般#ifndef
和#ifdef
通常用于避免头文件重复包含。
避免头文件包含还可以在头文件开头加这样一句:
#pragma once