C语言变参函数和可变参数宏

2023-05-16

文章目录

  • 一、变参函数的设计与实现
    • 1.变参函数初体验
    • 2.变参函数改进版
    • 3.变参函数 V3.0 版本
    • 3.变参函数 V4.0 版本
    • 4.变参函数 V5.0 版本
  • 二、可变参数宏的设计与实现
    • 1.什么是可变参数宏
    • 2.宏连接符##的作用
    • 3.可变参数宏的另一种写法
  • 三、利用变参函数和可变参数宏实现自己的代码模块
    • 1.实现函数重载
    • 2.实现using结构
    • 3.实现一个原子操作宏
    • 4.实现foreach结构


一、变参函数的设计与实现

对于一个普通函数,我们在函数实现中,不用关心实参,只需要在函数体内对形参直接引用即可。当函数调用时,传递的实参和形参个数和格式是匹配的。

变参函数,顾名思义,跟 printf 函数一样:参数的个数、类型都不固定。我们在函数体内因为预先不知道传进来的参数类型和个数,所以实现起来会稍微麻烦一点。首先要解析传进来的实参,保存起来,然后才能接着像普通函数一样,对实参进行处理。

1.变参函数初体验

我们接下来,就定义一个变参函数,实现的功能很简单,即打印传进来的实参值。

void print_num(int count, ...)
{
    int *args;
    args = &count + 1;
    for( int i = 0; i < count; i++)
    {
        printf("*args: %d\n", *args);
        args++;
    }
}
int main(void)
{
    print_num(5,1,2,3,4,5);
    return 0;
}

变参函数的参数存储其实跟 main 函数的参数存储很像,由一个连续的参数列表组成,列表里存放的是每个参数的地址。在上面的函数中,有一个固定的参数 count,这个固定参数的存储地址后面,就是一系列参数的指针。在 print_num 函数中,先获取 count 参数地址,然后使用 &count + 1 就可以获取下一个参数的指针地址,使用指针变量 args 保存这个地址,并依次访问下一个地址,就可以直接打印传进来的各个实参值了。程序运行结果如下:

*args:1
*args:2
*args:3
*args:4
*args:5

2.变参函数改进版

上面的程序使用一个 int * 的指针变量依次去访问实参列表。我们接下来把程序改进一下,使用 char * 类型的指针来实现这个功能,使之兼容更多的参数类型。

void print_num2(int count,...)
{
    char *args;
    args = (char *)&count + 4;
    for(int i = 0; i < count; i++)
    {
        printf("*args: %d\n", *(int *)args);
        args += 4;
    }
} 
int main(void)
{
    print_num2(5,1,2,3,4,5);
    return 0;
}

在这个程序中,我们使用char * 类型的指针。涉及到指针运算,一定要注意每一个参数的地址都是4字节大小,所以我们获取下一个参数的地址是: (char *)&count + 4; 。不同类型的指针加1操作,转换为实际的数值运算是不一样的。对于一个指向 int 类型的指针变量p,p+1表示 p + 1 * sizeof(int) ,对于一个指向 char 类型的指针变量,p + 1 表示 p + 1 * sizeof(char) 。两种不同类型的指针,其运算细节就体现在这里。当然,程序最后的运行结果跟上面的程序是一样的,如下所示:

*args:1
*args:2
*args:3
*args:4
*args:5

3.变参函数 V3.0 版本

对于变参函数,编译器或计算机系统一般会提供一些宏给程序员使用,用来解析函数的参数。这样程序员就不用自己解析参数了,直接使用封装好的宏即可。编译器提供的宏有:

  • va_list:定义在编译器头文件中 typedef char* va_list; 。
  • va_start(args,fmt):根据参数 fmt 的地址,获取 fmt 后面参数的地址,并保存在 args 指针变量中。
  • va_end(args):释放 args 指针,将其赋值为 NULL。
  • va_arg(va_list ap, type):该宏返回下一个额外的参数,是一个类型为 type 的表达式。

有了这些宏,我们的工作就简化了很多。我们就不用撸起袖子,自己解析了。

void print_num3(int count,...)
{
    va_list args;
    va_start(args,count);
    for(int i = 0; i < count; i++)
    {
        printf("*args: %d\n", va_arg(args, int));
    } 
    va_end(args);
}
int main(void)
{
    print_num3(5,1,2,3,4,5);
    return 0;
}

3.变参函数 V4.0 版本

在 V3.0 版本中,我们使用编译器提供的三个宏,省去了解析参数的麻烦。但打印的时候,我们还必须自己实现。在 V4.0 版本中,我们继续改进,使用 vprintf 函数实现我们的打印功能。vprintf 函数的声明在 stdio.h 头文件中。

CRTIMP int __cdecl __MINGW_NOTHROW \
    vprintf (const char*, __VALIST);

vprintf 函数有2个参数,一个是格式字符串指针,一个是变参列表。在下面的程序里,我们可以将,使用 va_start 解析后的变参列表,直接传递给 vprintf 函数,实现打印功能。

void my_printf(char *fmt,...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
} 
int main(void)
{
    int num = 0;
    my_printf("I am litao, I have %d car\n", num);
    return 0;
}

运行结果如下:

I am litao, I have 0 car

