全是通俗易懂的讲解,如果你本节之前的知识都掌握清楚,那就速速来看我的项目笔记吧~   

相关技术知识补充


不定参宏函数

在 C 语言中,不定参宏函数是一种强大的工具,它允许宏接受可变数量的参数,类似于不定参函数,不过宏是在预处理阶段展开的。下面详细介绍不定参宏函数的使用,以自定义日志打印宏为例。

代码示例

#include <stdio.h>



// 定义不定参宏函数,用于日志打印

#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__)



int main() {

    int number = 42;

    char name[] = "Alice";



    // 使用不定参宏函数进行日志打印

    LOG("The number is %d.\n", number);

    LOG("The name is %s.\n", name);

    LOG("Combined: %s's lucky number is %d.\n", name, number);



    return 0;

}

代码解释

1. 宏定义部分

#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__)
  • LOG 是宏的名称,它模拟了一个简单的日志打印功能。
  • fmt 是一个固定参数,它代表了 printf 函数的格式控制字符串。
  • ... 表示可变参数部分,意味着在调用 LOG 宏时可以传入任意数量的额外参数。
  • __FILE__ 和 __LINE__ 是预定义的宏,__FILE__ 会在预处理时被替换为当前源文件的文件名,__LINE__ 会被替换为宏调用所在的代码行号。
  • __VA_ARGS__ 是一个特殊的预定义宏,它会被替换为调用宏时传入的可变参数列表。

2. 主函数部分

int main() {

    int number = 42;

    char name[] = "Alice";



    LOG("The number is %d.\n", number);

    LOG("The name is %s.\n", name);

    LOG("Combined: %s's lucky number is %d.\n", name, number);



    return 0;

}
  • 首先定义了一个整数变量 number 和一个字符数组 name。
  • 然后多次调用 LOG 宏进行日志打印,每次调用传入不同数量的参数。
    • 第一次调用 LOG("The number is %d.\n", number); 时,fmt 被替换为 "The number is %d.\n",__VA_ARGS__ 被替换为 number。
    • 第二次调用 LOG("The name is %s.\n", name); 时,fmt 被替换为 "The name is %s.\n",__VA_ARGS__ 被替换为 name。
    • 第三次调用 LOG("Combined: %s's lucky number is %d.\n", name, number); 时,fmt 被替换为 "Combined: %s's lucky number is %d.\n",__VA_ARGS__ 被替换为 name, number。

输出结果

假设上述代码保存为 main.c,编译运行后可能的输出如下:

[main.c:11] The number is 42.

[main.c:12] The name is Alice.

[main.c:13] Combined: Alice's lucky number is 42.

        这样,通过不定参宏函数,我们可以方便地在日志中记录文件名和行号信息,同时灵活处理不同数量的参数。

        若要让 LOG 宏函数支持像 LOG("A charmer") 这种只传入一个参数的情况,就需要处理可变参数为空的情形。在 C 语言里,当可变参数为空时,__VA_ARGS__ 会在宏展开时产生一个多余的逗号,这会引发编译错误。为解决此问题,可借助 ## 操作符,它能在可变参数为空时去除多余的逗号。

#include <stdio.h>



// 定义支持空可变参数的不定参宏函数

#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__)



int main() {

    // 传入多个参数

    LOG("The name is %s.\n", "A charmer");

    // 只传入一个参数

    LOG("A charmer\n");



    return 0;

}   
  • 宏定义:#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__),这里的 ## 操作符是关键,当可变参数 __VA_ARGS__ 为空时,它会移除前面多余的逗号,避免编译错误。

C风格不定参函数

在 C 语言里,不定参函数指的是可以接受可变数量参数的函数。为了实现不定参函数,需要用到 <stdarg.h> 头文件里的一些宏,这些宏能够帮助你访问可变参数列表。下面为你提供一个打印整数的不定参函数示例,同时会详细解释代码的逻辑。

#include <stdio.h>
#include <stdarg.h>

// 不定参函数,用于打印多个整数
void print_integers(int count, ...) {
    // 定义 va_list 类型的变量,用于存储可变参数列表信息
    va_list args;
    // 初始化可变参数列表,count 为最后一个固定参数,表示可变参数的数量
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        // 从可变参数列表中取出一个 int 类型的参数
        int num = va_arg(args, int);
        // 打印取出的整数
        printf("%d ", num);
    }
    // 换行
    printf("\n");

    // 结束对可变参数列表的使用,释放相关资源
    va_end(args);
}

int main() {
    // 调用 print_integers 函数,传入可变参数的数量 5 以及 5 个整数
    print_integers(5, 1, 2, 3, 4, 5);
    return 0;
}    

