• 1. 引言

    • 1.1 宏定义的基本语法
    • 1.2 宏定义的优点
    • 1.3 宏定义的缺点
    • 1.4 宏还是函数
  • 2 使用宏时的注意点
    • 2.1 算符优先级问题
    • 2.2 分号吞噬问题
    • 2.3 宏参数重复调用
    • 2.4 对自身的递归引用
  • 3. 宏函数的集中特定语法
    • 3.1 利用宏参数创建字符串:”#运算符”
    • 3.2 预处理器的粘合剂:”##运算符”
    • 3.3 可变宏:… 和_VA_ARGS
  • 4 宏的常用用法
    • 4.1 通用数据结构封装
    • 4.2 日志打印,出错处理
  • 5 Reference

1. 引言

==预处理==命令可以改变程序设计环境,提高编程效率,它们并不是 C 语言本身的组成部分,不能直接对 它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理” 。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对==预处理==之后的源程序进行==编译==处理,得到可供执行的目标代码。C 语言提供的预处理功能有三种,分别为==宏定义==、文件包含和条件编译。


1.1 宏定义的基本语法

宏定义在 C 语言源程序中允许用一个标识符来表示一个==字符串==,称为“==宏/宏体==” ,被定义为“宏”的==标识符==称为“==宏名==”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“==宏替换==”或“==宏展开==”。 宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。

在 C 语言中,宏分为 有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为:

#define 标识符 字符串

#表示这是一条预处理命令(在C语言中凡是以#开头的均为预处理命令)
==define #117411==为宏定义命令
==标识符 #800023==为所定义的宏名,
==字符串 #800019==可以是常数、表达式、格式串等。符号常量

// 不带参数的宏定义
#define MAX 10/*带参宏定义*/
#define M(y) y*y+3*y/*宏调用*/
k=M(5);

1.2 宏定义的优点

方便程序的修改
使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。
相对于==全局变量==两者的区别如下:
1. 宏定义在编译期间即会使用并替换,而全局变量要到运行时才可以。
2. 宏定义的只是一段字符,在编译的时候被替换到引用的位置。在运行中是没有宏定义的概念的。而变量在运行时要为其分配内存。
3. 宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。
4. 宏定义只有在定义所在文件,或引用所在文件的其它文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不需要extern。


提高程序的运行效率

使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生==函数调用 #800023==时,需要保留调用函数的现场,以便子 函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽 略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问 题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。


1.3 宏定义的缺点

  1. 由于是直接嵌入的,所以代码可能相对多一点;
  2. 嵌套定义过多可能会影响程序的可读性,而且很容易出错,不容易调试。
  3. 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。

1.4 宏还是函数

宏函数,函数比较

==从时间上来看 #0c2ac0==

  1. 宏只占编译时间,函数调用则占用运行时间(分配单元,保存现场,值传递,返回),每次执行都要载入,所以执行相对宏会较慢。

  2. 使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。

==从安全上来看 #0c2ac0==

  1. 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。

  2. 函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。

  3. 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。

  4. 宏的定义很容易产生二义性,如:定义==#define S(a) (a)*(a)==,代码==S(a++)==,宏展开变成==(a++)*(a++)==这个大家都知道,在不同编译环境下会有不同结果。

  5. 调用函数只可得到一个返回值,且有返回类型,而宏没有返回值和返回类型,但是用宏可以设法得到几个结果。

  6. 函数体内有Bug,可以在函数体内打断点调试。如果宏体内有Bug,那么在执行的时候是不能对宏调试的,即不能深入到宏内部。

  7. C++中宏不能访问对象的私有成员,但是成员函数就可以。


内联函数
在C99中引入了内联函数(==inline==),联函数和宏的区别在于,==宏是由预处理器对宏进行替代 #80001e==,而==内联函数是通过编译器控制来实现的 #80000f==。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的==展开==,所以取消了函数的参数压栈,减少了调用的开销。可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。

内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。


宏函数的适用范围

  1. 一般来说,用宏来代表简短的表达式比较合适。
  2. 在考虑效率的时候,可以考虑使用宏,或者内联函数。
  3. 还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。

2 使用宏时的注意点

2.1 算符优先级问题

不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:

#define MULTIPLY(x, y) x * y

==MULTIPLY(1, 2) #80000a==没问题,会正常展开成==1 * 2 #80000a==。有问题的是这种表达式==MULTIPLY(1+2, 3) #800019==,展开后成了==1+2 * 3 #80000f==,显然优先级错了。
对宏体和给引用的每个参数加括号,就能避免这问题。

#define MULTIPLY(x, y) ((x) * (y))

2.2 分号吞噬问题

有如下宏定义

#define foo(x) bar(x); baz(x)

假设你这样调用:

if (!feral)foo(wolf);

这将被宏扩展为:

if (!feral)bar(wolf);
baz(wolf);

==baz(wolf);==,不在判断条件中,显而易见,这是错误。
如果用大括号将其包起来依然会有问题,例如

#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) #80001e==能够解决上述问题

