深入理解可变参数

news2024/11/26 4:31:28

目录

1.C语言方式

1.1.宏介绍

1.2.原理详解

1.3.案例分析

1.4.其他实例

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.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);  // 初始化args

	for (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 45
	return 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) {}  //1

    constexpr 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);
}
#endif

void  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}); // OK
    templated_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 }; //OK
	MyTest x2( 1,2 ); //OK
	MyTest 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 newline
    print(1); // calls second overload

    // these call the third overload, the variadic template,
    // which uses recursion as needed.
    print(10, 20);   //输出: 10, 20
    print(100, 200, 300); //输出:100, 200, 300
    print("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.coloradmin.cn/o/1357689.html

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

相关文章

YOLOv5算法进阶改进(13)— 更换上采样方式之CARAFE | 轻量级通用上采样算子

前言:Hello大家好,我是小哥谈。CARAFE算子是一种上采样运算符,全称为Content-Aware ReAssembly Feature Extraction,它在图像语义分割任务中被广泛应用。CARAFE算子通过学习像素之间的关系来进行上采样,从而提高了图像分割的精度。CARAFE算子的优势在于它能够根据图像的内…

Hotspot源码解析-第十二章-线程栈保护页

了解保护页&#xff0c;先从几个问题开始吧 1、为什么线程栈有栈帧了&#xff0c;还要有保护页&#xff1f; 答&#xff1a;在操作系统中内存可以看成是一个大数组&#xff0c;这就有一个问题&#xff0c;线程之间可能会互相踩了别人的内存空间&#xff0c;所以栈空间也存在这…

鸿鹄电子招投标系统:源码级别解析电子招投标的精髓

招投标管理系统是一个集门户管理、立项管理、采购项目管理、采购公告管理、考核管理、报表管理、评审管理、企业管理、采购管理和系统管理于一体的综合性应用平台。它适用于招标代理、政府采购、企业采购和工程交易等业务的企业&#xff0c;旨在提高项目管理的效率和质量。该系…

MySQL备份总结

物理备份 通常物理备份是保障数据库备份最有效的方法&#xff0c;优于逻辑备份&#xff0c;且其对数据库本身性能的损耗相较于逻辑备份较低。MySQL的物理备份通常我们使用Percona公司出品的基于 MySQL 的服务器的开源热备份实用程序。其使用方法如下&#xff1a; #下载 MySQL…

python毕设选题 - flink大数据淘宝用户行为数据实时分析与可视化

文章目录 0 前言1、环境准备1.1 flink 下载相关 jar 包1.2 生成 kafka 数据1.3 开发前的三个小 tip 2、flink-sql 客户端编写运行 sql2.1 创建 kafka 数据源表2.2 指标统计&#xff1a;每小时成交量2.2.1 创建 es 结果表&#xff0c; 存放每小时的成交量2.2.2 执行 sql &#x…

IOC解决程序耦合

1.什么是IOC IOC (Inverse of Control)即控制反转&#xff1a;由ioc容器来创建依赖对象&#xff0c;程序只需要从IOC容器获取创建好的对象。 我们在获取对象时&#xff0c;都是采用new的方式。是主动的。 我们获取对象时&#xff0c;同时跟工厂要&#xff0c;有工厂为我们查找…

大数据Doris(五十):数据导出的其他导出案例参考

文章目录 数据导出的其他导出案例参考 一、​​​​​

乘号在键盘上怎么打?速来学习这4个方法!

“我在用电脑编辑文档时&#xff0c;需要输入一些数学公式&#xff0c;现在我不知道乘号怎么打&#xff0c;有朋友可以教教我吗&#xff1f;” 乘号是数学中常用的符号&#xff0c;用于表示两个数的相乘关系。很多用户在使用电脑办公时可能需要输入乘号。但是却发现键盘上没有明…

sql如何获取字段是数组中的数字【搬代码】

我们可以看到表中字段是一个数组怎么获取其中的数据呢&#xff1f; SELECT sim->>$[0] FROM fin_xxx如果使用左外链接&#xff0c;如下&#xff0c;其他连接时一样的 SELECT a.* FROM fin_aaaa a LEFT JOIN fin_xxx b ON b.sim_r->>$[0]a.corr WHERE b.tid20210 …

【React系列】Redux(一)管理状态

本文来自#React系列教程&#xff1a;https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 在React的开发过程中&#xff0c;Redux对于我们是非常重要的。 但是对于很多人来说&#xff0c;初次接触redux会感觉r…

Leetcode_day01_88合并两个有序数组

Leetcode_day01_88合并两个有序数组 题目描述&#xff1a; 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 nums1 中&#xff0c;使合并后的数组同样按 非递减顺…

锂电池寿命预测 | Matlab基于LSTM长短期记忆神经网络的锂电池寿命预测

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 锂电池寿命预测 | Matlab基于LSTM长短期记忆神经网络的锂电池寿命预测 程序设计 完整程序和数据获取方式&#xff1a;私信博主回复Matlab基于LSTM长短期记忆神经网络的锂电池寿命预测。 参考资料 [1] http://t.csdn…

【已解决】若依系统前端打包后,部署在nginx上,点击菜单错误:@/views/system/role/index

​ 上面错误&#xff0c;是因为/views/system/role/index动态路由按需加载时候&#xff0c;错误导致。 解决办法&#xff1a; 如果您的前端项目访问时候&#xff0c;需要带有项目名称的话&#xff0c;参考凯哥上一篇文章&#xff1a;【已解决】若依前后端分离版本&#xff0…

《MySQL系列-InnoDB引擎05》MySQL索引与算法

文章目录 第五章 索引与算法1 InnoDB存储引擎索引概述2 数据结构与算法2.1 二分查找法2.2 二分查找树和平衡二叉树 3 B树3.1 B树的插入操作3.2 B树的删除操作 4 B树索引4.1 聚集索引4.2 辅助索引4.3 B树索引的分裂 5 Cardinality值5.1 什么是Cardinality5.2 InnoDB存储引擎的Ca…

树与二叉树笔记整理

摘自小红书 ## 树与二叉树 ## 排序总结

Zookeeper(持续更新)

VIP-01 Zookeeper特性与节点数据类型详解 文章目录 VIP-01 Zookeeper特性与节点数据类型详解正文1. 什么是Zookeeper&#xff1f;2. Zookeeper 核心概念2.1、 文件系统数据结构2.2、监听通知机制2.3、Zookeeper 经典的应用场景3.2. 使用命令行操作zookeeper 正文 什么是Zookee…

VMware Workstation虚拟机CentOS 7.9 配置固定ip的步骤

VMware Workstation虚拟机CentOS7.9配置固定ip的步骤 编辑虚拟机 打开VMware Workstation。 选择要配置的虚拟机&#xff0c;但不要启动它。 点击“编辑虚拟机设置”&#xff08;Edit virtual machine settings&#xff09;。 选择“网络适配器”&#xff08;Network Adapter&…

JAVA基础学习笔记-day13-数据结构与集合源1

JAVA基础学习笔记-day13-数据结构与集合源1 1. 数据结构剖析1.1 研究对象一&#xff1a;数据间逻辑关系1.2 研究对象二&#xff1a;数据的存储结构&#xff08;或物理结构&#xff09;1.3 研究对象三&#xff1a;运算结构1.4 小结 2. 一维数组2.1 数组的特点 3. 链表3.1 链表的…

c++牛客总结

一、c/c语言基础 1、基础 1、指针和引用的区别 指针是一个新的变量&#xff0c;指向另一个变量的地址&#xff0c;我们可以通过这个地址来修改该另一个变量&#xff1b; 引用是一个别名&#xff0c;对引用的操作就是对变量本身进行操作&#xff1b;指针可以有多级 引用只有一…

【React系列】Redux(二)中间件

本文来自#React系列教程&#xff1a;https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. 中间件的使用 1.1. 组件中异步请求 在之前简单的案例中&#xff0c;redux中保存的counter是一个本地定义的数据…