深入理解可变参数

1.C语言方式

目录

1.C语言方式

1.1.宏介绍

1.2.原理详解

1.3.宏的可变参数

1.4.案例分析

1.5.其他实例

2.C++之std::initializer_list

2.1.简介

2.2.原理详解

2.3.案例分析

3.C++之可变参数模版

3.1.简介

3.2.可变参数个数

3.3.递归包展开

3.4.逗号表达式展开

3.5.Lambda 捕获

3.6.转发参数包

4.总结


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.宏的可变参数

标准C/C++语言宏定义的参数允许用三个小数点 ... 表示这里是可变参数,在宏替换的时候,用 __VA_ARGS__ 表示 ... 位置的所有的参数,例如:

#define example1(...) printf(__VA_ARGS__)
#define example2(fmt, ...) printf(fmt, __VA_ARGS__)

很多编译器扩展了可变参数的宏替换,参数后面带三个小数点,这样的写法更容易记忆,宏定义的参数后面可以带三个小数点,表示这里是可变参数,宏替换的时候,直接写这个参数就表示这个位置是所有的可变参数了。例如:

#define example1(fmt...) printf(fmt)
#define example2(fmt, args...) printf(fmt, args)

1.4.案例分析

#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.5.其他实例

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 构造函数)

3.C++之可变参数模版

3.1.简介

一个可变参数模板就是一个可以接受可变参数的模版函数或模板类;参数的类型是一种模板,是可经推导的,可以是任意存在的类型(系统类型或自定义类型);参数数目可变的,可以包括零个、一个或多个;可变数目的参数被称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包function parameterpacket),表示零个或多个函数参数。如:

template<typename... Arguments> class vtclass;vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
vtclass<long, std::vector<int>, std::string> vtinstance4;

3.2.可变参数个数

利用sizeof...()计算可变参数的大小,如:

template<class... Types>
struct count
{static const std::size_t value = sizeof...(Types);
};

3.3.递归包展开

C++的包展开是通过 args... 的形式,后面... 就意味着展开包,需要两个函数:递归终止函数 和 递归函数过程就是参数包在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用递归终止函数终止递归过程。如下:

#include <iostream>using namespace std;void print() {cout << endl;
}template <typename T> 
void print(const T& t) {  //边界条件cout << t << endl;
}template <typename First, typename... Rest> 
void print(const First& first, const Rest&... rest) {cout << first << ", ";print(rest...); //打印剩余参数,注意省略号必须有
}int main()
{print(); // calls first overload, outputting only a newlineprint(1); // calls second overload// these call the third overload, the variadic template,// which uses recursion as needed.print(10, 20);   //输出: 10, 20print(100, 200, 300); //输出:100, 200, 300print("first", 2, "third", 3.14159); //输出: first, 2, third, 3.14159
}

3.4.逗号表达式展开

逗号表达式是会从左到右依次计算各个表达式,并将最后一个表达式的值作为返回值返回;我们将最后一个表达式设为整型值,所以最后返回的是一个整型;将处理参数个数的动作封装成一个函数,将该函数作为逗号表达式的第一个表达式;…代表参数包,列表展开。如:

template <class T>
void printArg(T t) {cout << t << endl;
}//展开参数包
template <class ...Args>
void expand(Args... args) {int arr[] = { (printArg(args), 0)... };
}
int main()
{expand(1);expand(1, 'A');expand(1, "hello", 3);return 0;
}

函数执行expand(1, "hello", 3);的时候,调用expand,数组arr初始化会展开args参数,变化为:

int arr[] = {(printArg(1), 0), (printArg("hello"), 0), (printArg(3), 0)};

根据逗号表达式的规则,arr[] 还是 {0,0,0};

另外,还可以利用std::initializer_list 可以接收任意长度的初始化列表来展开包,如:

template<class F, class... Args>
void expand(const F& f, Args&&...args) {std::initializer_list<int>{(f(std::forward< Args>(args)), 0)...};
}int main()
{expand([](int i) { cout << i << endl; }, 23, 44, 2423);return 0;
}

3.5.Lambda 捕获

包展开可以在 lambda 表达式的捕获子句中出现:

template<class... Args>
void f(Args... args)
{auto lm = [&, args...] { return g(args...); };lm();
}

3.6.转发参数包