#define foo(x)  do{ bar(x); baz(x); }while(0)
if (!feral)foo(wolf);
elsebin(wolf);

被扩展成:

#define foo(x)  do{ bar(x); baz(x); }while(0)
if (!feral)do{ bar(x); baz(x); }while(0);
elsebin(wolf);

使用do{…}while(0)构造后的宏定义不会受到大括号、分号等的影响,总是会按你期望的方式调用运行。


2.3 宏参数重复调用

有如下宏定义:

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

当有如下调用时==next = min (x + y, foo (z));==,宏体被展开成==next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));==,可以看到,foo(z)有可能会被重复调用了两次,做了重复计算。更严重的是,如果foo是不可重入的(foo内修改了全局或静态变量),程序会产生逻辑错误。


2.4 对自身的递归引用

有如下宏定义:

#define foo (4 + foo)

按前面的理解,==(4 + foo)==会展开成==(4 + (4 + foo))==,然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,foo只会展开成==4 + foo==,而展开之后foo的含义就要根据上下文来确定了。

对于以下的交叉引用,宏体也只会展开一次。

#define x (4 + y)
#define y (2 * x)

x展开成(4 + y) -> (4 + (2 * x)),y展开成(2 * x) -> (2 * (4 + y))

注意,这是极不推荐的写法,程序可读性极差。


3. 宏函数的集中特定语法

3.1 利用宏参数创建字符串:”#运算符”

在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:

#include <stdio.h>#define PSQR(x) printf("the square of "#x" is %d.\n",((x)*(x)))
#define PSQR2(x) printf("the square of %s is %d.\n",#x,((x)*(x)))int main() {int R=5;PSQR(R);  //the square of R is 25.PSQR2(R); // the square of R is 25.return 0;
}

这种用法可以用在一些出错处理中

#include <stdio.h>#define WARN_IF(EXPR)\
do {\if (EXPR)\fprintf(stderr, "Warning: EXPR \n");\
} while(0)int main() {int R=5;WARN_IF(R>0);return 0;
}

3.2 预处理器的粘合剂:”##运算符”

和#运算符一样,##运算符可以用于类函数宏的替换部分。另外,##还可以用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。例如

#define XNAME(n) x ## n
int x1=10;
XNAME(1)+=1;  //x1 11

这个地方还需要再添加一个常用的用法


3.3 可变宏:… 和_VA_ARGS