4.变参函数 V5.0 版本

上面的 my_printf() 函数,基本上实现了跟 printf() 函数相同的功能:支持变参,支持多种格式的数据打印。接下来,我们还需要对其添加 format 属性声明,让编译器在编译时,像检查 printf 一样,检查 my_printf() 函数的参数格式。
GNU 通过 attribute 扩展的 format 属性,用来指定变参函数的参数格式检查。
它的使用方法如下:

__attribute__(( format (archetype, string-index, first-to-check)))
    void LOG(const char *fmt, ...) __attribute__((format(printf,1,2)));

属性 format(printf,1,2) 有三个参数。第一个参数 printf 是告诉编译器,按照 printf 函数的检查标准来检查;第2个参数表示在 LOG 函数所有的参数列表中,格式字符串的位置索引;第3个参数是告诉编译器要检查的参数的起始位置。

V5.0 版本如下:

void __attribute__((format(printf,1,2))) my_printf(char *fmt,...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
} 
int main(void)
{
    int num = 0;
    my_printf("I am litao, I have %d car\n", num);
    return 0;
}

二、可变参数宏的设计与实现

1.什么是可变参数宏

在上面的教程中,我们学会了变参函数的定义和使用,基本套路就是使用 va_list 、 va_start 、 va_end 等宏,去解析那些可变参数列表我们找到这些参数的存储地址后,就可以对这些参数进行处理了:要么自己动手,自己处理;要么继续调用其它函来处理。

void print_num(int count, ...)
{
    va_list args;
    va_start(args,count);
    for(int i = 0; i < count; i++)
    {
        printf("*args: %d\n", va_arg(args, int));
    }
} 
void __attribute__((format(printf,2,3))) LOG(int k,char *fmt,...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
}

可变参数宏的定义和使用。其实,C99 标准已经支持了这个特性,但是其它的编译器不太给力,对 C99 标准的支持
不是很好,只有 GNU C 支持这个功能,所以有时候我们也把这个可变参数宏看作是 GNU C 的一个语法扩展。 上面的 LOG 函数,如果我们想使用一个变参宏实现,就可以直接这样定义:

#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define DEBUG(...) printf(__VA_ARGS__)
int main(void)
{
    LOG("Hello! I'm %s\n","Wanglitao");
    DEBUG("Hello! I'm %s\n","Wanglitao");
    return 0;
}

变参宏的实现形式其实跟变参函数差不多:用 … 表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开。可变参数宏使用 C99 标准新增加的一个 VA_ARGS 预定义标识符来表示前面的变参列表,而不是像变参函数一样,使用 va_list 、va_start 、 va_end 这些宏去解析变参列表。预处理器在将宏展开时,会用变参列表替换掉宏定义中的所有 VA_ARGS 标识符。

2.宏连接符##的作用

如果这个宏没有##

#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)

在这个宏定义中,有一个固定参数,通常为一个格式字符串,后面的变参用来打印各种格式的数据,跟前面的格式字符串相匹配。这种定义方式有一个漏洞,即当变参为空时,宏展开时就会产生一个语法错误。

#define LOG(fmt,...) printf(fmt,__VA_ARGS__)
int main(void)
{
    LOG("hello\n");
    return 0;
}

上面这个程序编译时就会通不过,产生一个语法错误。这是因为,我们只给 LOG 宏传递了一个参数,而变参为空。当宏展开后,就变成了下面这个样子。

printf("hello\n", );

宏展开后,在第一个字符串参数的后面还有一个逗号,所以就产生了一个语法错误。我们需要对这个宏进行改进,使用宏连接符##,来避免这个语法错误。

宏连接符 ## 的主要作用就是连接两个字符串,我们在宏定义中可以使用 ## 来连接两个字符。预处理器在预处理阶段对宏展开时,会将## 两边的字符合并,并删除 ## 这两个字符。

#define CONNECT2(__A, __B)    __A##__B
int safe_atom_code(void)
{
    uint32_t CONNECT2(wTemp,__LINE__) = __disable_irq(); 
    /* do something here */
    __set_PRIMASK(CONNECT2(wTemp,__LINE__));
    return 0;
}

假设__LINE__所在的行号为123,那么这里定义的变量名字就为wTemp123。

知道了宏连接符 ## 的使用方法,我们接下来就可以就对 LOG 宏做一些分析。

#define LOG(fmt,...) printf(fmt, ##__VA_ARGS__)
int main(void)
{
    LOG("hello\n");
    return 0;
}

我们在标识符 __ VA_ARGS __ 前面加上宏连接符 ##,这样做的好处是,当变参列表非空时,## 的作用是连接 fmt,和变参列表,各个参数之间用逗号隔开,宏可以正常使用;当变参列表为空时,## 还有一个特殊的用处,它会将固定参数 fmt 后面的逗号删除掉,这样宏也就可以正常使用了。

使用宏连接符 ##要注意一下两条结论:

  • 第一条:任何使用到胶水运算“##”对形参进行粘合的参数宏,一定需要额外的再套一层
  • 第二条:其余情况下,如果要用到胶水运算,一定要在内部借助参数宏来完成粘合过程

