【C语言】可变参数列表详解

news2024/11/24 9:13:09

可变参数列表

  • 一、可变参数列表的使用
    • 1、可变参数列表的形式
    • 2、可变参数列表的提取
    • 3、使用演示
    • 4、利用可变参数实现一个简单的日志打印功能
  • 二、可变参数列表的原理
    • 1、原理的讲解
    • 2、原理的证明

一、可变参数列表的使用

1、可变参数列表的形式

有时我们在使用C语言时可能会碰到这样的情况,希望函数带有可变数量的参数,而不是预定义数量的参数。
为此C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数。

使用方式为:

int func(int arg1, ...);

其中,省略号...表示可变参数列表,需要注意的是:如果你想使用可变参数列表,则至少有一个固定参数,即不存在下面的函数:

int func(...);

我们C语言常用的printfscanf函数就是使用了可变参数列表的函数:
在这里插入图片描述

2、可变参数列表的提取

对于可变参数列表,我们最关心的还是怎么将可变参数提取出来,关于可变参数的提取主要依赖一个类型和四个宏函数va_listva_startva_argva_copyva_end,而这些类型和宏函数在C语言的头文件stdarg.h中。


  • 类型va_list本质是一个char*类型,我们要使用可变参数列表,必须首先定义一个va_list类型的变量。
  va_list ap;

在这里插入图片描述


  • va_list类型的变量初始化的函数是va_start函数,初始化以后va_list类型的变量指向第一个可变参数的首地址,该函数的函数原型如下:
  void va_start(va_list ap, last);
  • ap: 这是一个 va_list 类型的对象。
  • last 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。

  • 用来提取可变参数列表中的参数的函数是va_arg,使用一次提取一个,每次提取的参数是直接返回的并且该函数提取的同时会自动将ap指向下一个参数。
 type va_arg(va_list ap, type);
  • ap: 这是一个 va_list 类型的对象。
  • type:要提取的参数的类型,如int , double,如果当前参数类型和type不统一,就会发生不可预知的错误

  • 这个函数不是必须使用的函数,这时一个拷贝函数,初始化dest作为src(当前状态)的副本。
 void va_copy(va_list dest, va_list src);
  • dest: 要作为副本的对象。
  • src: 原始值

  • 销毁va_list类型变量的函数是va_end,其本质就是将指针置为NULL
void va_end(va_list ap);
  • ap: 要销毁的变量。

3、使用演示

①打印每一个参数

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

void PrintArg(int num, ...)
{
	va_list ap;
	// 1.进行初始化
	va_start(ap, num);

	for (int i = 0; i < num; i++)
	{
		// 不断取出可变参数
		int a = va_arg(ap, int);
		printf("%d ", a);
	}
	// 销毁
	va_end(ap);
}

int main()
{
	PrintArg(4, 1, 3, 4, 5);
	return 0;
}

输出结果:

在这里插入图片描述

4、利用可变参数实现一个简单的日志打印功能

日志虽然很简单,但是在实际开发中,日志信息是很重要的,下面我们就来实现一个简单的日志打印函数,这里我们为了方便使用了C++的string来存储字符串,如果你没有学习过C++可以将它简单理解为char数组。

首先我们以后的消息都是要按照这种方式来进行结构化输出:

 日志格式: [错误等级] [时间]  :消息体

首先日志的左边是固定的,所以我们很容易实现,对于错误等级我们可以使用枚举变量的方式进行定义每一个错误等级,对于时间,我们可以使用C语言的time.h库中的函数time()localtime()函数配合使用得到。

实现日志的关键是在于对消息体的处理,因为消息体中的数据个数的不固定的而且类型也都是不一致的,对于它们的处理我们可以将它们转换为一个长的字符串,这就需要我们使用vsnprintf函数来将不同的参数进行格式化为字符串了。


vsnprintf函数可以将可变参数,按照一定的格式,格式化为一个字符串。

int vsnprintf(char *str, size_t size, const char *format, va_list ap);

