• 正文
    • 追本溯源
    • 实际应用
    • 更进一步
  • 相关推荐
申请入驻 产业图谱

【LeafC】C语言之宏魔法2:NARG

9小时前
52
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

追本溯源

这期主题是NARG宏,它被用于计算宏的变长参数的个数。所以这篇文章我想从C语言的变长参数聊起,先说说C语言变长参数应用,以及函数和宏的变长参数的原理,这样在介绍NARG时,我们不仅能够知道它是如何实现的,还能更好地学以致用。

| 函数的变长参数

变长参数看起来很高级,但当C语言初学者第一次敲下Hello World时,就已经在不知不觉中使用了这个重要的特性。

printf("Hello World");

printf函数是一个带变长参数的函数,它的定义如下:

static char buffer[BUFF_SIZE];
int printf(const char *fmt, ...)
{
    va_list args;
    int n;
    va_start(args, fmt);
    n = vsprintf(buffer, fmt, args);
    va_end(args);
    // OUTPUT TO DEVICE
    return n;
}

让我们逐行解析这个函数:

  1. ...:变长参数形参
  2. va_list args:定义一个指向形参列表的指针
  3. va_start(args, fmt):根据fmt形参获取函数栈帧中可变参数列表的地址,并初始化args

当调用printf函数时,其函数栈帧中包含形参列表,完整函数栈帧如下:
局部变量【栈顶】
形参1
...
形参n
函数返回地址
相关寄存器备份
函数返回值【栈底】

  1. vsprintf:将args变长参数列表按照fmt字符串中的规则解析,并将结果填到buffer中,返回值为填入buffer的字节数

拓展知识:
vsprintf也可以使用安全函数vsnprintf,后者多一个表示buffer大小的参数。
嵌入式环境中,通常在OUTPUT TO DEVICE位置会将buffer中数据输出到串口或者其他输出设备;在带文件系统的操作系统中,vsprintf函数会被vfprintf取代,数据输出结果不填入buffer,而是填入stdout标准输出流中(如命令行程序打印到屏幕上)

  1. va_end(args):将args置为NULL

细心的读者可能会提出疑问,printf函数使用变长参数时,为何不需要知道变长参数的个数呢?

其实C语言中函数的变长参数个数是需要通过入参传入的,函数的栈帧中也确实不会自动生成变长参数个数的信息,只不过printf函数的入参fmt中已间接包含了变长参数个数信息,即有多少个%

如果要实现一个不定长参数的my_sum求和函数,那就只能多加一个代表变长参数个数的入参了:

// 约束:变长入参为int类型
int my_sum(int num, ...)
{
    va_list args;
    int sum = 0;
    va_start(args, num);
    while (num--) {
        sum += va_arg(args, int);
    }
    va_end(args);
    return sum;
}

// 调用
int sum = my_sum(3, 1, 2, 3); // sum = 6

但是,有没有办法优化这个my_sum函数,让程序在编译时计算变长参数个数,而非手动填写呢?

| 宏的变长参数

与函数的变长参数不同的是,宏只作用在预编译阶段,即对宏的变长参数的操作在代码运行之前,不会造成运行时开销。

下面是使用NARG宏优化后的my_sum函数:

#define ARG_N(_0, _1, _2, _3, _4, _5, ...) _5
#define NARG(...) ARG_N(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
#define MY_SUM(...) my_sum(NARG(__VA_ARGS__), ...)
// 调用
int sum = MY_SUM(1, 2, 3); // sum = 6

我们来逐行解析MY_SUM的实现:

  1. ARG_N:定义一个含N+1个固定参数以及最后一个为变长参数的宏,其值为最后一个固定参数_N(这里N取5,最大支持计算5个参数数量)
  2. NARG:利用ARG_N值为最后一个固定参数的特性,将变长参数放参数列表开头,后续参数为从N0递减,巧妙地让变长参数个数与ARG_N宏的值相等
  3. MY_SUM:利用NARG在预编译时计算出参数个数,填入my_sum函数第一个参数

宏的变长参数在使用时有2种方式:
1、__VA_ARGS__:可以不搭配固定参数使用,但变长参数个数需大于0
2、##__VA_ARGS__:必须搭配至少一个固定参数使用,且变长参数个数可为0

以下为__VA_ARGS__##__VA_ARGS__合法和非法的使用方式:

#define MACRO1(...) func(__VA_ARGS__)
#define MACRO2(arg, ...) func(__VA_ARGS__)
#define MACRO3(arg, ...) func(##__VA_ARGS__)
// 合法
MACRO1(1)
MACRO2(1,2)
MACRO3(1)
MACRO3(1,2)
// 非法
MACRO1()
MACRO2(1)

实际应用

开源项目googletest中的googlemock就使用了NARG,以及除此之外的超多宏魔法,这是由于其需要对外部输入做很多的编译时合法性检查。在后续的宏魔法系列文章中,可能会再次引用googlemock中的实现思路。

更进一步

上述MY_SUM宏通过调用my_sum这个带变长参数函数实现了不定个数数字求和。假如MY_SUM宏的参数均为常量,有没办法继续优化,使程序完全在编译时计算呢?

#define SUM1(a1) (a1)
#define SUM2(a1, a2) (SUM1(a1) + (a2))
#define SUM3(a1, a2, a3) (SUM2(a1, a2) + (a3))
#define SUM4(a1, a2, a3, a4) (SUM3(a1, a2, a3) + (a4))

#define SUM_N(_1, _2, _3, _4, NAME, ...) NAME
#define MY_SUM(...) SUM_N(__VA_ARGS__, SUM4, SUM3, SUM2, SUM1)(__VA_ARGS__)

// 调用
int sum = MY_SUM(1, 2, 3); // sum = 6

以上MY_SUM实现原理留给读者思考,其原理与NARG宏如出一辙。

原理:
将一个前为固定参数,后为变长参数的前定后变宏,传入一个前为变长参数,后为固定参数的前变后定参数列表,利用前定后变宏固定参数位置不变的性质,按照规则排列好前变后定参数列表,完成参数列表的选择。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

个人博客:嵌入式技术&生活分享&开源项目

微信公众号