为了理解这一“结论”,我们不妨举一个例子:在前面的代码中,我们定义过一个用于自动关闭中断并在完成指定操作后自动恢复原来状态的safe_atom_code函数,现在我们把它改为宏来表示:


#define SAFE_ATOM_CODE(...)                          \
  {                                                    \
      uint32_t wTemp##__LINE__ = __disable_irq();   \
      __VA_ARGS__;                                   \
      __set_PRIMASK(wTemp##__LINE__);               \
  }

如果这里不适用##连接符,连接一个行号会出现什么情况?
由于这里定义了一个变量wTemp,而如果用户插入的代码中也使用了同名的变量,就会产生很多问题:轻则编译错误(重复定义);重则出现局部变量wTemp强行取代了用户自定义的静态变量的情况,从而直接导致系统运行出现随机性的故障(比如随机性的中断被关闭后不再恢复,或是原本应该被关闭的全局中断处于打开状态等等。

假设这里 SAFE_ATOM_CODE 所在行的行号是 123,那么我们期待的代码展开是这个样子的(我重新缩进过了):

  {                                                   
      uint32_t wTemp123 = __disable_irq();     
      __VA_ARGS__;                                    
      __set_PRIMASK(wTemp);                           
  }

然而,实际展开后的内容是这样的:

  {                                                   
      uint32_t wTemp__LINE__ = __disable_irq();     
      __VA_ARGS__;                                    
      __set_PRIMASK(wTemp);                           
  }

这里,__LINE__似乎并没有被正确替换为123,而是以原样的形式与wTemp粘贴到了一起——这就是很多人经常抱怨的 __ LINE __ 宏不稳定的问题。实际上,这是因为上述宏的构建没有遵守前面所列举的两条结论导致的。

从内容上看,SAFE_ATOM_CODE() 要粘合的对象并不是形参,根据结论第二条,需要借助另外一个参数宏来帮忙完成这一过程。为此,我们需要引入一个专门的宏:

#define CONNECT2(__A, __B)    __A##__B

注意到,这个参数宏要对形参进行胶水运算,根据结论第一条,需要在宏的外面再套一层,因此,修改代码得到:


#define __CONNECT2(__A, __B)    __A##__B
#define CONNECT2(__A, __B)      __CONNECT2(__A, __B)

#define __CONNECT3(__A, __B, __C)    __A##__B##__C
#define CONNECT3(__A, __B, __C)      __CONNECT3(__A, __B, __C)

修改前面的定义得到:

#define SAFE_ATOM_CODE(...)                           \
  {                                                   \
      uint32_t CONNECT2(wTemp,__LINE__) =              \
          __disable_irq();                            \
      __VA_ARGS__;                                    \
      __set_PRIMASK(CONNECT2(wTemp,__LINE__));          \
  }

3.可变参数宏的另一种写法

当我们定义一个变参宏时,除了使用预定义标识符 __ VA_ARGS __ 表示变参列表外,还可以使用下面这种写法。

#define LOG(fmt,args...) printf(fmt, args)

使用预定义标识符 VA_ARGS 来定义一个变参宏,是 C99 标准规定的写法。而上面这种格式是 GNU C 扩展的一个新写法。我们不再使用 VA_ARGS ,而是直接使用 args… 来表示一个变参列表,然后在后面的宏定义中,直接使用 args 代表变参列表就可以了。
跟上面一样,为了避免变参列表为空时的语法错误,我们也需要添加一个连接符##。

#define LOG(fmt,args...) printf(fmt,##args)
int main(void)
{
    LOG("hello\n");
    return 0;
}

使用这种方式,你会发现这种写法比使用 __ VA_ARGS __ 看起来更加直观和方便。

三、利用变参函数和可变参数宏实现自己的代码模块

1.实现函数重载

前边我们定义过CONNECT2, CONNECT3的宏,如果我们要粘连的字符串数量不同,比如,2个、4个、5个……n个,我们就要编写对应的版本:


#define __CONNECT2(__0, __1)            __0##__1
#define __CONNECT3(__0, __1, __2)       __0##__1##__2
#define __CONNECT4(__0, __1, __2, __3)  __0##__1##__2##__3
...
#define __CONNECT8(__0, __1, __2, __3, __4, __5, __6, __7)      \
           __0##__1##__2##__3##__4##__5##__6##__7
#define __CONNECT9(__0, __1, __2, __3, __4, __5, __6, __7, __8) \
           __0##__1##__2##__3##__4##__5##__6##__7##__8
           
//! 安全“套”           
#define CONNECT2(__0, __1)             __CONNECT2(__0, __1)
#define CONNECT3(__0, __1, __2)        __CONNECT3(__0, __1, __2)
#define CONNECT4(__0, __1, __2, __3)   __CONNECT4(__0, __1, __2, __3)
...
#define CONNECT8(__0, __1, __2, __3, __4, __5, __6, __7)        \
    __CONNECT8(__0, __1, __2, __3, __4, __5, __6, __7)
#define CONNECT9(__0, __1, __2, __3, __4, __5, __6, __7, __8)   \
    __CONNECT9(__0, __1, __2, __3, __4, __5, __6, __7, __8)

所谓宏的重载是说:我们不必亲自去数要粘贴的字符串的数量而“手工选取正确的版本”,而直接让编译器自己替我们挑选。

比如,我们举一个组装16进制数字的例子:

#define HEX_U8_VALUE(__B1, __B0)                         \
      CONNECT3(0x, __B1, __B0)

#define HEX_U16_VALUE(__B3, __B2, __B1, __B0)            \
      CONNECT5(0x, __B3, __B2, __B1, __B0)
            
#define HEX_U32_VALUE(__B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)\
      CONNECT9(0x, __B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)

在支持重载的情况下,我们希望这样使用:

#define HEX_U8_VALUE(__B1, __B0)                         \
      CONNECT(0x, __B1, __B0)

#define HEX_U16_VALUE(__B3, __B2, __B1, __B0)            \
      CONNECT(0x, __B3, __B2, __B1, __B0)
            
#define HEX_U32_VALUE(__B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)\
      CONNECT(0x, __B7, __B6, __B4, __B4, __B3, __B2, __B1, __B0)

无论实际给出的参数是多少个,我们都可以使用同一个参数宏CONNECT(),而CONNCT() 会自动计算用户给出参数的个数,从而正确的替换为CONNETn()版本。假设这一切都是可能做到的,那么实际上我们还可以对上述宏定义进行简化:

#define HEX_VALUE(...)          CONNECT(0x, __VA_ARGS__)

怎么实现宏的重载呢?为了简化这个问题,我们假设有一个“魔法宏”:它可以告诉我们用户实际传递了多少个参数,我们不妨叫它 VA_NUM_ARGS():

#define VA_NUM_ARGS(...)         /* 这里暂时先不管怎么实现 */

借助它,我们可以这样来编写宏 CONNECT():

#define CONNECT(...)                                \
    CONNECT2(CONNECT, VA_NUM_ARGS(__VA_ARGS__))     /*part1*/\
        (__VA_ARGS__)                               /*part2*/

当用户使用CONNECT()时,VA_NUM_ARGS(VA_ARGS)会给出参数的数量;“part1” 中 CONNECT2() 的作用就是将 字符串“CONNCET”与这个数组组合起来变成一个新的“参数宏的名字”;而 “part2” 的作用则是给这个组装出来的参数宏传递参数。

假设用户想用 HEX_VALUE() 组装一个数字

uint16_t hwValue = HEX_VALUE(D, E, A, D); //! 0xDEAD
它会被首先展开为:

uint16_t hwValue = CONNECT(0x, D, E, A, D); 

进而:


uint16_t hwValue = 
    CONNECT2(CONNECT, VA_NUM_ARGS(0x, D, E, A, D))
        (0x, D, E, A, D);

由于VA_NUM_ARGS() 告诉我们有5个参数,最终实际展开为:


uint16_t hwValue = 
    CONNECT5(0x, D, E, A, D);

那么我们就来逆推这个问题:如何实现我们的魔法宏“VA_NUM_ARGS()” 呢?答案如下:


#define VA_NUM_ARGS_IMPL(_1,_2,_3,_4,_5,_6,_7,_8,_9,__N,...) __N
#define VA_NUM_ARGS(...)                                                \
            VA_NUM_ARGS_IMPL(__VA_ARGS__,9,8,7,6,5,4,3,2,1)

这里,首先构造了一个特殊的参数宏,VA_NUM_ARGS_IMPL():

  • 在涉及"…"之前,它要用用户至少传递10个参数;

  • 这个宏的返回值就是第十个参数的内容;

  • 多出来的部分会被"…"吸收掉,不会产生任何后果

VA_NUM_ARGS() 的巧妙在于,它把__VA_ARGS__放在了参数列表的最前面,并随后传递了 “9,8,7,6,5,4,3,2,1” 这样的序号:

  1. 当__VA_ARGS__里有1个参数时,“1”对应第十个参数__N,所以返回值是1
  2. 当__VA_ARGS__里有2个参数时,“2”对应第十个参数__N,所以返回值是2
  3. 当__VA_ARGS__里有9个参数时,"9"对应第十个参数__N,所以返回值是9

如果觉得上述过程似懂非懂,我们不妨对前面的例子做一个展开:

VA_NUM_ARGS(0x, D, E, A, D)

展开为:

VA_NUM_ARGS_IMPL(0x, D, E, A, D,9,8,7,6,5,4,3,2,1)

从左往右数,第十个参数,正好是“5”。

宏的重载非常有用,可以极大的简化用户"选择困难",你甚至可以将VA_NUM_ARGS() 与 函数名结合在一起,从而实现简单的函数重载(即,函数参数不同的时候,可以通过这种方法在编译阶段有预编译器根据用户输入参数的数量自动选择对应的函数),比如:

extern device_write1(const char *pchString);
extern device_write2(uint8_t *pchStream, uint_fast16_t hwLength);
extern device_write3(uint_fast32_t wAddress, uint8_t *pchStream, uint_fast16_t hwLength);

#define device_write(...)                                       \
            CONNECT2(device_write, VA_NUM_ARGS(__VA_ARGS__))    \
                (__VA_ARGS__)

使用时:

device_write("hello world");       //!< 发送字符串

extern uint8_t chBuffer[32];
device_write(chBuffer, 32);        //!< 发送缓冲

//! 向指定偏移量写数据
#define LCD_DISP_MEM_START      0x4000xxxx
extern uint16_t hwDisplayBuffer[320*240];
device_write(
    LCD_DISP_MEM_START, 
    (uint8_t *)hwDisplayBuffer, 
    sizeof(hwDisplayBuffer)
);

2.实现using结构

在C#中有一个类似的语法,叫做 using(),其典型的用法如下:

using (StreamReader tReader = File.OpenText(m_InputTextFilePath))
{
    while (!tReader.EndOfStream)
    {
        ...
    }
}

以上述代码为例进行讲解:

  • 在 using 圆括号内定义的变量,其生命周期仅覆盖 using 紧随其后的花括号内部;

  • 当用于代码离开 using 结构的时候,using 会自动执行一个“扫尾工作”,而这个扫尾工作是对应的类事先定义好的。在上述例子中,所谓的扫尾工作就是关闭 与 类StreamReader的实例tReader 所关联的文件——简单说就是using会自动把文件关闭,而不必用户亲自动手。

要实现类似using的结构,首先要考虑如何构造一个"至执行一次"的for循环结构。要做到这一点,毫无难度:

for (int i = 1; i > 0; i++) {
    ...
}

为实现using的效果,定义如下宏:

#define using(__declare, __on_enter_expr, __on_leave_expr)   \
            for (__declare, *_ptr = NULL;                    \
                 _ptr++ == NULL ?                            \
                    ((__on_enter_expr),1) : 0;               \
                 __on_leave_expr                             \
                )

为了验证我们的结果,不妨写一个简单的代码:

using(int a = 0,printf("========= On Enter =======\r\n"), 
                printf("========= On Leave =======\r\n")) 
{
    printf("\t In Body a=%d \r\n", ++a);
} 

这是对应的执行效果:

========= On Enter =======
In Body a=1
========= On Leave =======

我们不妨将上述的宏进行展开,一个可能的结果是:

for (int a = 0, *_ptr = NULL; 
     _ptr++ == NULL ? ((printf("========= On Enter =======\r\n")),1) : 0; 
     printf("========= On Leave =======\r\n") ) 
{
    printf("\t In Body a=%d \r\n", ++a);
}

接下来,为了提高宏的鲁棒性,我们可以继续做一些改良,比如给指针一个唯一的名字:


#define using(__declare, __on_enter_expr, __on_leave_expr)                   \
            for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
                 CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \
                    ((__on_enter_expr),1) : 0;                                  \
                 __on_leave_expr                                                \
                )

更进一步,如果用户有不同的需求:比如想定义两个以上的局部变量,或是想省确 __on_enter_expr 或者是 __on_leave_expr ——我们完全可以定义多个不同版本的 using:

#define __using1(__declare)                                                     \
            for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
                 CONNECT3(__using_, __LINE__,_ptr)++ == NULL;                   \
                )


