C语言中宏定义的使用

2023-05-16

1. 引言

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

1.1 宏定义的基本语法

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

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

  • #表示这是一条预处理命令(在C语言中凡是以#开头的均为预处理命令)

  • define为宏定义命令

  • 标识符为所定义的宏名,

  • 字符串可以是常数、表达式、格式串等。符号常量

  • // 不带参数的宏定义
    #define MAX 10

  • /带参宏定义/
    #define M(y) yy+3y

  • /宏调用/
    k=M(5);

1.2 宏定义的优点

  • 方便程序的修改
    使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。

  • 相对于全局变量两者的区别如下:

    • 宏定义在编译期间即会使用并替换,而全局变量要到运行时才可以。
    • 宏定义的只是一段字符,在编译的时候被替换到引用的位置。在运行中是没有宏定义的概念的。而变量在运行时要为其分配内存。
    • 宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。
    • 宏定义只有定义在所在文件,或在引用所在文件的其它文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不需要extern。
  • 提高程序的运行效率

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

1.3 宏定义的缺点

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

1.4 宏还是函数(宏函数与函数比较)

  • 从时间上来看

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

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

  • 从安全上来看

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

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

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

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

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

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

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

1.5 内联函数

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

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

1.6 宏函数的适用范围

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

2 使用宏时的注意点

2.1 算符优先级问题

不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:#define MULTIPLY(x, y) x * yMULTIPLY(1, 2)没问题,会正常展开成1 * 2。有问题的是这种表达式MULTIPLY(1+2, 3),展开后成了1+2 * 3,显然优先级错了。

对宏体和给引用的每个参数加括号,就能避免这问题:#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);
    else
        bin(wolf);
    

    判断语言被扩展成:

    if (!feral) {
        bar(wolf);
        baz(wolf);
    };
    else
        bin(wolf);
    

    else将不会被执行

  • 通过do{…}while(0)能够解决上述问题:

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

    被扩展成:

    #define foo(x)  do{ bar(x); baz(x); }while(0)
    if (!feral)
        do{ bar(x); baz(x); }while(0);
    else
        bin(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())可以接受可变数量的参数。

 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数组的初始化那样

例如动态数组的添加:

//动态数组multi append
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)

循环链表list

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 38

    return 0;
}
常用预定义宏
 __FUNTION__  获取当前函数名 
 __LINE__ 获取当前代码行号 
 __FILE__ 获取当前文件名 
 __DATE__ 获取当前日期 
 __TIME__ 获取当前时间
 __STDC_VERSION__
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言中宏定义的使用 的相关文章

