目录

一、C语言的可变参数:基于栈帧的手动读取

(1)C函数调用的栈帧结构

(2)C 可变参数的 4 个核心宏:如何 “手动读栈”

(3)实战代码:用 C 可变参数实现求和函数

(4)C 可变参数的缺点:没有类型安全

二、可变宏函数

(1)核心语法:

(2)实战1:用可变宏实现日志打印

(3) 实战 2:处理 “无可变参数” 的情况

(4)底层原理:预处理阶段的文本替换​

三、C++的可变参数模板:编译器的“自动解包”

(1)核心语法:...的妙用


        在写日志的时候,以往都是直接用cout、printf等函数。但是每次都需要在前面加上线程ID、时间等信息,没有什么复用性,然后想自己写一个日志组件,方便日后开发。

        但是在学习日志组件的时候,发现有一些前置知识以前可能只是了解过,并未真正弄懂他的细节。本篇文章就谈谈日志组件中最为重要的可变参数部分,将从C语言函数栈帧出发,理解C形式的不定参数原理,再讨论一下C++对其优化及使用。

一、C语言的可变参数:基于栈帧的手动读取

        C 语言通过<stdarg.h>头文件提供的一套宏(va_list、va_start、va_arg、va_end)实现可变参数,其核心依赖函数栈帧的内存布局—— 因为 C 语言函数调用时,参数会按固定顺序压入栈中,我们可以通过栈指针 “手动” 读取这些参数。

(1)C函数调用的栈帧结构

        在讲解可变参数前,必须先理解 “函数栈帧”—— 它是函数调用时在内存栈区分配的一块空间,用于存储参数、局部变量、返回地址等信息。​

        以 32 位系统(栈向下增长,即从高地址向低地址延伸)为例,看看下面代码的汇编代码,可以更好的理解函数栈帧。

int add(int a,int b)
{int c = a + b;return c;
}int main()
{int x = 10;int y = 20;int z = add(x,y);printf("%d",z);system("pause");return 0;
}

关键规则:​

  • 调用者(main)会从右到左将参数压入栈;​
  • 被调用者(add)通过栈底指针ebp访问参数:第一个可变参数在ebp+8(因为ebp本身占 4 字节,返回地址占 4 字节),后续参数依次在ebp+12、ebp+16...

(2)C 可变参数的 4 个核心宏:如何 “手动读栈”

        在C语言中,想要使用可变参数一定要有一个确定的形参,因为要用这个形参来作为锚点,计算其他参数的偏移量。

int add(int a,int b,...)
{int c = a + b;return c;
}int main()
{int x = 10;int y = 20;int a = 30;int c = 50;int b = 40;int z = add(x,y,a,b,c);printf("%d",z);system("pause");return 0;
}

(3)实战代码:用 C 可变参数实现求和函数

        当使用va_list的时候,会创建一个char* 类型的指针。然后调用va_start把确定的形参传入,这个函数的底层会根据其类型自动偏移到可变参数部分。

        后续要想使用va_arg提取可变参数部分,需要明确每一个的类型,否则编译器会解析错误,可能访问到空白地方,引发程序未定义的错误。

#include <stdio.h>
#include <stdarg.h>  // 必须包含的头文件// 功能:计算n个整数的和(n是固定参数,后面是可变参数)
int sum(int n, ...) {  // ... 表示可变参数列表va_list args;      // 1. 定义参数列表指针int total = 0;// 2. 初始化:让args指向第一个可变参数(绑定ebp和固定参数n)va_start(args, n);// 3. 遍历可变参数:循环n次,每次读一个intfor (int i = 0; i < n; i++) {// 从args中读一个int,然后指针移动到下一个参数(int占4字节,所以移动4)total += va_arg(args, int);}// 4. 释放参数指针va_end(args);return total;
}int main() {// 调用:计算10+20+30的和(n=3,后面3个可变参数)int result = sum(3, 10, 20, 30);printf("总和:%d\n", result);  // 输出:总和:60return 0;
}

底层执行逻辑:​

  1. main调用sum时,先压入 30(右数第一个参数),再压 20,再压 10,最后压固定参数 3;​
  1. sum中va_start(args, n):通过n的地址(ebp+8),让args指向第一个可变参数 10(ebp+12);​
  1. va_arg(args, int):每次读取args指向的 4 字节(int),然后args += 4,移动到下一个参数;​
  1. 循环结束后,va_end(args)将args置空,避免后续误用。