#define __using2(__declare, __on_leave_expr)                                    \
            for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
                 CONNECT3(__using_, __LINE__,_ptr)++ == NULL;                   \
                 __on_leave_expr                                                \
                )


#define __using3(__declare, __on_enter_expr, __on_leave_expr)                   \
            for (__declare, *CONNECT3(__using_, __LINE__,_ptr) = NULL;          \
                 CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \
                    ((__on_enter_expr),1) : 0;                                  \
                 __on_leave_expr                                                \
                )

#define __using4(__dcl1, __dcl2, __on_enter_expr, __on_leave_expr)              \
            for (__dcl1, __dcl2, *CONNECT3(__using_, __LINE__,_ptr) = NULL;     \
                 CONNECT3(__using_, __LINE__,_ptr)++ == NULL ?                  \
                    ((__on_enter_expr),1) : 0;                                  \
                 __on_leave_expr                                                \
                )

借助宏的重载技术,我们可以根据用户输入的参数数量自动选择正确的版本:

#define using(...)  \
    CONNECT2(__using, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)

至此,我们完成了对 for 的改造,并提出了__using1, __using2, __using3 和 __using4 四个版本变体

3.实现一个原子操作宏

我们曾有意无意的提供过一个实现原子操作的封装:即在代码的开始阶段关闭全局中断并记录此前的中断状态;执行用户代码后,恢复关闭中断前的状态。其代码如下:

#define SAFE_ATOM_CODE(...)                               \
{                                                         \
    uint32_t CONNECT2(temp, __LINE__) = __disable_irq();  \
    __VA_ARGS__                                           \
    __set_PRIMASK((CONNECT2(temp, __LINE__)));            \
}

唯一的问题是,这样的写法,在调试时完全没法在用户代码处添加断点(编译器会认为宏内所有的内容都写在了同一行),这是大多数人不喜欢使用宏来封装代码结构的最大原因。借助 __using2,我们可以轻松的解决这个问题:

#define SAFE_NAME(__NAME)   CONNECT3(__,__NAME,__LINE__)
#   define safe_atom_code()                                                     \
            using(  uint32_t SAFE_NAME(temp) =                                  \
                        ({  uint32_t SAFE_NAME(temp2)=__get_PRIMASK();          \
                            __disable_irq();                                    \
                            SAFE_NAME(temp2);}),                                \
                        __set_PRIMASK(SAFE_NAME(temp)))

4.实现foreach结构

很多高级语言都有专门的 foreach 语句,用来实现对数组(或是链表)中的元素进行逐一访问。原生态C语言并没有这种奢侈,即便如此,Linux也定义了一个“野生”的 foreach 来实现类似的功能。为了演示如何使用 using 结构来构造 foreach,我们不妨来看一个例子:

typedef struct example_lv0_t {
    uint32_t    wA;
    uint16_t    hwB;
    uint8_t     chC;
    uint8_t     chID;
} example_lv0_t;

example_lv0_t s_tItem[8] = {
    {.chID = 0},
    {.chID = 1},
    {.chID = 2},
    {.chID = 3},
    {.chID = 4},
    {.chID = 5},
    {.chID = 6},
    {.chID = 7},
};

我们希望实现一个函数,能通过 foreach 自动的访问数组 s_tItem 的所有成员,比如:

foreach(example_lv0_t, s_tItem) {
    printf("Processing item with ID = %d\r\n", _.chID);
}

使用 “_” 表示当前循环下的元素。在这个例子中,为了使用 foreach,我们需要提供至少两个信息:目标数组元素的类型(example_lv0_t)和目标数组(s_tItem)。

这里的难点在于,如何定义一个局部的指针,并且它的作用范围仅仅只覆盖 foreach 的循环体。 __with1() 的功能就是允许用户定义一个局部变量,并覆盖由第三方所编写的、由 {} 包裹的区域:

#define dimof(__array)          (sizeof(__array)/sizeof(__array[0]))

#define foreach(__type, __array)                                               \
            __using1(__type *_p = __array)                                         \
            for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \
                    CONNECT2(count,__LINE__) > 0;                               \
                    _p++, CONNECT2(count,__LINE__)--                            \
                )

为了方便理解,我们不妨将前面的例子代码进行宏展开:

for (example_lv0_t *_p = s_tItem, *__using_177_ptr = NULL; 
     __using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
     ) 
     for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0])); 
           count177 > 0; 
           _p = _p+1, count177-- ) 
     {
        printf("Processing item with ID = %d\r\n", (*_p).chID);
     }

允许用户再指定一个专门的局部变量,用于替代"_" 表示当前循环下的对象:


#define foreach2(__type, __array)                                               \
            using(__type *_p = __array)                                         \
            for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \
                    CONNECT2(count,__LINE__) > 0;                               \
                    _p++, CONNECT2(count,__LINE__)--                            \
                )

#define foreach3(__type, __array, __item)                                       \
            using(__type *_p = __array, *__item = _p, _p = _p, )                \
            for (   uint_fast32_t CONNECT2(count,__LINE__) = dimof(__array);    \
                    CONNECT2(count,__LINE__) > 0;                               \
                    _p++, __item = _p, CONNECT2(count,__LINE__)--               \
                )

这里的 foreach3 提供了3个参数,其中最后一个参数就是用来由用户“额外”指定新的指针的;与之相对,老版本的foreach我们称之为 foreach2,因为它只需要两个参数,只能使用"_"作为对象的指代。进一步的,我们可以使用宏的重载来简化用户的使用:

#define foreach(...)        \
    CONNECT2(foreach, VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)

经过这样的改造,我们可以用下面的方法来为我们的循环指定一个叫做"ptItem"的指针:

foreach(example_lv0_t, s_tItem, ptItem) {
    printf("Processing item with ID = %d\r\n", ptItem->chID);
}

展开后的形式如下:

for (example_lv0_t *_p = s_tItem, ptItem = _p, *__using_177_ptr = NULL; 
     __using_177_ptr++ == NULL ? ((_p = _p),1) : 0;
     )
     for ( uint_fast32_t count177 = (sizeof(s_tItem)/sizeof(s_tItem[0])); 
           count177 > 0; 
           _p = _p+1, ptItem = _p, count177-- ) 
     {
           printf("Processing item with ID = %d\r\n", ptItem->chID);
     }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言变参函数和可变参数宏 的相关文章

  • android进阶---【注解(一)之运行时注解】

    android进阶 注解 注解1 什么是注解2 注解的产生3 注解的基础介绍3 1元注解3 2运行时注解与编译时注解区别 4 自定义注解4 1自定义编写规则4 2自定义运行时注解 注解 注解这个概念 xff0c 有些人可能会有些陌生 但是撸
  • 设计容器 实现put get getCount 方法,生产者消费者问题

    设计一个容器 xff0c 支持put get getCount 方法 xff0c 满足两个生产者 二十个消费者阻塞调用 public class ProdConsuCont static ReentrantLock lock 61 new
  • C++程序员必看书单

    转载 xff1a https blog csdn net ljy1988123 article details 7748913 comments C 43 43 xff1a Prata C 43 43 Primer Plus xff1a 基
  • git submodule 升级commit并push

    git submodule 升级commit并push 关于这个问题 xff0c 可以参照以下文章 xff1a https blog csdn net wwj 748 article details 73991862 流程写的很清楚 xff
  • 欧拉角pitch、yaw,roll的理解

    关于旋转永远是做游戏的难点和混乱点 我们知道表示一个旋转有多种方式 xff0c 简单的欧拉角 xff0c 复杂点的四元数 xff0c 再复杂点的矩阵 之前接触unity可以用四元数和欧拉角两种方式表示旋转 xff0c 最近一直研究虚幻引擎
  • Mac执行ruby命令提示 dyld: Library not loaded等类似问题解决方案

    说一下为啥会遇见这么个问题 xff0c 我在给一个xcode项目添加podfile的时候 xff0c 在终端执行了pod init命令 xff0c 随即给了我一个如下图的提示 xff08 报错信息一样的 xff0c 执行pod的命令早就被解
  • 编程题:多线程交替打印ABC

    要求创建3个线程 xff0c 分别打印ABC xff0c 共交替打印10次 span class token keyword public span span class token keyword class span span clas
  • Android App Bundle 自动打包原理

    Google推出Android App Bundle 已经有一段时间了 根据Google的政策说明 xff0c 预计2021年8月之后 xff0c 新发布的应用都必须使用Android App Bundle aab 来上架Google Pl
  • 【玩转Linux】Linux虚拟机设置固定IP

    Linux虚拟机Centos系统的ip总是变化 xff0c 如何固定下来 xff1f 尝试了好多方式 xff0c 终于找到一种最为简单的方法 文章目录 1 查看Centos的IP信息2 修改文件3 刷新网络配置 1 查看Centos的IP信
  • Docker安装MySQL数据库

    文章目录 一 简单方式二 挂载方式1 创建挂载目录2 启动容器 三 修改配置文件1 新建my cnf2 编辑my cnf3 查看是否生效 一 简单方式 span class token function docker span run sp
  • 解决Docker镜像拉取失败问题

    一 问题 Docker拉取mysql镜像 xff0c 发生报错 span class token function docker span pull mysql 8 0 22 报错信息 xff1a Error response from d
  • Docker安装RabbitMQ消息队列

    文章目录 1 启动容器2 连接访问 1 启动容器 span class token function docker span run name rabbitmq span class token punctuation span resta
  • 【玩转Linux】Linux安装宝塔面板

    文章目录 一 简介二 安装1 centos脚本安装2 浏览器访问 三 总结 一 简介 宝塔面板 xff0c 是安全高效的服务器运维面板 xff0c 一个提升运维效率的服务器管理软件 xff0c 支持一键LAMP LNMP 集群 监控 网站
  • 使用JDK的keytool工具生成JKS证书

    使用JDK的keytool工具生成JKS证书 文章目录 1 生成JKS证书2 查看JKS证书详细信息3 导出证书 1 生成JKS证书 keytool genkey alias jwt keyalg RSA keystore jwt jks
  • 【算法】二分查找

    算法 二分查找 题目 xff1a 请实现无重复数字的升序数组的二分查找 难度 xff1a 简单 代码 xff1a 二分查找 xff0c 又叫折半查找 xff0c 要求待查找的序列有序 每次取中间位置的值与待查关键字比较 xff0c 如果中间
  • 【算法】反转链表

    算法 反转链表 题目 xff1a 给定一个单链表的头结点pHead xff0c 长度为n xff0c 反转该链表后 xff0c 返回新链表的表头 难度 xff1a 简单 代码 xff1a span class token keyword c
  • 【算法】合并两个排序的链表

    算法 合并两个排序的链表 题目 xff1a 输入两个递增的链表 xff0c 单个链表的长度为n xff0c 合并这两个链表并使新链表中的节点仍然是递增排序的 难度 xff1a 简单 代码 xff1a span class token key
  • 【算法】判断链表中是否有环

    算法 判断链表中是否有环 题目 xff1a 判断给定的链表中是否有环 如果有环则返回true xff0c 否则返回false 难度 xff1a 简单 代码 xff1a span class token keyword public span
  • 教你快速高效接入SDK——手游聚合SDK的总体思路和架构

    U8SDK技术博客 xff1a http www uustory com xff0c 欢迎来坐坐 百度传课已经停运 xff0c 最新U8SDK视频教程已经转移至B站 xff1a U8SDK最新视频教程 题记 xff1a 很多做游戏开发的人