参数:

  • str : 缓冲区的起始地址。
  • size : 缓冲区的大小。
  • format : 格式化字符串。
  • ap :可变参数。

返回值:

  • 写入到缓冲区的字节数(不包括\0),如果返回值大于等于size意味着输出被截断了。

代码实现

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <cstring>
#include <ctime>

// 日志等级
enum { Debuge = 0, Info, Warning, Error, Fatal, Unkonw };

// 将日志等级转换为字符串
static std::string toLevelString(int level)
{
    switch (level)
    {
    case Debuge:
        return "Debuge";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Unkonw";
    }
}

// 获取当前时间
static std::string getTime()
{
    char buf[128];
    time_t timep = time(nullptr);
    struct tm stdtm;
    localtime_s(&stdtm, &timep);

    snprintf(buf, sizeof(buf), "%d-%d-%d  %d:%d:%d", stdtm.tm_year + 1900, stdtm.tm_mon + 1, stdtm.tm_mday,
        stdtm.tm_hour, stdtm.tm_min, stdtm.tm_sec);
    return buf;
}

// 日志打印函数
// 日志格式: [等级] [时间]  :消息体
void logMessage(int level, const char* format, ...)
{
    // 1.形成左边的固定格式
    char logLeft[1024];
    char logRight[1024];
    std::string logLevel = toLevelString(level);
    std::string curTime = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] : ", logLevel.c_str(), curTime.c_str());

    // 2.形成右边的消息体格式
    va_list ap;
    va_start(ap, format);
    
    // 利用vsnprintf函数将可变参数按照一定的格式,格式化为一个字符串。
    vsnprintf(logRight, sizeof(logRight), format, ap);
    va_end(ap);

    // 3.进行拼接,形成完整的日志 (此处可以根据需要重定向到文件中,进行持久化保存)
    printf("%s%s\n", logLeft, logRight);
}

int main()
{
    // 故意制造一个失败
    FILE* fp = fopen("a.txt", "r");
    if (!fp)
    {
        logMessage(Fatal, " fopen fail : exit code %d, info : %s", errno, strerror(errno));
    }
    return 0;
}

输出结果:

在这里插入图片描述

注意事项

  • 结构体struct tm 结构体的定义:
    在这里插入图片描述
    对于月数,我们要进行+1,因为其定义中包含了0月,对于年数,我们要加上1900,这样时间戳才能够正确的转换为我们想要的年数。

  • 我们这里没有使用C语言的localtime,因为其存在线程安全问题,在Linux平台下我们可以使用localtime_r代替它,在windows平台下我们可以使用localtime_s来代替它,这些代替的函数是没有线程安全的。

二、可变参数列表的原理

1、原理的讲解

这一部分涉及了函数栈帧,建议读者理解函数栈帧以后再进行观看。

首先有三个知识点:

  1. 对于C语言如果函数没有形式参数,也是可以给函数传递参数的。(C++是不允许的!)
  2. 在C语言中,只要发生了函数调用并且传递了参数,必定形成临时变量。
  3. 所谓的临时拷贝本质就是在栈帧内部形成的。C语言的函数参数从右向左依次形成临时变量

我们还是以这段代码为例,进行分析:

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

void PrintArg(int num, ...)
{
	va_list ap;
	// 1.进行初始化
	va_start(ap, num);

	for (int i = 0; i < num; i++)
	{
		// 不断取出可变参数
		int a = va_arg(ap, int);
		printf("%d ", a);
	}
	// 销毁
	va_end(ap);
}

int main()
{
	PrintArg(4, 1, 3, 4, 5);

	return 0;
}

在调用函数PrintArg时,其参数会先从右向左依次压栈:

在这里插入图片描述

我们可以在Visual Studio中打开内存和寄存器窗口,在执行PrintArg函数时转到反汇编进行观察esp位置内存中的值的变化。

  • ebp:栈底寄存器
  • esp:栈顶寄存器

在这里插入图片描述