在C++11标准下,我们可以组合使用可变参数模板与std::forword机制来编写函数,实现将其实参不变地传递给其他函数,关于std::forward的详解讲解,可参考我的博客:C++之std::forward_c++ std::forward-CSDN博客

借助std::forward<Args>(args)... 就可以实现参数的完美转发了,如STL中map的插入函数emplace下:

template <class... _Valty>
iterator emplace(_Valty&&... _Val)
{return _Mybase::emplace(_STD forward<_Valty>(_Val)...).first;
}

4.总结

纸上得来终觉浅,绝知此事要躬行。

参考

形参包 (C++11 起) - cppreference.com

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

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

相关文章

C#编程-使用集合

使用集合 您学习了如何使用数组来有效地存储和操作相似类型额数据。但是,以下限制于数组的使用相关联: 您必须在声明时定义数组的大小。您必须编写代码以对数组执行标准操作,如排序。让我们思考一个示例。假设您想要存储在组织工作的五个雇员的姓名。您可以使用以下语句来声…

AJAX(三)跨域

一、同源策略 同源策略最早由Netscape公司提出&#xff0c;是浏览器的一种安全策略。 同源&#xff1a;协议、域名、端口号必须完全相同。&#xff08;同一个来源&#xff09; 违背同源策略就是跨域。 AJAX发送请求时是默认要遵循同源策略的&#xff0c;不是同源策略&#…

C++——类型转换

在文章的开始&#xff0c;先祝大家牢大年快乐 C语言中的类型转换 在C语言中&#xff0c;如果赋值运算两边类型不同&#xff0c;则会发生类型转换。一般来说&#xff0c;C语言有两种形式的类型转换&#xff1a;隐式转换和显式转换。 隐式转换&#xff0c;就是编译器自动根据其…

Mac Parallels19.1.0 Install CentOS7.9

0、资源准备 # centos7.9镜像一份 链接: https://pan.baidu.com/s/1acIjUnsTGhk_2cYCZLSoGg?pwd6666 提取码: 6666 --来自百度网盘超级会员v7的分享1、打开PD 2、选择镜像进行安装 指定镜像名称 创建 进行密码设置 安装目的地点开后直接点击完成 网络和主机名称 开…

学生数据可视化与分析工具 vue3+flask实现

目录 一、技术栈亮点 二、功能特点 三、应用场景 四、结语 学生数据可视化与分析工具介绍 在当今的教育领域&#xff0c;数据驱动的决策正变得越来越重要。为了满足学校、教师和学生对于数据深度洞察的需求&#xff0c;我们推出了一款基于Vue3和Flask编写的学生数据可视化…

选择排序算法

选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小&#xff08;或最大&#xff09;的一个元素&#xff0c;存放在序列的起始位置&#xff0c;直到全部待排序的数据元素排完。 选择排序的基本思想是&…

PC+Wap仿土巴兔装修报价器源码 PHP源码

核心功能&#xff1a; 业主自助预算计算&#xff1a;通过简洁的界面&#xff0c;业主可以输入装修需求&#xff0c;系统自动进行预算计算信息自动收集&#xff1a;系统自动收集业主的基本信息&#xff0c;如姓名、联系方式、房屋面积等一键发送报价&#xff1a;业主完成预算计…

【mysql】报错1349 - View‘s SELECT contains a subquery in the FROM clause

操作 创建视图的sql语句中有不支持子查询 mysql创建视图 select * from (select name,age from table_name where 11 and namea ) tb where 11 and type1问题 报错1349 - View’s SELECT contains a subquery in the FROM clause 原因 原因创建视图的sql语句中有不支持子查…

WEB前端知识点整理(JQUERY+Bootstrap+ECharts)

1.JQUERY的概述&#xff1a; jQuery 是一个 JavaScript 库。jQuery 极大地简化了JavaScript 编程&#xff0c;它很容易学习。 jQuery库包含以下功能&#xff1a;HTML 元素选取&#xff1b;HTML 元素操作&#xff1b;CSS 操作&#xff1b;HTML 事件函数&#xff1b;JavaScript …

Nginx - 使用error_page实现带有图片的自定义错误页面

文章目录 概述官网文档需求实现 概述 在Nginx中&#xff0c;您可以使用error_page指令来指定当请求遇到特定错误时应当显示的自定义错误页面。为了实现带有图片的自定义错误页面&#xff0c;可以按照以下步骤操作&#xff1a; 创建错误页面&#xff1a; 首先&#xff0c;需要…

