深入理解可变参数

1.C语言方式

1.1.宏介绍

C语言中的可变参数是指函数可以接受可变数量的参数。这些参数的数量在编译时是未知的。在这些可变参数中的参数类型可以相同,也可以不同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活;在头文件stdarg.h中,涉及到的宏有:
                va_list :   是指向参数的指针 ,通过指针运算来调整访问的对象
                va_start :获取可变参数列表的第一个参数的地址
                va_arg : 获取可变参数的当前参数,返回指定类型并将指针指向下一参数
                va_end : 清空va_list可变参数列表

1.2.原理详解

函数的参数是存放在栈中,地址是连续的,所以可以通过相对位置去访问,这也是可变参数的访问方式;变长参数的实现需要依赖于C语言默认的cdecl调用惯例的自右向左压栈传递方式;可变参数是由1.1介绍的几个宏来实现,但是由于硬件平台的不同,编译器的不同,宏的定义也不相同,下面是AMD CPU x64平台下的定义:

typedef char* va_list;

va_list的定义

//[1]
#ifdef __cplusplus
#define _ADDRESSOF(v) (&reinterpret_cast<const char &>(v))
#else
#define _ADDRESSOF(v) (&(v))
#endif//[2]
#define va_start _crt_va_start
#define va_arg   _crt_va_arg
#define va_end   _crt_va_end
#define va_copy(destination, source) ((destination) = (source))//[3]
#define _PTRSIZEOF(n) ((sizeof(n) + sizeof(void*) - 1) & ~(sizeof(void*) - 1))//系统内存对齐
#define _ISSTRUCT(t)  ((sizeof(t) > sizeof(void*)) || (sizeof(t) & (sizeof(t) - 1)) != 0)
#define _crt_va_start(v,l)	((v) = (va_list)_ADDRESSOF(l) + _PTRSIZEOF(l))
#define _crt_va_arg(v,t)	_ISSTRUCT(t) ?						\(**(t**)(((v) += sizeof(void*)) - sizeof(void*))) :	\( *(t *)(((v) += sizeof(void*)) - sizeof(void*)))
#define _crt_va_end(v)		((v) = (va_list)0)
#define _crt_va_copy(d,s)	((d) = (s))

从上面的源码可以看出:
1) va_list  v; 定义一个指向char类型的指针v。
2) va_start(v,l) ;执行 v = (va_list)&l + _PTRSIZEOF(l) ,v指向参数 l 之后的那个参数的地址,即 v指向第一个可变参数在堆栈的地址。
3) va_arg(v,t) , ( (t )((v += _PTRSIZEOF(t)) - _PTRSIZEOF(t)) ) 取出当前v指针所指的值,并使 v 指向下一个参数。 v+=sizeof(t类型) ,让v指向下一个参数的地址。然后返回 v - sizeof(t类型) 的t类型指针,这正是第一个可变参数在堆栈里的地址。然后 用取得这个地址的内容。
va_end(v) ; 清空 va_list v。

1.3.案例分析

#include <iostream>
#include <stdarg.h>void printValues(const char* format, ...) {va_list args;  // 定义一个va_list类型的变量va_start(args, format);  // 初始化argsfor (const char* arg = format; *arg != '\0'; ++arg) {if (*arg == '%') {++arg;switch (*arg) {case 'd':  // 对于整数std::cout << va_arg(args, int);break;case 's':  // 对于字符串std::cout << va_arg(args, char*);break;default:std::cout << "Invalid format specifier: " << *arg;}}else {std::cout << *arg;}}va_end(args);  // 清理va_list变量
}int main() {printValues("say self info: %s, age %d\n", "xiao", 45);  //输出: say self info xiao, age 45return 0;
}

printValues函数调用的时候展开为:

void printValues(const char* format, const char* param1, int param2)

从上面的代码来分析一下这个示例:在windows中,栈由高地址往低地址生长,调用printValues函数时,其参数入栈情况如下:

当调用va_start(args, format)时:args指针指向情况对应下图:

        当调用va_arg(args, ...)时,它必须返回一个由va_list所指向的恰当的类型的数值,同时递增args,使它指向参数列表中的一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标,所以va_arg宏首先使用sizeof来确定需要递增的大小,然后把它直接加到va_list上,这样得到的指针再被转换为要求的类型。

        在上面的示例中,我们定义了一个名为printValues的函数,它接受一个格式字符串和一个可变数量的参数。我们使用va_list、va_start、va_arg和va_end这些宏来处理可变参数。在格式字符串中,我们使用%来指定参数的类型,例如%d表示整数,%s表示字符串。然后,我们使用va_arg宏来获取相应的参数值。最后,我们使用va_end宏来清理va_list变量。

1.4.其他实例

1) printf实现

#include <stdarg.h>int printf(char *format, ...)
{va_list ap;int n;va_start(ap, format);n = vprintf(format, ap);va_end(ap);return n;    
}

2)定制错误打印函数error

#include  <stdio.h>
#include  <stdarg.h>void error(char *format, ...)
{va_list ap;va_start(ap, format);fprintf(stderr, "Error: ");vfprintf(stderr, format, ap);va_end(ap);fprintf(stderr, "\n");return;    
}

2.C++之std::initializer_list

        在C++中我们一般用()和=初始化参数或对象,还可以用{}来初始化参数或对象,比如数组的初始化int m[] = {1,4,5},除了数组,在STL里面很多标准的容器和自定义类型都用{} 进行初始化。

        自C++11标准开始就引入了列表初始化的概念,即支持使用{}对变量或对象进行初始化,且与传统的变量初始化的规则一样,也分为拷贝初始化和直接初始化两种方式。

2.1.简介

std::initializer_list<T> 类型对象是一个访问 const T 类型对象数组的轻量代理对象。
std::initializer_list 对象在这些时候自动构造:
1)用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个 std::initializer_list 参数,如std::vector的构造函数  vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())
2)以花括号初始化器列表为赋值的右运算数,或函数调用参数,而对应的赋值运算符/函数接受 std::initializer_list 参数
3)绑定花括号初始化器列表到 auto ,包括在范围 for 循环中

initializer_list 可由一对指针或指针与其长度实现。复制一个 std::initializer_list 不会复制其底层对象。

注意:

a、底层数组不保证在原始 initializer_list 对象的生存期结束后继续存在。 std::initializer_list 的存储是未指定的(即它可以是自动、临时或静态只读内存,依赖场合)。

b、底层数组是 const T[N] 类型的临时数组,其中每个元素都从原始初始化器列表的对应元素复制初始化(除非窄化转换非法)。底层数组的生存期与任何其他临时对象相同,除了从数组初始化 initializer_list 对象会延长数组的生存期,恰如绑定引用到临时量(有例外,例如对于初始化非静态类成员)。底层数组可以分配在只读内存。

c、若声明了 std::initializer_list 的显式或偏特化则程序为谬构。

2.2.原理详解

源码面前无秘密,直接上源码:

template <class _Elem>
class initializer_list {
public:using value_type      = _Elem;using reference       = const _Elem&;using const_reference = const _Elem&;using size_type       = size_t;using iterator       = const _Elem*;using const_iterator = const _Elem*;constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) {}  //1constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept: _First(_First_arg), _Last(_Last_arg) {}                               //2_NODISCARD constexpr const _Elem* begin() const noexcept {return _First;}_NODISCARD constexpr const _Elem* end() const noexcept {return _Last;}_NODISCARD constexpr size_t size() const noexcept {return static_cast<size_t>(_Last - _First);}private:const _Elem* _First;const _Elem* _Last;
};// FUNCTION TEMPLATE begin
template <class _Elem>
_NODISCARD constexpr const _Elem* begin(initializer_list<_Elem> _Ilist) noexcept {return _Ilist.begin();
}// FUNCTION TEMPLATE end
template <class _Elem>
_NODISCARD constexpr const _Elem* end(initializer_list<_Elem> _Ilist) noexcept {return _Ilist.end();
}

        从上面的STL的std::initializer_list源码来看,std::initializer_list是一个模版类,定义了指向该类对象首端、尾端的迭代器(即常量对象指针const T*),实际上就是对{}表达式内容的简单封装,当使用{}时,就会调用 initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) 构造出std::initializer_list。

        当得到了一个std::initializer_list对象后,再来寻找标准容器中以std::initializer_list为形参的构造函数,并调用该构造函数对容器进行初始化。