随机推荐

  • 【算法】删除链表的倒数第N个结点

    算法 删除链表的倒数第N个结点 题目 xff1a 给你一个链表 xff0c 删除链表的倒数第 n 个结点 xff0c 并且返回链表的头结点 难度 xff1a 中等 代码 xff1a span class token keyword publ
  • 阿里云智能编码插件Cosy,提升开发效率杠杠滴!

    文章目录 一 简介二 核心功能1 代码智能补全2 代码示例搜索 三 产品特性1 提升编码效率2 沉浸式开发3 低资源消耗4 隐私保护 四 快速开始1 安装IntelliJ IDEA插件2 测试代码智能补全3 测试代码示例搜索 五 总结 一
  • Alibaba Cloud Toolkit轻量部署插件,一键发布服务器

    文章目录 一 简介二 部署方式1 传统部署方式2 Cloud Toolkit部署方式 四 产品功能五 部署步骤1 安装插件2 添加主机3 Deploy to Host4 控制台5 服务器 六 服务器代码热部署七 Arthas诊断 一 简介
  • 新人一看就懂:Dubbo3 + Nacos的RPC远程调用框架demo

    文章目录 一 前言Feign和Dubbo到底有啥区别 xff0c 为啥大厂都爱用RPC框架 xff1f 二 简介三 dubbo api xff08 对外暴漏的接口 xff09 1 TestService接口 四 dubbo provider
  • Docker安装Kafka消息队列

    文章目录 1 安装zookeeper2 安装kafka3 安装kafka map xff08 可选 xff09 1 安装zookeeper span class token function docker span run span cla
  • 【Spring Boot实战与进阶】集成Kafka消息队列

    汇总目录链接 xff1a Spring Boot实战与进阶 学习目录 文章目录 一 简介二 集成Kafka消息队列1 引入依赖2 配置文件3 测试生产消息4 测试消费消息 一 简介 Kafka是由Apache软件基金会开发的一个开源流处理平
  • Hutool工具类之excel导入导出

    文章目录 1 导入excel2 导出excel 1 导入excel span class token class name ExcelReader span reader span class token operator 61 span
  • Docker安装RockerMQ消息队列

    文章目录 1 安装namesrv2 安装broker3 安装console xff08 可选 xff09 1 安装namesrv namesrv就类似于消息队列的注册中心 span class token function docker s
  • 【Spring Boot实战与进阶】集成RockerMQ消息队列

    汇总目录链接 xff1a Spring Boot实战与进阶 学习目录 文章目录 一 简介二 集成RockerMQ消息队列1 引入依赖2 配置文件3 测试生产消息4 测试消费消息 一 简介 RocketMQ 是阿里巴巴在2012年开源的分布式
  • linux下elasticsearch 安装、配置及示例

    简介 开始学es xff0c 我习惯边学边记 xff0c 总结出现的问题和解决方法 本文是在两台linux虚拟机下 xff0c 安装了三个节点 本次搭建es同时实践了两种模式 单机模式和分布式模式 条件允许的话 xff0c 可以在多台机器上
  • Java服务器热部署的实现原理

    今天发现早年在大象笔记中写的一篇笔记 xff0c 之前放在ijavaboy上的 xff0c 现在它已经访问不了了 前几天又有同事在讨论这个问题 这里拿来分享一下 在web应用开发或者游戏服务器开发的过程中 xff0c 我们时时刻刻都在使用热
  • win10打开热点提示:我们无法设置移动热点

    解决方案 xff1a 1 右键我的电脑 xff0c 打开管理 2 双击带有下载标记wi fi 适配器 xff0c 点击启用设备 xff0c 确认即可
  • Docker容器打包迁移

    有时在一个docker内部署的容器不想在其它服务器容器上重新部署 xff0c 再配置配置文件 这时直接将当前的容器打包然后直接部署到新的服务器上即可 xff0c 免了再配置一遍 1 将容器保存为镜像 先把容器停止运行不然会有问题 容器保存为
  • 四旋翼飞行器的原理研究和建模

    四旋翼飞行器的原理研究和建模 对四旋翼飞行器的工作原理进行了简单介绍 xff0c 对其飞行姿态角进行描述 xff0c 并在此基础上建立数学模型 四旋翼飞行器的原理 根据四旋翼飞行器的运动方式的特点将其飞行控制划分为四种基本的飞行控制方式 1
  • 数组名和函数名是什么东西

    数组名和函数名的本质都是一个 指向数组首地址或函数体的指针常量 的名字 规则1 xff1a 数组 61 指向数组首地址的指针常量 43 数组元素 简单说就是 xff0c 数组 61 指针常量 43 数组内容 xff0c 数组名就是这个指针常
  • RT-Thread进阶之低功耗PM组件应用笔记

    电源管理组件 嵌入式系统低功耗管理的目的在于满足用户对性能需求的前提下 xff0c 尽可能降低系统能耗以延长设备待机时间 高性能与有限的电池能量在嵌入式系统中矛盾最为突出 xff0c 硬件低功耗设计与软件低功耗管理的联合应用成为解决矛盾的有
  • 【STM32H750】玩转ART-Pi(一)——使用STM32CUBMX生成TouchGFX工程

    目录 STM32H750 玩转ART Pi xff08 一 xff09 使用STM32CUBMX生成TouchGFX工程 STM32H750 玩转ART Pi xff08 二 xff09 制作MDK的外部QSPI FLASH烧录算法 STM
  • 【STM32H750】玩转ART-Pi(二)——制作MDK的外部QSPI-FLASH烧录算法

    目录 STM32H750 玩转ART Pi xff08 一 xff09 使用STM32CUBMX生成TouchGFX工程 STM32H750 玩转ART Pi xff08 二 xff09 制作MDK的外部QSPI FLASH烧录算法 STM
  • linux驱动开发篇(四)—— platform平台设备驱动

    linux系列目录 xff1a linux基础篇 xff08 一 xff09 GCC和Makefile编译过程 linux基础篇 xff08 二 xff09 静态和动态链接 ARM裸机篇 xff08 一 xff09 i MX6ULL介绍 A
  • C语言变参函数和可变参数宏

    文章目录 一 变参函数的设计与实现1 变参函数初体验2 变参函数改进版3 变参函数 V3 0 版本3 变参函数 V4 0 版本4 变参函数 V5 0 版本 二 可变参数宏的设计与实现1 什么是可变参数宏2 宏连接符 的作用3 可变参数宏的另