(4)C 可变参数的缺点:没有类型安全

        C 的可变参数完全依赖 “手动指定类型”,编译器不会检查参数类型是否匹配。比如下面的错误代码,编译器不会报错,但运行结果会出错:

// 错误:第二个可变参数是字符串,但用va_arg读成int
int wrong = sum(2, 10, "hello");  // 编译通过,但运行时会读取字符串的地址(4字节)当int用,结果混乱

二、可变宏函数

        除了函数的可变参数,还支持宏替换形式的可变参数。他是在预处理阶段直接替换,而普通的可变参数是运行时手动根据栈帧解析,相比较来,宏函数的运行效率肯定较高,因为他没有手动解析这一层的开销。

(1)核心语法:

#define 宏名(固定参数, ...) 替换内容(__VA_ARGS__)

  • ...:表示宏的可变参数(必须放在参数列表最后);​
  • __VA_ARGS__:在替换时,会被宏调用时传入的可变参数 “原样替换”。
  • ##__VA_ARGS__是编译器对##的特殊处理,##本身是用于字符拼接的,但是在和__VA_ARGS__放到一起的时候,编译器只会对其前面的逗号做处理。

(2)实战1:用可变宏实现日志打印

#include <stdio.h>// 可变宏:LOG(格式字符串, ...) → 替换为printf(格式字符串, 可变参数)
#define LOG(fmt, ...) printf("[" fmt "]\n", __VA_ARGS__)int main() {int age = 20;char name[] = "张三";// 宏替换后:printf("[年龄:%d,姓名:%s]\n", 20, "张三");LOG("年龄:%d,姓名:%s", age, name);  // 输出:[年龄:20,姓名:张三]return 0;
}

        也就是说...用于接住宏中传入的任意多少个参数,然后__VA_ARGS__在预处理阶段直接把刚刚...接住的所有内容原封不动的拷贝到这里。

(3) 实战 2:处理 “无可变参数” 的情况

// 改进版:##__VA_ARGS__ 会自动删除前面的逗号(如果没有可变参数)
#define LOG(fmt, ...) printf("[" fmt "]\n", ##__VA_ARGS__)int main() {LOG("程序启动");  // 替换后:printf("[程序启动]\n");(无逗号,正常编译)LOG("数值:%d", 100);  // 替换后:printf("[数值:%d]\n", 100);return 0;
}

(4)底层原理:预处理阶段的文本替换​

可变宏的本质是 “文本替换”,不涉及栈帧或编译期解包,完全由预处理程序处理:​

  1. 预处理阶段(编译前),预处理程序扫描代码,找到LOG(...)的调用;​
  2. 将fmt替换为传入的格式字符串,__VA_ARGS__替换为可变参数;​
  3. 如果用了##,则自动处理逗号问题;​
  4. 最终生成普通的printf语句,再进入编译阶段。​

缺点:​

  • 无类型检查:和 C 可变参数一样,宏替换是文本级别的,编译器不会检查参数类型;​
  • 调试困难:宏替换后代码会变化,调试时看到的是替换后的代码,不是原始宏调用。

三、C++的可变参数模板:编译器的“自动解包”

        C++11 引入了 “可变参数模板”(Variadic Templates),解决了 C 可变参数的 “类型不安全” 问题。它的核心是编译期递归解包—— 编译器会根据传入的参数数量和类型,自动生成对应的函数实例,无需手动操作栈指针,减少了程序员手动解析的错误。

(1)核心语法:...的妙用

我们先直接看看代码:

​
1. 形式1: print_single ——单个固定参数(终止器)代码实现#include <iostream>
#include <string>// 函数名:print_single(明确表示“处理单个参数”)
// 参数形式:T arg → 只有1个固定参数,类型为T(任意类型)
template <typename T>
void print_single(T arg) {// 功能:打印最后一个参数,末尾加换行(标志递归结束)std::cout << "[最后一个参数] " << arg << std::endl;
}核心解析- 参数本质:没有任何“可变参数”,就是一个普通的单参数模板函数;
- 为什么需要它:参数包展开是“从多到少”的过程(比如3个参数→2个→1个),当参数包只剩1个参数时,没有更多参数可拆,需要这个函数“接住”最后一个参数,避免递归无限进行;
- 调用时机:仅在参数包中只剩1个参数时被调用,是递归的“终点”。2. 形式2: print_pack ——固定首参+可变参数包(拆解器)代码实现// 函数名:print_pack(明确表示“处理参数包”)
// 参数形式:T first(第一个固定参数) + Args... rest(剩余可变参数包)
template <typename T, typename... Args>
void print_pack(T first, Args... rest) {// 第一步:先处理当前拆出的“第一个固定参数”std::cout << "[拆解出的参数] " << first << " | 剩余参数个数:" << sizeof...(rest) << " → ";// 第二步:判断剩余参数包是否为空,决定下一步调用if constexpr (sizeof...(rest) == 0) {// 若剩余参数为空,直接调用终止器(但此时rest为空,不符合print_single的单参数要求,实际不会走这里)std::cerr << "错误:剩余参数为空,无法调用print_single" << std::endl;} else if constexpr (sizeof...(rest) == 1) {// 若剩余参数只剩1个,调用终止器处理最后一个参数print_single(rest...);} else {// 若剩余参数多于1个,继续调用自己(拆解器),传递剩余参数包print_pack(rest...);}
}核心解析- 参数本质:是“固定参数+可变参数包”的组合,核心是**“拆解”** ——每次从完整参数包中拆出第一个参数( first ),剩下的部分仍用可变参数包( rest )表示;
- 关键操作: sizeof...(rest) 是“参数包大小运算符”,用于获取剩余参数的个数(比如 rest 是 (2,3) 时, sizeof...(rest)=2 );
- 调用时机:仅在参数包中参数个数≥2时被调用,负责逐步拆解参数包,直到剩余1个参数时,转调终止器( print_single )。​

        ...是C语言、宏函数中用于存放可变参数部分的容器,在C++中也不例外。不过对...操作符赋予了更多功能。你可以把...当做T来用,使用typename...的时候,就是在定义一个可变参数包模板类型。

        当...位于参数包类型名后面时用于打包(如typename... Args定义一个Args类型,后续使用时Args...就用来打包)。

        而...位于参数包变量的后面时候就用来解包。(如Args定义的形参变量arg)

总结一下:

(1)在C++中,只要在类型或者变量后面使用了...就表示要展开。当对类型展开的时候,表示用这个类型参数包创建一个值参数包,会有一个变量名,此时,你无论传多少个变量,都会被这个值参数包接收。使用的时候也要通过这个变量名才能展开。当对值参数包展开的时候,相当于把原来的值直接写出来,而不存放在值参数包中了。

(2)而在前面使用则表示定义(或者说对原本C语言的定义进行扩展),比如sizeof原本只能用于计算普通变量的大小,但是sizeof...却表示参数包中的个数。比如typename 只能用于定义普通的模板类型,而typename...则表示一个参数包类型。

通过一个具体调用案例( print_pack(10, "Hello", 3.14) ),一步步看两种函数如何分工协作,彻底理清调用逻辑。调用案例:处理3个参数(int, string, double)int main() {// 初始调用:传入3个参数,触发参数包拆解std::cout << "开始处理参数包:(10, \"Hello\", 3.14)\n";print_pack(10, "Hello", 3.14);return 0;
}

(1)第一步:第一次调用 print_pack (处理3个参数)
 