代码解释

  1. 头文件包含

    • #include <stdio.h>:提供标准输入输出函数,例如 printf
    • #include <stdarg.h>:提供处理可变参数列表所需的宏和类型。
  2. print_integers 函数

    • va_list args:定义一个 va_list 类型的变量 args,用于存储可变参数列表的信息。
    • va_start(args, count):初始化可变参数列表,count 是最后一个固定参数,它表示后续可变参数的数量。
    • va_arg(args, int):从可变参数列表中取出一个 int 类型的参数。
    • va_end(args):结束对可变参数列表的使用,释放相关资源。
  3. main 函数

    • 调用 print_integers 函数,传入可变参数的数量 5 以及 5 个整数 12345。程序会将这些整数依次打印出来。

        这个示例展示了如何使用不定参函数来打印多个整数,你可以根据需求修改调用时传入的参数数量和具体整数值。


 模拟实现printf

对字符串处理

下面是一个模拟实现 printf 函数的 C 语言代码示例,该示例支持 %d(打印整数)、%s(打印字符串)和 %c(打印字符)这几种常见的格式说明符。

#include <stdio.h>
#include <stdarg.h>

// 模拟实现 printf 函数
void my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    while (*format) {
        if (*format == '%') {
            format++;
            switch (*format) {
                case 'd': {
                    int num = va_arg(args, int);
                    printf("%d", num);
                    break;
                }
                case 's': {
                    char *str = va_arg(args, char *);
                    printf("%s", str);
                    break;
                }
                case 'c': {
                    int ch = va_arg(args, int);
                    printf("%c", (char)ch);
                    break;
                }
                default:
                    putchar(*format);
                    break;
            }
        } else {
            putchar(*format);
        }
        format++;
    }

    va_end(args);
}

int main() {
    int num = 123;
    char *str = "Hello";
    char ch = 'A';

    my_printf("Number: %d, String: %s, Character: %c\n", num, str, ch);
    return 0;
}    

代码解释

  1. 头文件包含

    • #include <stdio.h>:提供标准输入输出函数,如 printf 和 putchar
    • #include <stdarg.h>:提供处理可变参数列表所需的宏和类型。
  2. my_printf 函数

    • va_list args:定义一个 va_list 类型的变量 args,用于存储可变参数列表的信息。
    • va_start(args, format):初始化可变参数列表,format 是最后一个固定参数。
    • 遍历 format 字符串:
      • 当遇到 % 时,检查下一个字符:
        • 如果是 d,使用 va_arg(args, int) 取出一个 int 类型的参数并打印。
        • 如果是 s,使用 va_arg(args, char *) 取出一个字符串指针并打印。
        • 如果是 c,使用 va_arg(args, int) 取出一个字符(以 int 类型存储)并打印。
        • 对于其他字符,直接输出该字符。
      • 如果不是 %,直接输出该字符。
    • va_end(args):结束对可变参数列表的使用,释放相关资源。
  3. main 函数

    • 定义一个整数 num、一个字符串 str 和一个字符 ch
    • 调用 my_printf 函数,传入格式化字符串和相应的参数。

这个示例只是一个简单的模拟实现,真正的 printf 函数支持更多的格式说明符和复杂的功能。

 

用vasprintf接口⭐

vasprintf 是一个标准 C 库函数,它可以根据格式化字符串和可变参数列表动态分配内存并生成格式化后的字符串。下面是一个使用 vasprintf 接口模拟实现 printf 功能的示例代码: 

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>

// 模拟实现 printf 函数,使用 vasprintf
void my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    char *output = NULL;
    // 使用 vasprintf 动态分配内存并格式化字符串
    if (vasprintf(&output, format, args) == -1) {
        perror("vasprintf");
        va_end(args);
        return;
    }

    // 输出格式化后的字符串
    printf("%s", output);

    // 释放动态分配的内存
    free(output);

    va_end(args);
}

int main() {
    int num = 42;
    const char *str = "World";
    char ch = '!';

    my_printf("Hello, %s %d%c\n", str, num, ch);
    return 0;
}    

代码解释

1. 头文件包含
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
  • stdio.h:提供标准输入输出函数,如 printf 和 perror
  • stdarg.h:提供处理可变参数列表所需的宏和类型,如 va_listva_startva_end
  • stdlib.h:提供动态内存分配和释放函数,如 mallocfreevasprintf 也依赖此头文件。
2. my_printf 函数
void my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    char *output = NULL;
    if (vasprintf(&output, format, args) == -1) {
        perror("vasprintf");
        va_end(args);
        return;
    }

    printf("%s", output);
    free(output);

    va_end(args);
}
  • va_list args:定义一个 va_list 类型的变量 args,用于存储可变参数列表的信息。
  • va_start(args, format):初始化可变参数列表,format 是最后一个固定参数。
  • vasprintf(&output, format, args):根据格式化字符串 format 和可变参数列表 args 动态分配内存并生成格式化后的字符串,将结果存储在 output 指针指向的内存区域。如果分配内存失败,vasprintf 返回 -1。
  • perror("vasprintf"):如果 vasprintf 失败,使用 perror 输出错误信息。
  • printf("%s", output):输出格式化后的字符串。
  • free(output):释放 vasprintf 动态分配的内存,避免内存泄漏。
  • va_end(args):结束对可变参数列表的使用,释放相关资源。