如何选择最适合的采购付款 (P2P) 解决方案?

无论企业的业务流程执行得如何&#xff0c;流程中始终存在改进空间。更好的管理系统是获得更好结果的关键&#xff0c;尤其是当企业处于增长阶段时。强大的采购到付款&#xff08;P2P&#xff09;系统是加快采购流程&#xff0c;同时保持采购支出可见性的最有效方法之一。 什么…

python实现给定两个列表,“求同存异”

目录 问题描述&#xff1a; 代码实现&#xff1a; 问题描述&#xff1a; 给定两个列表&#xff0c;list1和list2。 python实现求list1和list中重复的元素&#xff0c;以及在list1中&#xff0c;不在list2的元素。 代码实现&#xff1a; def common_unique(pred_list, gold_l…

【信息论与编码】习题-单选题

目录 单选题1.下列说法正确的是&#xff08;B&#xff09;2.在信息论中&#xff0c;若用对数底2为&#xff0c;则信息量的单位为&#xff08;C&#xff09;3.率失真函数的下限为&#xff08;A&#xff09;4.给定xi条件下随机事件yj所包含的不确定度和条件自信息量p(yj /xi)。&a…

启动 Mac 时显示闪烁的问号

启动 Mac 时显示闪烁的问号 如果启动时在 Mac 屏幕上看到闪烁的问号&#xff0c;这意味着你的 Mac 无法找到自身的系统软件。 如果 Mac 启动时出现闪烁的问号且无法继续启动&#xff0c;请尝试以下步骤。 1.通过按住其电源按钮几秒钟来关闭 Mac。 2.按一下电源按钮&#xf…

爬虫与反爬-localStorage指纹(某易某盾滑块指纹检测)(Hook案例)

概述&#xff1a;本文将用于了解爬虫中localStorage的检测原理以及讲述一个用于检测localStorage的反爬虫案例&#xff0c;最后对该参数进行Hook断点定位 目录&#xff1a; 一、LocalStorage 二、爬虫中localStorage的案例&#xff08;以某盾滑块为例&#xff09; 三、如何…

Linux ssh 实现远程免密登录

一、背景 我搭建了一个 zookeeper 集群&#xff0c;写了一个 shell 脚本来控制集群的启动和关闭&#xff0c;但是我发现每次我执行 shell 脚本的时候&#xff0c;都需要我输入各个服务器的密码才可以运行&#xff0c;感觉很麻烦。shell 脚本里面连接其他服务器用的就是 ssh 的方…

RocketMQ MQClientInstance、生产者实例启动源码分析

&#x1f52d; 嗨&#xff0c;您好 &#x1f44b; 我是 vnjohn&#xff0c;在互联网企业担任 Java 开发&#xff0c;CSDN 优质创作者 &#x1f4d6; 推荐专栏&#xff1a;Spring、MySQL、Nacos、Java&#xff0c;后续其他专栏会持续优化更新迭代 &#x1f332;文章所在专栏&…

JVM篇:直接内存

直接内存 直接内存并不是JVM的内存结构&#xff0c;直接内存是操作系统的内存&#xff0c;Java本身并不能对操作系统的内存进行操作&#xff0c;而是通过调用本地方法。直接内存常用于NIO作为缓冲区存在&#xff0c;分配成本较高但是读写性能好&#xff0c;并且不受JVM内存回收…

有网友希望我推荐几个创建产品手册工具,这不就来了!

上次我有说到&#xff0c;企业应该充分认识到产品手册的重要性&#xff0c;并采取有效的策略和措施来制作和传播高质量的产品手册&#xff0c;以提升品牌知名度和市场份额。后台有网友问我除了设计排版的那种产品手册工具&#xff0c;还有什么方式可以去做产品手册。今天就介绍…

Verifiable Credentials可验证证书 2023 终极指南

1. 引言 Dock公司为去中心化数字身份领域的先驱者&#xff0c;其自2017年以来&#xff0c;已知专注于构建前沿的可验证证书&#xff08;Verifiable Credentials&#xff09;技术。本文将阐述何为电子证书、电子证书工作原理、以及其对组合和个人的重要性。 伪造实物证书和数字…