可以看到调用函数所使用的参数压入栈时数据是连续的,那么也就是说我们只要拿到第一个参数的地址,后面所有的参数我们都可以拿到,只不过需要我们在读取数据时进行一下类型转换,改变一下指针的步长,保证我们拿到完整的数据。

  • 所以为什么va_listchar *类型?

因为char*的指针读取数据时是按照1字节进行读取的,1字节读取方便我们读取数据。

  • 为什么va_list类型的变量需要进行初始化,可变参数列表必须至少要有一个固定参数,而且va_start()函数的第二个参数必须是最后一个固定参数?

这是因为初始化的工作就是将va_list类型的变量根据第一个固定参数,让其指向第一个可变参数。如果没有一个固定参数,就会导致va_list类型的变量不能指向第一个可变参数的地址。

  • 为什么我们使用va_arg()函数提取参数时第二个参数需要一个类型?

因为只有根据这个类型,va_list类型的变量才知道接下来要提取的参数的大小是多少字节。


这里我们再来看下面的一个特例:

我们传入char类型的变量,然后使用int类型进行提取,故意让其不匹配。

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

void PrintIntArg(int num, ...)
{
	va_list ap;
	// 1.进行初始化
	va_start(ap, num);

	for (int i = 0; i < num; i++)
	{
		// 注意这里我们使用int类型提取char类型的变量
		int a = va_arg(ap, int);
		printf("%c ", a);
	}
	// 销毁
	va_end(ap);
}

int main()
{
	char a = 'a';
	char b = 'b';
	char c = 'c';
	char d = 'd';
	PrintIntArg(4, a, b, c, d);

	return 0;
}

输出结果:

在这里插入图片描述

程序没有崩溃,正确的提取了我们想要的参数,为什么没有出现数据不匹配呢?

通过查看汇编,我们可以看到,在可变参数场景下:

  1. 实际传入的参数如果是char,shortfloat,编译器在编译的时候,会自动进行整形提升(通过查看汇编,我们都能看到)
  2. 函数内部使用的时候,根据类型提取数据,更多的是通过int或者double来进行提取。

在这里插入图片描述


原理总结

  1. 可变参数列表对应的函数,最终调用也是函数调用,也要形成栈帧。
  2. 栈帧形成前,临时变量是要先入栈的,入栈的参数之间位置关系是固定的。
  3. 通过上面的特例我们发现了短整型在可变参数部分,会默认进行整形提升,那么函数内部在提取该数据的时候,就要考虑提升之后的值,如果不加考虑,获取数据可能会报错或者结果不正确。

注意事项 :

  • 可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。
  • 参数列表中至少有一个固定参数。如果连一个固定参数都没有,就无法使用va_start
  • 这些宏是无法直接判断实际存在参数的数量,提取时提取的个数由你控制,或者通过其他的方式让这些宏知道参数的个数,例如printf()的格式控制时,就是根据%来确定参数的个数的。
  • 这些宏无法判断每个参数的是类型,提取时你必须显示指定类型,或者通过其他方式让这些宏知道参数的类型,例如printf()的格式控制中,就是根据%后面的d,s,c,lf来确定参数的类型的。
  • 如果在va_arg中指定了错误的类型,那么其后果是不可预测的。

2、原理的证明

  • va_start宏函数的定义:

在这里插入图片描述

例如下面的例子:

在这里插入图片描述

按照此宏函数的定义:我们先取出ch变量的地址,然后判断ch是否满足4字节对齐,不满足就进行提升,所以后面_INTSIZEOF(ch)的结果是4,于是ap被赋值为了第一个可变参数的地址!

  • va_arg宏函数的定义:
    在这里插入图片描述

在这里插入图片描述

ap指针先被赋值为指向下一个参数的位置(ap已经改变了),然后再回退过去(此时ap不变),再然后利用回退过去的值进行指针类型转换,然后解引用进行提取,拿到参数。

  • va_end宏函数的定义:

在这里插入图片描述
可以看到ap指针被置空了,最前面的(void)是不想让此函数有返回值。


理解_INITSIZEOF
在这里插入图片描述

