全文目录
- 前言
- 预定义符号
- `#define` 定义标识符常量
- `#define` 定义宏
- `#define` 替换规则
- `#` 宏参数转换字符串
- `##` 宏参数拼接
- 带有副作用的宏参数
- 宏与函数的对比
- `#undef` 移出宏定义
- 命令行定义
- 条件编译
- `#include` 文件包含
- 头文件的包含方式
- 头文件的重复包含
前言
前面我们学习了程序的编译和链接的大致流程,其中编译、汇编、链接太过深奥,只需要了解流程即可。但是预编译中的文本操作需要深入了解一下。
预定义符号
C语言中有内置的预定义符号包括main
、关键字、库函数等。除了这些还有一些日志信息:
__func__ //当前调用的函数
__FILE__ //进行编译的源文件(绝对路径)
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些符号可以很好的帮我们输出当前程序的日志信息,便于日后维护代码。
// demo
printf("file:%s\tline:%d \tdate:%s\ttime:%s\n", __FILE__, __LINE__, __DATE__, __TIME__);
#define
定义标识符常量
语法:
#define name stuff
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外
// 每行的后面都加一个反斜杠(续行符)。
在预编译阶段就会将文件中的全部name
替换成 stuff
。
#define
结尾可以加上;
,加上语句就是多了一条空语句,但是一般是不加上;
,容易引发语法错误。
比如上面日志信息的输出就可以使用 #define
定义一个表示符,节省代码量(而且看起来很牛的样子)。
// demo#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ )
DEBUG_PRINT;
在VS2019中,可以通过以下设置将生成预处理的文件,但是设置后就不能运行程序了,在看完预处理文件后需要重置该设置
打开预处理的文件就是以下内容:
printf("file:%s\tline:%d\t date:%s\ttime:%s\n" , "D:\\code\\daily_code\\FlexibleArray\\FlexibleArray\\test.c",13 , "Apr 14 2023","09:13:00" );
#define
定义宏
#define
机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro
)或 定义宏(define macro
)。
定义语法:
#define name( parament-list ) stuff
//其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与
name
紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff
的一部分。
宏的语法和函数的语法十分相似,所以在定义宏和函数的时候尽量做到命名约定:
把宏名全部大写
函数名不要全部大写
//demo
#define MUL(a, b) a * b
int a = 5;
int b = 15;
int c = MUL(a, b); // 75
这样看来宏是很简单的,那么再来看一组实例
//demo
#define MUL(a, b) a * b
int a = 5;
int b = 15;
int c = MUL(a + 5, b + 5); // 200 ? 85!
这是因为宏是不加任何处理,直接将参数替换成对应的值,运算符的优先级就会导致结果与预期的结果不一致。所以需要为宏的每一位参数加上括号
// 修正
#define MUL(a, b) (a) * (b)
再来看一组示例:
#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
看上去,好像打印100,但事实上打印的是55.
解决方法:将宏整体加上括号
// 修正
#define MUL(a, b) ((a) * (b))
总结:
用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
#define
替换规则
在程序中扩展#define
定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由
#define
定义的符号。如果是,它们首先被替换。 - 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由
#define
定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和
#define
定义中可以出现其他#define
定义的符号。但是对于宏,不能出现递归。 - 当预处理器搜索
#define
定义的符号的时候,字符串常量的内容并不被搜索。
#
宏参数转换字符串
#
的作用:
把一个宏参数变成对应的字符串。
如果想在宏中让参数不被对应的值替换,直接使用参数名时,就可以使用 #
:
//demo
#define PRINT(n) printf(#n"'s value is %d\n", n)
int a = 10;
int b = 20;
PRINT(a); // a's value is 10
PRINT(b); // b's value is 20
如果想让宏能够打印不同类型的值,可以将宏的参数类型作为字符串进行打印
#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE);PRINT("%d", 10);
PRINT("%lf", 3.100);
##
宏参数拼接
##
可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
简单来说就是将两边的宏参数作为字符串拼接起来:
//demo
#define CAT(girl, friend) girl##friend
int grilfriend = 18;
printf("%d\n", CAT(girl, friend)); // 18
带有副作用的宏参数
所谓的带有副作用的参数:表达式求值的时候出现的永久性效果。
x+1;//不带副作用
x++;//带有副作用
MAX
宏可以证明具有副作用的参数所引起的问题。
//demo
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
对于预处理之后的表达式:
z = ( (x++) > (y++) ? (x++) : (y++));
可以很容易得到输出结果:
x=6 y=10 z=9
所以我们在使用宏时,对于参数的选择需要小心谨慎
宏与函数的对比
宏因为是文本替换,所以适合用来做一些小型计算,一个MAX
就可以看出宏的优点:
#define MAX(a, b) ((a)>(b)?(a):(b))
宏的优点:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
宏是类型无关的。 并且宏的参数可以出现类型
//demo
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
既然有优点就一定会有缺点,宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题和参数的副作用,导致程容易出现错。
总结对比:
#undef
移出宏定义
如果需要重定义一个宏,则需要用到#undef
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除,否则会报警告
命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
但是在一般的集成开发环境中这个好像不能不能使用,在Linux的gcc中可以使用,只需要在编译的时候加上 -D
选项即可。
#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;
}
编译指令:
gcc -D ARRAY_SIZE=10 test.c
条件编译
条件编译的使用方法跟if else
语句相似,不同的是条件编译时由预处理器求值的,所以只能使用一些常量。
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif3.判断是否被定义
#if defined(symbol) // 是
#if !defined(symbol) // 否#ifdef symbol // 是
#ifndef symbol // 否4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
这一点在库文件中十分常见,对于跨平台的文件有着很强的实用性。
#include
文件包含
在编码过程的第一步通常是#include <stdio.h>
这样的文件包含,使得stdio.h
参与编译,在之前的预编译已经知道了,其实就是头文件的内容替换,预处理器先删除这条指令,并用包含文件的内容替换。如果一个文件被包含10次,就会替换10次。
头文件的包含方式
在编码中有两种包含头文件的方式:
一种是本地文件包含:
#include "test.h"
该方式的查找策略:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。
如果找不到就提示编译错误。
一种是库文件的包含:
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
库文件的包含当然也可以使用本地文件被包含方式,但是效率会更低一些,所以还是遵守规则好些。
头文件的重复包含
在正常的编码中肯定是很少出现头文件的重复包含,但是很多情况是在我们不经意间发生的, 如:
如果不加处理test.c
中就包含了两份comm.h
。虽然没什么大问题,但是每一次重复包含都会造成代码冗余。
处理方法:
每个头文件的开头写:
1.
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__2.
#pragma once