有些函数(如==prinft() #06906d==)可以接受可变数量的参数。

  int __cdecl printf(const char * __restrict__ _Format,...);

实现思想就是在宏定义中参数列表的最后一个参数作为省略号(三个句号)。这样,预定义宏_VA_ARGS就可以被用在替换部分中,以表明省略号代表什么,

例如

输出

#define PR(...) printf(__VA_ARGS_)
PR("Howdy");
PR("weight=%d,shipping=$%.2f.\n",wt,sp)

参数初始化
通过可以参数可以完成对多个参数的初始化,就像int数组的初始化那样
例如动态数组的添加

darray(int)  arr=darray_new();
int *i;
darray_appends(arr, 0,1,2,3,4);
darray_foreach(i, arr)
{printf("%d ", *i);
}

4 宏的常用用法

4.1 通用数据结构封装

宏是一种字符串替换==不做类型检查==,可以将==类型做为参数==传入宏函数,利用这种特性可以实现通用数据结构的封装,以动态数组darray,和循环链表list为例

动态数组是把自己的结构体放在规定的结构体之内,还有一种实现方式,把规定的结构体放到自己的结构体之中,这种方式扩展性更好,这个时候需要根据成员指针得到结构体指针。通过==container_of==实现。

==#define container_of(member_ptr, containing_type, member)==


4.2 日志打印,出错处理

合理的适用预定义宏如__FILE__,字符串化符号#可以封装很多打印功能,如打印日志,断言检查等功能。

日志打印

#define MacroLog(...)\
{\
FILE* file;\
fopen_s(&file,"./a.txt","a");\
if (file != nullptr)\
{\
fprintf(file, "%s: Line %d:\t", __FILE__, __LINE__);\
fprintf(file, __VA_ARGS__);\
fprintf(file, "\n");\
}\
fclose(file);\
}void FuncLog(const char *filename, int line,const char * str)
{FILE* file;fopen_s(&file, "./a.txt", "a");if (file != nullptr){fprintf(file, "%s: Line %d:\t", filename, line);fprintf(file, str);fprintf(file, "\n");}fclose(file);
}int main()
{log("%s,%s", "hello","log");funclog(__FILE__, __LINE__,"hello");return 0;
}

断言

    _ACRTIMP void __cdecl _wassert(_In_z_ wchar_t const* _Message,_In_z_ wchar_t const* _File,_In_   unsigned       _Line);#define assert(expression) (void)(                                                       \(!!(expression)) ||                                                              \(_wassert(_CRT_WIDE(>>++#expression++<<), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \)
int main()
{int a = 10;assert(a == 1); //Assertion failed: a == 1, file c:\users\10241258\source\repos\c_win64_test\c_win64_test\c_win64_test.cpp, line 38return 0;
}

常用预定义宏

__FUNTION__  获取当前函数名 __LINE__ 获取当前代码行号 __FILE__ 获取当前文件名 __DATE__ 获取当前日期 __TIME__ 获取当前时间__STDC_VERSION__

5 Reference

[1] C Primer Plus

[2] C语言宏定义的几个坑和特殊用法

[3] CCAN

C语言中宏定义的使用相关推荐

  1. 如何用C语言改变宏定义的大小,C语言中宏定义使用的小细节

    C语言中宏定义使用的小细节 #pragma#pragma 预处理指令详解 在所有的预处理指令中,#Pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作.#p ...

  2. C/C++编程笔记:浅析 C 语言中宏定义的使用,知识点全解

    宏定义是用一个标识符来表示一个字符串,在宏调用中将用该字符串代替宏名.给程序员提供了便利,使程序更加清晰,便于阅读和理解,进一步提高了程序的运行效率,对于嵌入式系统而言,为了能达到性能要求,宏是一种很 ...

  3. c语言中宏定义的字符替换#define M(x,y,z) x*y+z

    C语言中宏定义的字符替换问题 例子: 在c语言中定义如下宏 #define M(x,y,z) x*y+z 给定如下程序 #include<stdio.h> #include<stdl ...

  4. C语言中宏定义和函数的取舍

    要写好C语言,漂亮的宏定义是非常重要的.宏定义可以帮助我们防止出错,提高代码的可移植性和可读性等. 在软件开发过程中,经常有一些常用或者通用的功能或者代码段,这些功能既可以写成函数,也可以封装成为宏定 ...

  5. C语言中宏定义使用方法详解

    C语言中的宏替换详解 首先看一个问题: #include <stdio.h> #define    PRINT_CLINE()    printf("%d", ____ ...

  6. C语言中宏定义和函数的区别

    前言 在C语言中,对于一些常用或通用的代码段的封装可以有两种方式:函数和宏定义. 这篇博客就来带大家梳理一下对于这两种方式我们在使用时应该如何抉择,以及它们的区别和优缺点. 宏定义和函数的区别 从程序 ...

  7. C语言中宏定义宏编译的使用#if,#ifdef,#ifndef,#elif,#else,#endif,defined使用详解,可以实现和if else一样的效果

    #define            定义一个预处理宏 #undef            取消宏的定义 #if                   编译预处理中的条件命令,相当于C语法中的if语句 ...

  8. c语言用宏定义常量_使用宏定义常量以在C的数组声明中使用

    c语言用宏定义常量 As we know that, while declaring an array we need to pass maximum number of elements, for ...

  9. C 语言编程 — 宏定义与预处理器指令

    目录 文章目录 目录 前文列表 宏 预处理器 预处理器指令 预处理器指令示例 预处理器指令运算符 宏定义 简单宏定义 带参数的宏定义 符号吞噬问题 使用 do{}while(0) 结构 预定义的宏 常 ...

最新文章

  1. explain 之 table || explain 之 type
  2. jQuery使用(十二):工具方法之type()之类型判断
  3. mybatisplus查询今天的数据_MybatisPlus(CRUD)
  4. 积跬步以至千里_《荀子》名句76则:不积跬步,无以至千里;不积小流,无以成江海...
  5. Libra教程之:move语言的特点和例子
  6. 电商系统的自提订单,提货流程如何设计
  7. java post get 请求
  8. C语言编译php环境,vscode中C语言编译环境的配置方法(分享)
  9. Python画汉诺塔
  10. Java中String、StringBuffer和StringBuilder的区别和堆栈内存分配
  11. 受力分析软件_大赛作品 | 关于Z型路灯受力情况分析
  12. EmguCv模板匹配学习日记
  13. java406错误_Springmvc报406错误(Not Acceptable)的有效解决方法
  14. 两色注塑机的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  15. AI微信小程序源码下载人脸照片AI转换动漫照片全新源码安装简单无需服务器域名
  16. 最全面贴片电阻知识,封装、尺寸、最大承受功率及选型统统包括-电子技术方案|电路图讲解
  17. 元旦用SpringBoot撸了个博客网站送给大家
  18. eclipse:Project is already imported into workspace
  19. 首款Unreal Engine 4引擎制作手机游戏曝光
  20. vue点tab不刷新页面_如何使tab页切换,页面不刷新

热门文章

  1. admob里集成KeyMob横幅广告教程
  2. java程序设计雷电游戏设计步骤_基于Java的飞机雷电射击游戏的设计任务书
  3. C/C++多文件处理
  4. 北森继续冲刺港交所上市:累计亏损约54亿元,王朝晖为董事长
  5. 假如服务器被打死,我的源IP暴露怎么办补救?
  6. 我的jQuery笔记
  7. Php 线性 非线性,非线性方程的线性化
  8. Mac睡眠、注销、重启、关机的快捷键
  9. Behavior实现UC浏览器首页动画效果
  10. python 如何调试uc浏览器_UC浏览器调试移动端网站