随机推荐

  • 解决Mingw-w64下载太慢问题

    官网下载太慢了 xff0c 我们只用换一个镜像源就可以 1 点击Problems Downloading 2 切换香港的
  • 嵌入式Linux开发8——UART串口通讯

    1 背景知识 1 1 UART通讯格式 串口全称叫做串行接口 xff0c 通常也叫做 COM 接口 xff0c 串行接口指的是数据一个一个的顺序传输 xff0c 通信线路简单 使用两条线即可实现双向通信 xff0c 一条用于发送 xff0c
  • 二叉树笔记

    二叉树 二叉搜索 xff08 排序 查找 xff09 树 二叉查找树 xff08 Binary Search Tree xff09 xff0c xff08 又 xff1a 二叉搜索树 xff0c 二叉排序树 xff09 它或者是一棵空树 x
  • C++面试常见题目

    C 43 43 面试常见题目 c 43 43 编译过程自动类型推导auto和decltype重载 重写 xff08 覆盖 xff09 和隐藏的区别C 43 43 构造函数和析构函数能调用虚函数吗volatile关键词运算符重载格式noexe
  • 计算机网络面试常问问题

    C 43 43 面试 计算机网络常见问题 计算机网络常见问题TCP IP协议笔记TCPTCP的特点及目的序列号与确认应答提高可靠性为什么是三次握手和四次挥手滑动窗口流量控制拥塞控制TCP粘包问题 httphttp和https的区别https
  • Trajectory generation for quadrotor while tracking a moving target in cluttered environment

    四旋翼在杂波环境下跟踪运动目标的轨迹生成 摘要1 文章主要贡献2 前言2 1 轨迹公式2 2 实现结构 3 跟踪轨迹生成3 1 标称路径点生成3 2 可行路径点生成3 3 安全飞行走廊生成3 4 代价函数3 5 强制约束3 6 求解跟踪轨迹
  • 翻译-Frustum PointNets for 3D Object Detection from RGB-D Data

    Frustum PointNets for 3D Object Detection from RGB D Data 摘要介绍相关工作从RGB D数据中检测三维物体基于前视图图像的方法 xff1a 基于鸟瞰图的方法 基于3D的方法 点云的深度
  • Online Trajectory Generation of a MAV for Chasing a Moving Target in 3D Dense Environments

    微型无人机的在线轨迹生成 xff0c 用于在3D密集环境中追踪运动目标 摘要一 介绍二 相关工作A 在障碍物环境中追逐B 通过预先规划安全地生成轨迹 三 问题陈述A 问题设置B 能力C 命名 IV 视点生成A 可见度指标B 具有安全性和可见
  • 配置目标跟踪开源项目traj_gen_vis踩过的坑

    项目地址 https github com icsl Jeon traj gen vis 安装依赖需注意的问题 traj gen with qpoases 需安装ros分支的代码 xff08 这个作者并没有指出 xff0c 坑 xff09
  • cmake arm-none-eabi-gcc for stm32 cpp project

    尝试把原有的stm32工程F1canBootloader用cmake来管理 xff0c 遇到了以下几个坑 xff1a 1 报错 xff0c undefined reference to 96 dso handle 39 CMakeFiles
  • 网络攻防之wireshark抓取登录信息

    使用wireshark抓取登录信息 简介 xff1a Wireshark xff08 前称Ethereal xff09 是一个网络封包分析软件 网络封包分析软件的功能是撷取网络封包 xff0c 并尽可能显示出最为详细的网络封包资料 Wire
  • 头文件互相包含所引发的的问题(深入剖析)

    今天写程序出现了一个让人蛋疼的错误 xff0c 后来发现是由于头文件互相包含所引起的 原本只是简单的以为头文件互相包含只会触发 xff0c 头文件的递归包含 即 xff0c A包含B xff0c 所以才A的头文件里会将B的头文件内容拷贝过来
  • C++11异步操作future和aysnc 、function和bind

    C 43 43 11异步操作future和aysnc function和bind 前言异步操作std future和std aysnc 介绍std future和std aysnc的使用Demostd packaged task 介绍std
  • C++文件服务器项目—FastDFS—1

    C 43 43 文件服务器项目 FastDFS 1 前言1 项目架构2 分布式文件系统2 1 传统文件系统2 2 分布式文件系统 3 FastDFS介绍3 1 fdfs概述3 2 fdfs框架中的三个角色3 3 fdfs三个角色之间的关系3
  • C++文件服务器项目—Redis—2

    C 43 43 文件服务器项目 Redis 2 前言1 数据库类型1 1 基本概念1 2 关系 非关系型数据库搭配使用 2 redis基础知识点2 1 redis安装2 2 redis中的两个角色2 3 redis中数据的组织格式2 4 r
  • C++文件服务器项目—Nginx—3

    C 43 43 文件服务器项目 Nginx 3 前言1 Nginx一些基本概念1 1 Nginx初步认识1 2 正向代理概念理解1 3 反向代理概念理解 2 Nginx的安装与配置2 1 Nginx与相关依赖库的安装2 2 Nginx相关的
  • C++文件服务器项目—FastCGI—4

    C 43 43 文件服务器项目 FastCGI 4 前言1 CGI 概念理解2 FastCGI 概念理解3 FastCGI和spawn fcgi安装4 FastCGI和 Nginx的关系5 Nginx数据转发 修改配置文件6 spawn f
  • C++文件服务器项目—Nginx+FastDFS插件—5

    C 43 43 文件服务器项目 Nginx 43 FastDFS插件 5 前言1 文件上传下载流程1 1 文件上传流程1 2 文件下载流程1 3 文件下载优化流程 2 Nginx和fastDFS插件2 1 安装Nginx和fastdfs n
  • C++文件服务器项目—数据库表设计 与 后端接口设计—6

    C 43 43 文件服务器项目 数据库表的设计 6 前言1 数据库建表1 1 用户信息表 user info1 2 文件信息表 file info1 3 用户文件列表表 user file list1 4 用户文件数量表 user file
  • C语言中宏定义的使用

    1 引言 预处理命令可以改变程序设计环境 提高编程效率 它们并不是 C 语言本身的组成部分 不能直接对 它们进行编译 必须在对程序进行编译之前 先对程序中这些特殊的命令进行 预处理 经过预处理后 程序就不再包括预处理命令了 最后再由编译程序