- 实际参数: first=10 (int类型), rest=(\"Hello\", 3.14) (剩余2个参数,类型为 (const char*, double) );
- 执行逻辑:
1. 打印: [拆解出的参数] 10 | 剩余参数个数:2 →  ;
2. 因 sizeof...(rest)=2 (多于1个),继续调用 print_pack(rest...) ,即 print_pack("Hello", 3.14) 。
 
(2)第二步:第二次调用 print_pack (处理2个参数)
 
- 实际参数: first="Hello" (const char*类型), rest=(3.14) (剩余1个参数,类型为double);
- 执行逻辑:
1. 打印: [拆解出的参数] Hello | 剩余参数个数:1 →  ;
2. 因 sizeof...(rest)=1 (只剩1个),转调 print_single(rest...) ,即 print_single(3.14) 。
 
(3)第三步:调用 print_single (处理最后1个参数)
 
- 实际参数: arg=3.14 (double类型);
- 执行逻辑:打印 [最后一个参数] 3.14 ,递归结束。
 
(4)最终输出结果(清晰展示调用流程)
 
开始处理参数包:(10, "Hello", 3.14)
[拆解出的参数] 10 | 剩余参数个数:2 → [拆解出的参数] Hello | 剩余参数个数:1 → [最后一个参数] 3.14

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/95313.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/95313.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/95313.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Android】【设计模式】抽象工厂模式改造弹窗组件必知必会

写一个 Android 版本的抽象工厂弹窗 Manager 管理器&#xff0c;使用 DialogFragment 实现&#xff0c;这样能更贴近真实的开发场景。结构设计 抽象产品&#xff1a;BaseDialogFragment&#xff08;继承 DialogFragment&#xff09;具体产品&#xff1a;LoginDialogFragment, …

Win64OpenSSL-3_5_2.exe【安装步骤】

官网下载 注意&#xff1a;科学上网&#xff0c;可以加速下载速度&#xff01; Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions 下载后得到&#xff1a;Win64OpenSSL-3_5_2.exe 双击安装 修改安装路径&#xff1a; 默认就选择第一个。 重要提醒​…

华为云云原生架构赋能:大腾智能加速业务创新步伐

巨大的涡轮、细小的螺丝&#xff0c;一台航天飞机发动机的三维模型呈现在屏幕上&#xff0c;远程同事同步协作&#xff0c;一台复杂设备在工程师高效的协同中不断完善。深圳市大腾信息技术有限公司&#xff0c;正是这场工业变革的推动者之一。大腾智能以“云原生工业”的融合为…

基于https+域名的Frp内网穿透教程(Linux+Nginx反向代理)

系列文章目录 基于http公网ip的Frp内网穿透教程(win server) 基于http域名的Frp内网穿透教程(win serverIIS反向代理) 基于http公网ip的Frp内网穿透教程(Linux) 基于https域名的Frp内网穿透教程(LinuxNginx反向代理) 目录 系列文章目录 前言 一、Frp是什么&#xff1f; 1. …

裸机程序(1)

一、裸机裸机是一个在计算机硬件与软件开发领域高频出现的概念&#xff0c;核心定义是 “未安装操作系统&#xff08;OS&#xff09;&#xff0c;仅包含硬件本身&#xff08;或仅运行最底层硬件驱动 / 控制程序&#xff09;的设备”。在电脑中&#xff0c;裸机会映射代码到cpu&…

95%企业AI失败?揭秘LangGraph+OceanBase融合数据层如何破局!​

本文较长&#xff0c;建议点赞收藏&#xff0c;以免遗失。更多AI大模型应用开发学习视频及资料&#xff0c;尽在聚客AI学院。不知道你们有没有遇到过&#xff0c;在我们一些实际落地的AI项目中&#xff0c;虽然前期“Demo 很惊艳&#xff0c;但上线后却无人问津”。你们有没有想…

树莓集团产教融合:数字学院践行职业教育“实体化运营”要求

在职业教育改革不断深化的背景下&#xff0c;“实体化运营” 成为推动职业教育高质量发展的重要方向。树莓集团积极响应这一要求&#xff0c;以产教融合为核心&#xff0c;打造数字学院&#xff0c;切实践行职业教育 “实体化运营”&#xff0c;为培养高素质数字领域专业人才探…

ELK 统一日志分析系统部署与实践指南(上)

#作者&#xff1a;张桐瑞 文章目录1 ELK 技术栈概述1.1ELK 核心组件详解1.2 ELK 工作流程2 ELK部署2.1 环境描述2.1.7 配置es集群下篇&#xff1a;《ELK 统一日志分析系统部署与实践指南&#xff08;下&#xff09;》 链接: [https://blog.csdn.net/qq_40477248/article/detail…

上位机知识篇---poweshellcmd

要理解 PowerShell 和 CMD 的区别&#xff0c;我们可以先打个通俗的比方&#xff1a;CMD 像老式功能机&#xff0c;只能干打电话、发短信这些 “基础活”&#xff1b;而 PowerShell 像智能手机&#xff0c;不仅能做基础操作&#xff0c;还能装 APP、玩复杂功能&#xff0c;甚至…

利用 Python 绘制环形热力图

暑假伊始&#xff0c;Coldrain 参加了学校举办的数模集训&#xff0c;集训的过程中&#xff0c;遇到了需要展示 59 个特征与 15 个指标之间的相关性的情况&#xff0c;在常用的图表不大合适的情况下&#xff0c;学到了一些厉害的图表&#xff0c;但是似乎千篇一律都是用 R 语言…

【序列晋升】27 Spring Cloud Sleuth给分布式系统装上透视镜

Spring Cloud Sleuth作为微服务架构中的核心监控组件&#xff0c;通过轻量级的无侵入式跟踪机制&#xff0c;解决了分布式系统中请求路径复杂、问题定位困难的痛点。它自动为每个服务请求创建唯一的Trace ID&#xff0c;并为每个服务间调用生成Span ID&#xff0c;形成完整的调…

Linux(2)|入门的开始:Linux基本指令(2)

一、基本指令介绍 回顾上篇博客Linux(1)|入门的开始&#xff1a;Linux基本指令-CSDN博客&#xff0c;我们已经学习了mkdir目录的创建&#xff0c;touch普通文件的创建&#xff0c;光有创建肯定是不行的&#xff0c;接下来就介绍我们的删除指令 1、rmdir指令&&rm指令 …

sv中forever如何结束

在 SystemVerilog 中&#xff0c;forever 循环本身无法自我结束。它的设计初衷就是创建一个永不终止的循环。 因此&#xff0c;要结束一个 forever 循环&#xff0c;必须从外部强制中断它。主要有以下两种方法&#xff1a;1. 使用 disable 语句&#xff08;最常用和推荐的方法&…

关于熵减 - 从法拉第圆盘到SEG

我们清楚的知道法拉第圆盘发电机的原理。当导线切割磁感线的时候&#xff0c;会产生电流&#xff0c;当然电流产生需要的是电动势&#xff0c;也就是&#xff0c;这里写 不写 &#xff0c;避免和电场强度混淆。根据上面的分析&#xff0c;我们知道磁场强度特斯拉 的单位&#x…

【机器学习】实战:市场增长点分析挖掘项目

在电商行业激烈竞争的背景下&#xff0c;精准挖掘市场增长点是企业保持竞争力的关键。本文基于拜耳官方旗舰店驱虫剂市场分析项目&#xff0c;先对原文核心内容进行梳理与解读&#xff0c;再续写关键的竞争分析模块&#xff0c;形成完整的市场增长点挖掘闭环&#xff0c;为企业…

【Day 18】21.合并两个有序链表 2.两数相加

文章目录21.合并两个有序链表题目&#xff1a;思路&#xff1a;迭代代码实现&#xff08;Go&#xff09;&#xff1a;2.两数相加题目&#xff1a;思路&#xff1a;代码实现&#xff08;Go&#xff09;&#xff1a;21.合并两个有序链表 题目&#xff1a; 将两个升序链表合并为…

Vue 3 WebSocket通信方案:从原理到实践

Vue 3 WebSocket通信方案&#xff1a;从原理到实践 在现代Web应用开发中&#xff0c;实时通信已成为许多应用的核心需求。从即时聊天到实时数据更新&#xff0c;用户对应用响应速度的期望越来越高。本文将深入剖析一个Vue 3环境下的WebSocket通信方案&#xff0c;包括基础封装与…

Windows 电源管理和 Shutdown 命令详解

一、Windows 电源管理概述 Windows 操作系统通过其内置的电源管理框架&#xff0c;为用户提供了多种电源状态和配置选项&#xff0c;以在性能、能耗和数据安全之间找到最佳平衡点。以下是 Windows 系统中常见的电源状态及其特点&#xff1a; 1. 睡眠&#xff08;Sleep&#xff…

Selenium WebUI 自动化“避坑”指南——从常用 API 到 10 大高频问题

目录 一、为什么 90% 的 UI 自动化脚本活不过 3 个月&#xff1f; 二、Selenium必会 API 速查 三、实践 四、10 大高频异常“症状 → 病因 → 处方” 五、可复用的工具函数 六、面试高频追问&#xff08;附标准答案&#xff09; 一、为什么 90% 的 UI 自动化脚本活不过 …

【微信小程序】微信小程序基于双token的API请求封装与无感刷新实现方案

文章目录前言一、设计思路二、执行流程三、核心模块3.1 全局配置3.2 request封装3.2.1 request方法配置参数3.2.2 请求预处理3.2.3 核心请求流程3.3 刷新accessToken3.4 辅助方法四、api封装示例总结前言 现代前后端分离的模式中&#xff0c;一般都是采用token的方式实现API的…