3. main 函数
int main() {
    int num = 42;
    const char *str = "World";
    char ch = '!';

    my_printf("Hello, %s %d%c\n", str, num, ch);
    return 0;
}

  • 定义一个整数 num、一个字符串 str 和一个字符 ch
  • 调用 my_printf 函数,传入格式化字符串和相应的参数,模拟 printf 函数的功能。

通过这种方式,我们利用 vasprintf 接口实现了一个简单的 printf 模拟函数。


 C++风格不定参函数

#include <iostream>

// 无参的 xprintf 函数,用于递归终止
void xprintf() {
    std::cout << std::endl;
}

// 可变参数模板的 xprintf 函数
template<typename T, typename... Args>
void xprintf(const T &v, Args &&...args) {
    std::cout << v;
    if ((sizeof...(args)) > 0) {
        xprintf(std::forward<Args>(args)...);
    } else {
        xprintf();
    }
}

int main() {
    xprintf(1);
    xprintf(1, 2, 3);
    return 0;
}    

 

代码功能概述

这段 C++ 代码实现了一个名为 xprintf 的不定参数函数,其功能是将传入的不定数量的参数依次输出到控制台,每个 xprintf 调用结束后会换行。

  • 模板参数
    • template<typename T, typename... Args>:定义了一个可变参数模板。T 代表第一个参数的类型,typename... Args 是可变参数包,它能容纳零个或多个不同类型的参数。
  • 函数参数
    • const T &v:第一个参数的常量引用,采用常量引用可避免不必要的拷贝,提高性能。
    • Args &&...args:可变参数包,使用右值引用(也叫万能引用),它既可以绑定左值,也可以绑定右值,并且结合 std::forward 能实现完美转发。
  • 函数体逻辑
    • std::cout << v;:输出第一个参数。
    • if ((sizeof...(args)) > 0)sizeof...(args) 用于获取可变参数包中参数的数量。若数量大于 0,说明还有剩余参数,就递归调用 xprintf 函数,同时使用 std::forward 对剩余参数进行完美转发,以保留参数的左值或右值属性。
    • else 分支:若可变参数包为空,调用无参的 xprintf() 函数,输出换行符,结束递归。

为什么要xprintf(),而不是直接cout<<endl?

当你调用 xprintf(1) 时,编译器会进行模板实例化的过程。在这个过程中:

 
  1. 模板参数推导:对于 template<typename T, typename... Args> ,根据传入的参数 1(类型为 int),T 被推导为 int。而 Args 被推导为空参数包,因为此时只有一个参数 1 传入,没有其他参数来构成可变参数包了。
  2. 函数体执行
    • std::cout << v; 这行代码会将 v(也就是 1)输出到标准输出流。
    • 接下来判断 if ((sizeof...(args)) > 0) ,由于 Args 被推导为空参数包,sizeof...(args) 的值为 0,所以这个条件不成立,会执行 else 分支。
    • 在 else 分支中,代码是 std::cout << std::endl; ,这一步本身不会有问题,它会输出一个换行符。

但是,这里存在一个潜在的问题,就是在这个函数模板中,xprintf 函数本身是递归调用的(xprintf(std::forward<Args>(args)...); 这一行)。当编译器处理函数调用时,它需要知道在所有可能的情况下,xprintf 调用都有对应的函数定义可以匹配。

 

在 xprintf(1) 这种情况下,虽然当前这次调用不会触发递归调用(因为可变参数包为空),但编译器在编译时并不能保证未来不会在其他情况下触发递归调用到一个无参数的 xprintf 调用。也就是说,从编译器的角度来看,为了保证函数调用的完整性和正确性,它需要找到一个无参数的 xprintf 函数定义,以便在递归过程中可能出现的无参数调用时能够正确解析。

 

由于在没有定义无参 xprintf 函数的情况下,当编译器遇到这种可能的无参数调用情况(即使在当前调用中不会实际发生),它找不到合适的函数定义来匹配,就会报错,提示没有找到匹配的 xprintf 函数。

 

简单来说,就是编译器为了确保函数调用在各种情况下都能正确解析,要求有一个无参的 xprintf 函数定义来应对递归调用中可能出现的无参数调用场景,即使当前这次调用不会触发这个情况。


 如果你对日志系统感到兴趣,欢迎关注我👉【A charmer】

后续我将继续带你实现日志系统~

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