为了后面方便表述,我们假设sizeof(n)的值是n(char 1,short 2, int 4)我们在32位平台,vs2013下测试,sizeof(int)大小是4,其他情况我们不考虑。

_INTSIZEOF(n)的意思:计算一个最小数字x,满足 x>=n && x%4==0,其实就是一种4字节对齐的方式。

比如n是:1,2,3,4 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 4
比如n是:5,6,7,8 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 8

为什么有这个4字节对齐是因为短整型参数传递时会进行整形提升

怎么办到的:

第一步理解:4的倍数
既然是4的最小整数倍取整,那么本质是: x = 4 ∗ m x=4*m x=4m,m是具体几倍。对 x = 7 x=7 x=7来讲,m就是2,对齐的结果就是8,而m具体是多少,取决于n是多少。

  • 如果n能整除4,那么m就是 n / 4 n/4 n/4
  • 如果n不能整除4,那么m就是 n / 4 + 1 n/4+1 n/4+1

上面是两种情况,如何合并成为一种写法呢?

常见做法是 : ( n + s i z e o f ( i n t ) − 1 ) ) / s i z e o f ( i n t ) − > ( n + 4 − 1 ) / 4 ( n+sizeof(int)-1) )/sizeof(int) -> (n+4-1)/4 (n+sizeof(int)1))/sizeof(int)>(n+41)/4

  • 如果n能整除4,那么m就是 ( n + 4 − 1 ) / 4 − > ( n + 3 ) / 4 (n+4-1)/4->(n+3)/4 (n+41)/4>(n+3)/4,+3的值无意义,会因取整自动消除,等价于 n / 4 n/4 n/4
  • 如果n不能整除4,那么 n = 最大能整除 4 部分 + r , 1 < = r < 4 n=最大能整除4部分+r,1<=r<4 n=最大能整除4部分+r,1<=r<4那么m就是 ( n + 4 − 1 ) / 4 − > ( 能整除 4 部分 + r + 3 ) / 4 (n+4-1)/4->(能整除4部分+r+3)/4 (n+41)/4>(能整除4部分+r+3)/4,其中
    4 < = r + 3 < 7 − > 能整除 4 部分 / 4 + ( r + 3 ) / 4 − > n / 4 + 1 4<=r+3<7 -> 能整除4部分/4 + (r+3)/4 -> n/4+1 4<=r+3<7>能整除4部分/4+(r+3)/4>n/4+1

第二步理解:最小4字节对齐数

搞清楚了满足条件最小是几倍问题,那么,计算一个最小数字x,满足 x>=n && x%4==0,就变成了 ( ( n + s i z e o f ( i n t ) − 1 ) / s i z e o f ( i n t ) ) [ 最小几倍 ] ∗ s i z e o f ( i n t ) [ 单位大小 ] − > ( ( n + 4 − 1 ) / 4 ) ∗ 4 ((n+sizeof(int)-1)/sizeof(int))[最小几倍] * sizeof(int)[单位大小] -> ((n+4-1)/4)*4 ((n+sizeof(int)1)/sizeof(int))[最小几倍]sizeof(int)[单位大小]>((n+41)/4)4
这样就能求出来4字节对齐的数据了,其实上面的写法,在功能上,已经和源代码中的宏等价了。

第三步理解:理解源代码中的宏

