C 语言编程 — 宏定义与预处理器指令
目录
文章目录
- 目录
- 前文列表
- 宏
- 预处理器
- 预处理器指令
- 预处理器指令示例
- 预处理器指令运算符
- 宏定义
- 简单宏定义
- 带参数的宏定义
- 符号吞噬问题
- 使用 do{}while(0) 结构
- 预定义的宏
- 常用的宏定义
- 总结
前文列表
《程序编译流程与 GCC 编译器》
《C 语言编程 — 基本语法》
《C 语言编程 — 基本数据类型》
《C 语言编程 — 变量与常量》
《C 语言编程 — 运算符》
《C 语言编程 — 逻辑控制语句》
《C 语言编程 — 函数》
《C 语言编程 — 高级数据类型 — 指针》
《C 语言编程 — 高级数据类型 — 数组》
《C 语言编程 — 高级数据类型 — 字符串》
《C 语言编程 — 高级数据类型 — 枚举》
《C 语言编程 — 高级数据类型 — 结构体与位域》
《C 语言编程 — 高级数据类型 — 共用体》
《C 语言编程 — 高级数据类型 — void 类型》
《C 语言编程 — 数据类型的别名》
《C 语言编程 — 数据类型转换》
宏
C 语言中,宏的本质是预处理器指令。它用来将一个标识符(宏名)定义为一个字符串,被定义的字符串称为替换文本。程序在预编译阶段,所有的宏名都会被定义的字符串替换,这便是宏替换。它的功能非常强大,甚至自成一门语言,有兴趣的可以参看宏编程。宏定义通常被用来简化代码的实现,让代码的逻辑更加清晰。
宏的工作原理是定义一些参数,将这些参数复制到特定的格式(宏定义)中,通过修改宏定义(e.g. 以 #define
为开头的代码片段)或者参数,宏可以生成我们想要的代码。
预处理器
C 预处理器(C Preprocessor)简写为 CPP,又称预编译器,它并不是 C 编译器的组成部分,但是它是编译过程中一个单独的步骤。本质上,C 预处理器不过是一个文本替换工具而已,它们会指示编译器在实际的编译工作之前完成所需的预处理准备。
预处理器指令
C 语言中,所有的预处理器指令都是以 #
开头的。它必须是第一个非空字符,通常位于源文件首部。下面列出了所有重要的预处理器指令:
预处理器指令示例
- 这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 替换为 20。通常用于定义常量。
#define MAX_ARRAY_LENGTH 20
- 这些指令告诉 CPP 从系统库目录中获取头文件 stdio.h,并添加文本到当前的源文件中。
#include <stdio.h>
- 这些指令告诉 CPP 从本地目录中获取头文件 myheader.h,并添加内容到当前的源文件中。
#include "myheader.h"
- 这个指令告诉 CPP 取消已定义的 FILE_SIZE,并重新定义它为 42。
#undef FILE_SIZE
#define FILE_SIZE 42
- 这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。
#ifndef MESSAGE#define MESSAGE "You wish!"
#endif
- 这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果向 gcc 编译器传递了 -DDEBUG 开关选型,这个指令就非常有用。它定义了 DEBUG,可以在编译期间随时开启或关闭。
#ifdef DEBUG/* Your debugging statements here */
#endif
- 指令
#pragma pack(n)
用于设定结构体、联合以及类成员变量以 n 字节方式对齐。
#pragma pack(push) // 保存对齐状态
#pragma pack(4) // 设定为 4 字节对齐struct test
{char m1;double m4;int m3;
};#pragma pack(pop) // 恢复对齐状态
预处理器指令运算符
CPP 提供了下列的运算符来帮助进行宏定义:
- 宏延续运算符:一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符
\
。例如:
#define message_for(a, b) printf(#a " and " #b ": We love you!\n")// or#define message_for(a, b) \printf(#a " and " #b ": We love you!\n")
- 字符串常量化运算符:在宏定义中,当需要把一个宏的参数转换为 “字符串常量” 时,则使用字符串常量化运算符
#
。例如:
#include <stdio.h>#define message_for(a, b) \printf(#a " and " #b ": We love you!\n")int main(void) {message_for(Carole, Debra);return 0;
}
运行:
$ ./main
Carole and Debra: We love you!
- 标记(Token)粘贴运算符:在宏定义中,标记粘贴运算符
##
会合并两个参数。它允许将两个独立的标记被合并为一个标记。例如:
#include <stdio.h>#define tokenpaster(n) printf ("token" #n " = %d", token##n)int main(void) {int token34 = 40;tokenpaster(34);return 0;
}
运行:
$ ./main
token34 = 40
上述示例会从编译器产生下列的实际输出:
printf ("token34 = %d", token34);
将 token##n
连接为 token34。在这里,使用了字符串常量化运算符 #
和标记粘贴运算符 ##
。
- defined() 运算符:用在常量表达式中的,用来判断一个标识符是否已经使用
#define
定义过。如果指定的标识符已定义,则值为真。
#include <stdio.h>#if !defined (MESSAGE)#define MESSAGE "You wish!"
#endifint main(void) {printf("Here is the message: %s\n", MESSAGE); return 0;
}
- VA_ARGS 可变参数运算符:用于简便管理打印信息。在写代码或 DEBUG 时通常需要将一些重要参数打印出来,但在发行时又不希望有这些打印,这时就用到可变参数宏了。
# define PR(...) printf(_VA_ARGS_)...
PR("hello world\n");
// 输出结果:hello world
宏定义
简单宏定义
在上文中已经给出了很多的例子,简单的宏定义的格式如下:
#define 标识符 字符串
带参数的宏定义
CPP 一个强大的功能是可以使用参数化的宏来模拟函数,这里应用了 CPP 的 “字符串常量化运算符”,格式如下:
#define <宏名>(<参数列表>) <宏体>
EXAMPLE 1:
// 一般函数
int square(int x) {return x * x;
}// 使用参数化的宏来模拟函数
#define square(x) ((x) * (x))
EXAMPLE 2:
#include <stdio.h>#define MAX(x,y) ((x) > (y) ? (x) : (y))int main(void) {printf("Max between 20 and 10 is %d\n", MAX(10, 20)); return 0;
}
符号吞噬问题
#define COUNT(M) M*Mint x=5;
print(COUNT(x+1));
print(COUNT(++X));
注意:CPP 本质上仍是进行了简单的文本替换,所以上述 EXAMPLE 3 的 COUNT(x+1)
实际上会被替换成 COUNT(x+1*x+1)
,得到 5+15+1=11 而不是 66=36;同理,COUNT(++x)
则被替换成了 COUNT(++x*++x)
,得到 67=42。
可见,千万不要在参数化的宏定义中使用 ++、- 等算数运算法。
再看一个例子:
#define foo(x) bar(x); baz(x)if (!feral)foo(wolf);
替换之后得到的结果是:
if (!feral)bar(wolf);
baz(wolf);
可见 baz(wolf);
已经跳出 if 判断体了,显然是不对的。
这个问题即便加上 {} 也很难解决,例如:
#define foo(x) { bar(x); baz(x); }if (!feral)foo(wolf);
elsebin(wolf);
替换之后的结果:
if (!feral) {bar(wolf);baz(wolf);
}
elsebin(wolf);
结果就是 else 将不会被执行,依旧不对。
使用 do{}while(0) 结构
可以使用 do{}while(0) 结构来解决上述两个问题:
#define foo(x) do{ \bar(x); \baz(x); \
}while(0)if (!feral)foo(wolf);
elsebin(wolf);
替换之后的结果:
if (!feral)do{ bar(x); baz(x); }while(0);
elsebin(wolf);
显然,结果是正确的。这个思路很简单,就是在定义多行的参数化宏时,将多条语句都固定在一个只会执行一次的 do/while 循环体内。使用 do{…}while(0) 架构后的多行的负责宏定义不再受到大括号、分号等 token 的影响,总是会按你期望的方式调用运行。
预定义的宏
C 语言中预先定义了许多宏。在编程中可以直接使用这些预定义的宏,但是不能直接修改它们。
#include <stdio.h>int main() {printf("File :%s\n", __FILE__ );printf("Date :%s\n", __DATE__ );printf("Time :%s\n", __TIME__ );printf("Line :%d\n", __LINE__ );printf("ANSI :%d\n", __STDC__ );return 0;
}
运行:
$ ./main
File :main.c
Date :Apr 4 2020
Time :14:13:10
Line :7
ANSI :1
常用的宏定义
这些宏定义就像是定义好的一个个小工具函数。
- 防止一个头文件被重复包含
#ifndef COMDEF_H
#define COMDEF_H
// 头文件内容
#endif
- 得到指定地址上的一个字节或字(Word)
#define MEM_B(x) (*((byte *)(x)))
#define MEM_W(x) (*((word *)(x)))
- 求最大值和最小值
#define MAX(x,y) (((x)>(y)) ? (x) : (y))
#define MIN(x,y) (((x) < (y)) ? (x) : (y))
- 得到一个成员在结构体中的偏移量
#define FPOS(type,field) ((dword)&((type *)0)->field)
- 得到一个结构体中成员所占用的字节数
#define FSIZ(type,field) sizeof(((type *)0)->field)
- 按照 LSB 格式把两个字节转化为一个字(Word)
#define FLIPW(ray) ((((word)(ray)[0]) * 256) + (ray)[1])
- 得到一个字的高位和低位字节
#define WORD_LO(xxx) ((byte) ((word)(xxx) & 255))
#define WORD_HI(xxx) ((byte) ((word)(xxx) >> 8))
- 将一个字母转换为大写
#define UPCASE(c) (((c)>='a' && (c) <= 'z') ? ((c) – 0×20) : (c))
- 判断字符是不是 10 进制的数字
#define DECCHK(c) ((c)>='0' && (c)<='9')
- 判断字符是不是 16 进制的数字
#define HEXCHK(c) (((c) >= '0' && (c)<='9') ((c)>='A' && (c)<= 'F') \
((c)>='a' && (c)<='f'))
- 防止溢出的一个方法
#define INC_SAT(val) (val=((val)+1>(val)) ? (val)+1 : (val))
- 返回数组元素的个数
#define ARR_SIZE(a) (sizeof((a))/sizeof((a[0])))
总结
- 虽然宏定义很灵活,但宏定义应该简单而清晰。
- 宏名采用大写下划线驼峰风格。
- 如果需要公布某个宏,那么该宏定义应当放置在头文件中。
- 不要使用宏来定义新类型名,应该使用 typedef,否则容易造成错误。
- 给宏添加注释时使用块注释,而不要使用行注释。因为有些编译器可能会把宏后面的行注释理解为宏体的一部分。
- 尽量使用 const 关键字来取代宏用于定义符号常量。
- 对于较长的使用频率较高的重复代码片段,建议使用函数或模板而不要使用带参数的宏定义;
- 对于较短的重复代码片段,可以使用带参数的宏定义,这不仅是出于类型安全的考虑,而且也是优化与折衷的体现。
- 尽量避免在局部命名空间中定义宏,例如:函数内、类型定义内。
C 语言编程 — 宏定义与预处理器指令相关推荐
- C语言编程宏定义的优缺点,C语言重要知识点总结(二)--内存结构、函数调用过程(栈帧)、宏的优缺点以及##和#的使用...
一.内存结构 内存大致可以分为四个部分:代码段,静态存储区,堆,栈. 具体划分如下图所示: 栈:在执行函数时,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束后会自动释放内存.栈内存的分配运算 ...
- C/C++编程笔记:浅析 C 语言中宏定义的使用,知识点全解
宏定义是用一个标识符来表示一个字符串,在宏调用中将用该字符串代替宏名.给程序员提供了便利,使程序更加清晰,便于阅读和理解,进一步提高了程序的运行效率,对于嵌入式系统而言,为了能达到性能要求,宏是一种很 ...
- 如何用C语言改变宏定义的大小,C语言中宏定义使用的小细节
C语言中宏定义使用的小细节 #pragma#pragma 预处理指令详解 在所有的预处理指令中,#Pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作.#p ...
- 【C语言】----宏定义,预处理宏
什么是宏? 宏是学习任何语言所不可缺少的,优秀的宏定义可以使得代码变得很简洁且高效,有效地提高编程效率. 宏是一种预处理指令,它提供了一种机制,可以用来替换源代码中的字符串,解释器或编译器在遇到宏时会 ...
- C程序设计语言现代方法14:预处理器
目录 1. 预处理器工作原理 1.1 预处理器性质 1.2 预处理器主要功能 1.3 GCC编译过程及常用选项 1.3.1 GCC编译过程 1.3.2 编译选项实例 1.4 注意事项 2. 预处理指令 ...
- C语言中宏定义的使用
1. 引言 1.1 宏定义的基本语法 1.2 宏定义的优点 1.3 宏定义的缺点 1.4 宏还是函数 2 使用宏时的注意点 2.1 算符优先级问题 2.2 分号吞噬问题 2.3 宏参数重复调用 2.4 ...
- 大牛深入浅出讲解C语言#define宏定义应用及使用方法
在C语言中,我们使用#define来定义宏.在C程序编译的预处理阶段,预处理器会把宏定义的符号替换成指定的文本. 不带参数的宏 关于宏最常见的就是用来定义数值常量的名称,即没有参数的宏定义,采用如下形 ...
- C语言-入门-宏定义(十七)
预处理 编译一个C语言程序的第一步骤就是预处理阶段,这一阶段就是宏发挥作用的阶段.C预处理器在源代码编译之前对其进行一些文本性质的操作,主要任务包括删除注释.插入被#include进来的文件内容.定义 ...
- C语言 | 预处理 | 宏定义 | #define | 定义函数
文章目录 预处理 预处理运算符 宏定义 无参宏定义 带参宏定义 宏定义-定义函数 此文主要介绍宏定义,并在介绍宏定义时举例介绍预处理命令 预处理 参考:C 预处理器 | 菜鸟教程 重要的预处理器指令如 ...
最新文章
- yanf4j引入了客户端非阻塞API
- HDU 4630 No Pain No Game 树状数组+离线操作
- Divan and a New Project 贪心,模拟(1000)
- c++Insertion Sort插入排序的实现算法(附完整源码)
- C#中float怎样保留两位小数?
- python硬件交互_Python操作系统库说明,pythonos,笔记
- windows下同时安装python2与python3
- 重构-改善既有代码的设计(十)--简化函数调用
- final修饰符、抽象类、接口、多态、内部类的简单小结
- 常用docker管理UI
- Oracle在HPUX IA64平台登陆缓慢问题分析
- 基于html5的在线教育,基于HTML5的在线学习系统的设计与实现
- 存储服务器格式化恢复方法
- 关于公司架构管控的思考
- pythoncqt_python基础篇
- oracle ipad函数(从左边填充)
- 基于matlab的单相pwm逆变电路的仿真研究,基于MATLAB的单相PWM逆变电路的仿真研究.pdf...
- 【第04题】给定 a 和 b,问 a 能否被 b 整除 | if 语句 和 条件运算符的应用
- SHA-3标准(NIST.FIPS.202)阅读笔记
- c++的复制省略(copy elision)
热门文章
- 网页图表Highcharts实践教程之认识Highcharts
- python automl_分享一篇比较全面的AutoML综述
- JAVA实现二维数组中的查找(《剑指offer》)
- 你的眼睛一天内经历几万次“失明”,只是为了让你看清世界
- 芯片巨人也要搞医疗?
- 这十大科学文献最烂配图,你可千万别学
- 苹果用户可以自修手机了!原厂零件工具都能买,网友:iScrew螺丝刀600多块?...
- 程序员为这支笔掰头10个月,隔壁小学生都馋哭了
- 北京对无人车的热情,华尔街都感受到了
- AI正在如何重塑生活和消费?头部企业齐聚,邀你共谈智能产业新机会