2.3.案例分析

示例1:

class IMessageField1 {};//1
void  addMessageField(std::initializer_list<IMessageField1*> t)
{std::vector<IMessageField1*>  pTest(t);
}#if  0
//2
void  addMessageField(std::vector<IMessageField1*> t)
{std::vector<IMessageField1*>  pTest(t);
}
#endifvoid  main()
{//[1]std::unique_ptr<IMessageField1> a(new IMessageField1);std::unique_ptr<IMessageField1> b(new IMessageField1);std::unique_ptr<IMessageField1> c(new IMessageField1);std::unique_ptr<IMessageField1> d(new IMessageField1);std::unique_ptr<IMessageField1> e(new IMessageField1);addMessageField({ a.get(), b.get(), c.get(), d.get(), e.get() });
}

   上面代码1和2的方式都可以实现功能,2的方式实际上也是先临时生成一个std::initializer_list,再调用std::vector的构造函数临时生成一个std::vector,最后再用刚生成的std::vector初始化pTest,相比1的方式,多了几重复制,效率比较低,一般采用1的方式实现功能。

示例2:

#include <iostream>
#include <vector>
#include <initializer_list>template <class T>
struct S {std::vector<T> v;S(std::initializer_list<T> l) : v(l) {std::cout << "constructed with a " << l.size() << "-element list\n";}void append(std::initializer_list<T> l) {v.insert(v.end(), l.begin(), l.end());}std::pair<const T*, std::size_t> c_arr() const {return {&v[0], v.size()};  // 在 return 语句中复制列表初始化// 这不使用 std::initializer_list}
};template <typename T>
void templated_fn(T) {}int main()
{int a1[] = { 1,2,3,4,5,6 }; //数组拷贝初始化int a2[]{ 5,6,7,8,9,0 };   //数组直接初始化S<int> s = {1, 2, 3, 4, 5}; // 复制初始化s.append({6, 7, 8});      // 函数调用中的列表初始化std::cout << "The vector size is now " << s.c_arr().second << " ints:\n";for (auto n : s.v)std::cout << n << ' ';std::cout << '\n';std::cout << "Range-for over brace-init-list: \n";for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作std::cout << x << ' ';std::cout << '\n';auto al = {10, 11, 12};   // auto 的特殊规则std::cout << "The list bound to auto has size() = " << al.size() << '\n';//    templated_fn({1, 2, 3}); // 编译错误!“ {1, 2, 3} ”不是表达式,// 它无类型,故 T 无法推导templated_fn<std::initializer_list<int>>({1, 2, 3}); // OKtemplated_fn<std::vector<int>>({1, 2, 3});           // 也 OK
}

输出:

constructed with a 5-element list
The vector size is now 8 ints:
1 2 3 4 5 6 7 8
Range-for over brace-init-list: 
-1 -2 -3 
The list bound to auto has size() = 3

示例3:

struct MyTest{explicit  X(int a, int b) :a(a), b(b) { std::cout << "MyTest(int a,int b)\n"; }int a{};int b{};
};int main() {MyTest x{ 1,2 }; //OKMyTest x2( 1,2 ); //OKMyTest x3 = { 1,2 }; //Error
}

MyTest x3 ={1,2}; 参考复制初始化的规则:复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)

后面继续。。。

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

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

相关文章

Docker安装sentinel控制台

1、拉取镜像&#xff0c;直接使用run命令&#xff0c;如果说本地没有镜像就会直接去远程仓库拉取&#xff1a; docker run -d \ -p 8858:8858 \ --name sentinel-dashboard \ --network demo \ -e AUTH_USERNAMEsentinel \ -e AUTH_PASSWORD123456 \ bladex/sentinel-dashboa…