简洁写法: ( ( n + 4 − 1 ) / 4 ) ((n+4-1)/4) ((n+41)/4* 4,设 w = n + 4 − 1 w=n+4-1 w=n+41, 那么表达式可以变化成为 ( w / 4 ) ∗ 4 (w/4)*4 (w/4)4,而4就是 2 2 2^2 22 w / 4 w/4 w/4,不就相当于右移两位吗?再次 ∗ 4 *4 4不就相当左移两位吗?先右移两位,在左移两位,最终结果就是,最后2个比特位被清空为0!

这就相当于w & ~3 ,所以,简洁版:(n+4-1) & ~(4-1)
原码版:((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决RequestParam.value() was empty on parameter 0

在网上查询很多种方式&#xff0c;都解决不了问题&#xff0c;比如&#xff1a; 在RequestParam RequestBody注解 解决办法:在RequestParam中的name加上对应参数 如 : RequestParam( name "name" ) RequestBody也是同理 最后解决的办法&#xff0c;是发现mave…

【Linux】nohub指令--终端退出后命令仍旧执行

文章目录 0、背景1、作用2、语法3、用法演示4、关于2>&1 0、背景 Shell中&#xff0c;执行一个持续进行的指令&#xff0c;会"霸屏"&#xff0c;即你想再执行其他指令&#xff0c;要么重开个shell终端&#xff0c;要么退出这个执行。 1、作用 nohub&#x…

vue 把echarts封装成一个方法 并且从后端读取数据 +转换数据格式 =动态echarts 联动echarts表

1.把echarts 在 methods 封装成一个方法mounted 在中调用 折线图 和柱状图 mounted调用下边两个方法 mounted(){//最早获取DOM元素的生命周期函数 挂载完毕console.log(mounted-id , document.getElementById(charts))this.line()this.pie()},methods里边的方法 line() {// …

苹果麻烦了,全球没有消费者愿意接受印度制造的iPhone

据外媒报道指印度制造的iPhone良率只有一半&#xff0c;以至于发出的货被质量工程师打回一半&#xff0c;由此引发欧洲消费者的抗拒&#xff0c;为安抚欧洲消费者&#xff0c;苹果表示欧洲市场的iPhone15将全数由中国制造供应&#xff0c;而印度制造的iPhone将在印度市场销售以…

基于python的urllib 库抓取网站上的图片

最近写了个爬虫实例&#xff0c;有python环境的话就可以直接运行了。 运行效果是这样的&#xff1a; 完整代码如下&#xff1a; import urllib import urllib.request import re import random import time import os #目标网址: imagePath"https://pic.netbian.com&quo…

JVM对象的创建过程、内存分配、内存布局、访问定位等问题详解

对象 内存分配的两种方式 指针碰撞 适用场合&#xff1a;堆内存规整&#xff08;即没有内存碎片&#xff09;的情况下。 原理&#xff1a;用过的内存全部整合到一边&#xff0c;没有用过的内存放在另一边&#xff0c;中间有一个分界指针&#xff0c;只需要向着没用过的内存…

计算机视觉与深度学习-卷积神经网络-卷积图像去噪边缘提取-卷积-[北邮鲁鹏]

目录标题 参考学习链接卷积的定义卷积的性质叠加性平移不变性交换律结合律分配律标量 边界填充边界填充方法 - 常数填充最常用常数填充零填充&#xff08;zero padding&#xff09;拉伸镜像 卷积示例单位脉冲核无变化平移平滑锐化 卷积核平均卷积核高斯卷积核高斯卷积核定义高斯…

智能金融决策策略,规则引擎在大数据金融行业的实战案例

在金融风控场景中&#xff0c;规则引擎是一个核心风险管理的利器&#xff0c;它预先设定一系列规则设定&#xff0c;用于便捷的评估和处理各种交易、客户行为或其他需要自动化决策、计算、推理判断的情况。 以下是一个详细的示例&#xff0c;说明规则引擎在金融风控中的使用。 …

智能工厂的产业前景如何?

智能工厂的产业前景相当光明&#xff0c;并且正在迅速发展。智能工厂&#xff0c;也称为工业 4.0 或第四次工业革命&#xff0c;代表了先进技术和数据驱动自动化推动的制造和工业流程的重大转变。以下是智能工厂产业前景的一些关键方面&#xff1a; 1.提高效率&#xff1a;智能…

OSCP系列靶场-Intermediate-BTRSys2.1保姆级

OSCP系列靶场-Intermediate-BTRSys2.1 目录 OSCP系列靶场-Intermediate-BTRSys2.1总结准备工作信息收集-端口扫描目标开放端口收集目标端口对应服务探测 信息收集-端口测试21-FTP端口的信息收集21-FTP版本版本信息与MSF利用21-FTP端口匿名登录测试(成功)21-FTP端口-文件GET收集…

C++中的深拷贝和浅拷贝介绍

对于基本类型的数据以及简单的对象,它们之间的拷贝非常简单,就是按位复制内存。例如: class Base{public:Base(): m_a(0), m_b(0){ }Base(int a, int b): m_a(a), m_b(b){ }private:int m_a;int m_b;};int main(){int a = 10;int b = a; //拷贝Base obj1(10, 20);Base obj2…

项目管理软件在项目中的这些作用,你知道吗?

现在项目管理软件成为各类企业必不可少的工具&#xff0c;给项目提供全面的视图、促进团队协作、实时跟踪和监控、优化资源利用、改善决策制定等优势。 它们可以帮助团队成员更好地组织和协作&#xff0c;是实现项目目标的必备工具。通过合理利用项目管理软件的功能和特点&…

87 # express 应用和创建应用的分离

创建应用的过程和应用本身要进行分离。路由和创建应用的过程也做一个分离。 下面实现创建应用的过程和应用本身要进行分离&#xff1a; express.js const Application require("./application");function createApplication() {// 通过类来实现分离操作return ne…

一篇文章告诉您立仪3D线激光位移传感器

激光位移传感器是利用激光技术进行测量的传感器。它由激光器、激光检测器和测量电路组成。激光传感器是新型测量仪表。能够精确非接触测量被测物体的位置、位移等变化。 可以测量位移、厚度、振动、距离、直径等精密的几何测量。激光有直线度好的优良特性&#xff0c;同样激光…

【MATLAB第75期】#源码分享 | 基于MATLAB的不规则数据插值实现时间序列数据扩充

【MATLAB第75期】#源码分享 | 基于MATLAB的不规则数据插值实现时间序列数据扩充 如时间数据以单位1为间隔排序&#xff0c; 可插间隔为0.5的数据 。 一、实现效果 1.规则间隔数据 2.非规则间隔数据 二、主程序代码 1.插值测试效果 %% 清空环境变量 warning off …

【送书活动1】强势挑战Java,Kotlin杀回TIOBE榜单Top 20!

⭐简单说两句⭐ 作者&#xff1a;后端小知识 CSDN个人主页&#xff1a;后端小知识 &#x1f50e;GZH&#xff1a;后端小知识 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; 强势挑战Java&#xff0c;Kotlin杀回TIOBE榜单Top 20&#xff01; …

python绘制钻头外径磨损图

import matplotlib.pyplot as plt import numpy as np srcpathrC:\Users\user\Documents\F1-21\data0.125-1.8.txtdef openreadtxt(file_name):data []with open(file_name, r) as file:file_data file.readlines() # 读取所有行for row in file_data:tmp_list row.split()…

跨境支付融合解析:有效解决跨境电商系统开发的支付问题

作为跨境电商系统开发的专家&#xff0c;我们深知支付问题对整个系统的重要性。在不同国家、不同支付体系的交叉领域里&#xff0c;跨境支付融合是一个引人注目的话题。本文将深入探讨跨境支付融合的必要性&#xff0c;分析其优势&#xff0c;并提供一系列解决方案&#xff0c;…

vue-element-admin项目部署 nginx动态代理 含Docker部署、 Jenkins构建

介绍三种方式&#xff1a; 1.直接部署到nginx中 2.用nginx docker镜像部署 3.使用Jenkins构建 1.直接用nginx部署 vue-element-admin项目下有两个.env文件&#xff0c;.env.production是生产环境的&#xff0c;.env.developpment是开发环境的 vue-element-admin默认用的是mock数…

ChatGPT竞争对手Writer,获得1亿美元融资;面向不同任务微调Llama-2经验总结

&#x1f989; AI新闻 &#x1f680; ChatGPT竞争对手Writer&#xff0c;获得1亿美元融资 摘要&#xff1a;美国生成式AI平台Writer宣布获得1亿美元的B轮融资。Writer提供类似于ChatGPT的功能&#xff0c;主要聚焦在企业领域&#xff0c;提供文本生成、总结摘要、文本纠错等服…