听GPT 讲Rust源代码--compiler(7)

File: rust/compiler/rustc_infer/src/infer/sub.rs 文件rust/compiler/rustc_infer/src/infer/sub.rs是Rust编译器的类型推断模块的一部分&#xff0c;它包含了类型推断的具体实现。 类型推断是编程语言中的重要步骤&#xff0c;它通过分析代码中的上下文信息来确定变量、表达…

清风数学建模笔记-时间序列分析

内容&#xff1a;时间预测分析 一.时间序列 1.时点时间序列 2.时期时间序列&#xff1a;可相加 二.时间趋势分解 1.季节趋势 拓展&#xff1a;百度指数&#xff1a; 2.循环变动趋势&#xff08;和季节很像但是是以年为单位&#xff09; 3.不规则变动趋势&#xff08;像扰…

跨国公司为什么要部署SD-WAN

随着全球化进一步加深&#xff0c;越来越多的企业开始实施跨国战略&#xff0c;但要在各个地区建立分支机构、数据中心&#xff0c;跨国企业可能会遇到各地区之间网络性能差异大、导致数据传输效率低下的问题&#xff0c;而且由于网络场景复杂&#xff0c;网络设备和运维成本高…

jmeter使用心得(一)

jmeter作为接口测试的常用工具之一&#xff0c;在我们的测试中经常会用到&#xff0c;往期的文章中&#xff0c;我们也分享过jmeter的各种功能和用法&#xff0c;基本覆盖了方方面面&#xff0c;可以满足各种接口测试的需求。但实际测试中我们也会发现&#xff0c;jmeter这么强…

2024年PayPal贝宝账号最新注册教程,贝宝账号如何避免关联?

说到 PayPal 贝宝&#xff0c;跨境电商卖家们是再熟悉不过了&#xff0c;它支持全球众多电商平台&#xff0c;让卖家能够轻松收付款&#xff0c;很多卖家都需要用到 PayPal &#xff0c;今天就来和大家分享如何注册贝宝账号&#xff0c;而对于拥有十几个 PayPal 贝宝账号的卖家…

bash脚本简单界面(2)-设置选项

在有了简单界面可以选择执行命令后&#xff08;参见&#xff1a;bash脚本简单界面&#xff08;1&#xff09;-选择执行&#xff09;&#xff0c;有时候用户需要做些设置&#xff0c;让程序按照用户的设置去执行&#xff0c;如下&#xff1a; 输入52&#xff0c;回车&#xff0…

在Docker中安装Tomact

目录 前言&#xff1a; 一.安装Tomact 查找指定的tomact版本 下载tomact9.0 查看该镜像是否安装成功 安装成功之后就开始运行镜像了 ps&#xff08;用于列出正在运行的Docker容器&#xff09; ​编辑 测试(虚拟机ip:8080) ​编辑 解决措施 ​编辑 完成以上步骤&…

精密、CMOS、轨到轨输入/输出、宽带运算放大器MS8601/MS8602/MS8604

产品简述 MS8601/MS8602/MS8604 分别是单 / 双 / 四通道、轨到轨输入和输出、 单电源放大器&#xff0c;具有极低的失调电压和宽信号带宽。它采用 1.8V 至 5V 单电 源&#xff08; 0.9 V 至 2.5 V 双电源&#xff09;供电。 MS8601/MS8602/MS8604 低失调、极低的输入偏置…

互联网演进历程:从“全球等待”到“全球智慧”的技术革新与商业变革

文章目录 一、导言二、World Wide Wait (全球等待)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 三、World Wide Web (万维网)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 四、World Wide Wisdom (全球智慧)阶段1. 技术角度2. 用户体验3. 企业收益4. 教育影响 五、…

Bean如何诞生与消亡:生命周期探秘【beans 二】

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 Bean如何诞生与消亡&#xff1a;生命周期探秘【beans 二】 前言bean的创建过程bean的初始化阶段1. 实现InitializingBean接口&#xff1a;2. 使用PostConstruct注解&#xff1a; bean的属性注入1. Set…