【C++】C++11 ——— 可变参数模板

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:C++学习
🎯长路漫漫浩浩,万事皆有期待

上一篇博客:【C++】STL详解(九)—— set、map、multiset、multimap的介绍及使用

文章目录

  • 可变参数模板的概念
  • 可变参数模板的定义方式
  • 参数包的展开方式
    • 递归展开参数包
    • 逗号表达式展开参数包
  • STL容器中的emplace相关接口函数
  • 总结:

可变参数模板的概念

可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。

在C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。

说明:本篇博客只讲解函数模板的可变参数。

可变参数模板的定义方式

函数的可变参数模板定义方式如下:

template<class …Args>
返回类型 函数名(Args… args)
{//函数体
}

例如:

template<class ...Args>
void ShowList(Args... args)
{}

说明一下:

模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N ( N ≥ 0 ) N(N\geq 0)N(N≥0)个模板参数,而args则是一个函数形参参数包。
模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args。

现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。比如:

int main()
{ShowList();ShowList(1);ShowList(1, 'A');ShowList(1, 'A', string("hello"));return 0;
}

我们可以在函数模板中通过sizeof计算参数包中参数的个数。比如:

template<class ...Args>
void ShowList(Args... args)
{cout << sizeof...(args) << endl; //获取参数包中参数的个数
}

但是我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。

特别注意,语法并不支持使用args[i]的方式来获取参数包中的参数。比如:

template<class ...Args>
void ShowList(Args... args)
{//错误示例:for (int i = 0; i < sizeof...(args); i++){cout << args[i] << " "; //打印参数包中的每个参数}cout << endl;
}

因此要获取参数包中的各个参数,只能通过展开参数包的方式来获取,一般我们会通过递归或逗号表达式来展开参数包。

参数包的展开方式

递归展开参数包

递归展开参数包的方式如下:

给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数出来。
在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
如此递归下去,每次分离出参数包中的一个参数,直到参数包中的所有参数都被取出来。

比如我们要打印调用函数时传入的各个参数,那么函数模板可以这样编写:

//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << " "; //打印分离出的第一个参数ShowList(args...);    //递归调用,将参数包继续向下传
}

这时我们面临的问题就是,如何终止函数的递归调用。

编写无参的递归终止函数

我们可以在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同。如下:

//递归终止函数
void ShowList()
{cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << " "; //打印分离出的第一个参数ShowList(args...);    //递归调用,将参数包继续向下传
}

这样一来,当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。

但如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数。

鉴于此,我们可以将展开函数和递归调用函数的函数名改为ShowListArg,然后重新编写一个ShowList函数模板,该函数模板的函数体中要做的就是调用ShowListArg函数展开参数包。比如:

//递归终止函数
void ShowListArg()
{cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{cout << value << " "; //打印传入的若干参数中的第一个参数ShowListArg(args...); //将剩下参数继续向下传
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{ShowListArg(args...);
}

这时无论外部调用时传入多少个参数,最终匹配到的都是同一个函数了。

编写带参的递归终止函数

除了编写无参的递归终止函数,也可以编写带参数的递归终止函数来终止递归,比如这里编写带一个参数的递归终止函数:

//递归终止函数
template<class T>
void ShowListArg(const T& t)
{cout << t << endl;
}
//展开函数
template<class T, class ...Args>
void ShowListArg(T value, Args... args)
{cout << value << " "; //打印传入的若干参数中的第一个参数ShowList(args...);    //将剩下参数继续向下传
}
//供外部调用的函数
template<class ...Args>
void ShowList(Args... args)
{ShowListArg(args...);
}

这样一来,在递归调用过程中,如果传入的参数包中参数的个数为1,那么就会匹配到这个递归终止函数,这样也就结束了递归。但是需要注意,这里的递归调用函数需要写成函数模板,因为我们并不知道最后一个参数是什么类型的。

但该方法有一个弊端就是,我们在调用ShowList函数时必须至少传入一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。

判断参数包中参数的个数(不可行!)

既然我们可以通过sizeof计算出参数包中参数的个数,那我们能不能在ShowList函数中设置一个判断,当参数包中参数个数为0时就终止递归呢?比如:

//错误示例
template<class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << " "; //打印传入的若干参数中的第一个参数if (sizeof...(args) == 0){return;}ShowList(args...);    //将剩下参数继续向下传
}

这种方式是不可行的,原因如下:

函数模板并不能调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
而这个推演过程是在编译时进行的,当推演到参数包args中参数个数为0时,还需要将当前函数推演完毕,这时就会继续推演传入0个参数时的ShowList函数,此时就会产生报错,因为ShowList函数要求至少传入一个参数。
这里编写的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。

逗号表达式展开参数包

通过列表获取参数包中的参数

数组可以通过列表进行初始化,比如:

int a[] = {1,2,3,4}

除此之外,如果参数包中各个参数的类型都是整型,那么也可以把这个参数包放到列表当中初始化这个整型数组,此时参数包中参数就放到数组中了。比如:

//展开函数
template<class ...Args>
void ShowList(Args... args)
{int arr[] = { args... }; //列表初始化//打印参数包中的各个参数for (auto e : arr){cout << e << " ";}cout << endl;
}

这时调用ShowList函数时就可以传入多个整型参数了。比如:

int main()
{ShowList(1);ShowList(1, 2);ShowList(1, 2, 3);return 0;
}

但C++并不像Python这样的语言,C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用ShowList函数时传入的参数只能是整型的,并且还不能传入0个参数,因为数组的大小不能为0,因此我们还需要在此基础上借助逗号表达式来展开参数包。

通过逗号表达式展开参数包

虽然我们不能用不同类型的参数去初始化一个整型数组,但我们可以借助逗号表达式。

逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回。
将逗号表达式的最后一个表达式设置为一个整型值,确保逗号表达式返回的是一个整型值。
将处理参数包中参数的动作封装成一个函数,将该函数的调用作为逗号表达式的第一个表达式。

这样一来,在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。比如:

//处理参数包中的每个参数
template<class T>
void PrintArg(const T& t)
{cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式cout << endl;
}

说明一下:

我们这里要做的就是打印参数包中的各个参数,因此处理函数当中要做的就是将传入的参数进行打印即可。
可变参数的省略号需要加在逗号表达式外面,表示需要将逗号表达式展开,如果将省略号加在args的后面,那么参数包将会被展开后全部传入PrintArg函数,代码中的{(PrintArg(args), 0)…}将会展开成{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc…}。

这时调用ShowList函数时就可以传入多个不同类型的参数了,但调用时仍然不能传入0个参数,因为数组的大小不能为0,如果想要支持传入0个参数,也可以写一个无参的ShowList函数。比如:

//支持无参调用
void ShowList()
{cout << endl;
}
//处理函数
template<class T>
void PrintArg(const T& t)
{cout << t << " ";
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args), 0)... }; //列表初始化+逗号表达式cout << endl;
}

实际上我们也可以不用逗号表达式,因为这里的问题就是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设置为整型,然后用这个返回值去初始化整型数组也是可以的。比如:

//支持无参调用
void ShowList()
{cout << endl;
}
//处理函数
template<class T>
int PrintArg(const T& t)
{cout << t << " ";return 0;
}
//展开函数
template<class ...Args>
void ShowList(Args... args)
{int arr[] = { PrintArg(args)... }; //列表初始化cout << endl;
}

STL容器中的emplace相关接口函数

emplace版本的插入接口

C++11标准给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:
在这里插入图片描述

这些emplace版本的插入接口支持模板的可变参数,比如list容器的emplace_back函数的声明如下:
在这里插入图片描述

注意: emplace系列接口的可变模板参数类型都带有“&&”,这个表示的是万能引用,而不是右值引用。

emplace系列接口的使用方式

emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

以list容器的emplace_back和push_back为例:

调用push_back函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
调用emplace_back函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
除此之外,emplace系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。

比如:

int main()
{list<pair<int, string>> mylist;pair<int, string> kv(10, "111");mylist.push_back(kv);                              //传左值mylist.push_back(pair<int, string>(20, "222"));    //传右值mylist.push_back({ 30, "333" });                   //列表初始化mylist.emplace_back(kv);                           //传左值mylist.emplace_back(pair<int, string>(40, "444")); //传右值mylist.emplace_back(50, "555");                    //传参数包return 0;
}

emplace系列接口的工作流程

emplace系列接口的工作流程如下:
先通过空间配置器为新结点获取一块内存空间,注意这里只会开辟空间,不会自动调用构造函数对这块空间进行初始化。
然后调用allocator_traits::construct函数对这块空间进行初始化,调用该函数时会传入这块空间的地址和用户传入的参数(需要经过完美转发)。
在allocator_traits::construct函数中会使用定位new表达式,显示调用构造函数对这块空间进行初始化,调用构造函数时会传入用户传入的参数(需要经过完美转发)。
将初始化好的新结点插入到对应的数据结构当中,比如list容器就是将新结点插入到底层的双链表中。

emplace系列接口的意义

由于emplace系列接口的可变模板参数的类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。
如果调用emplace系列接口时传入的是左值对象,那么首先需要先在此之前调用构造函数实例化出一个左值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数。
如果调用emplace系列接口时传入的是右值对象,那么就需要在此之前调用构造函数实例化出一个右值对象,最终在使用定位new表达式调用构造函数对空间进行初始化时,就会匹配到移动构造函数。
如果调用emplace系列接口时传入的是参数包,那就可以直接调用函数进行插入,并且最终在使用定位new表达式调用构造函数对空间进行初始化时,匹配到的是构造函数。

总结一下:
传入左值对象,需要调用构造函数+拷贝构造函数。
传入右值对象,需要调用构造函数+移动构造函数。
传入参数包,只需要调用构造函数。

当然,这里的前提是容器中存储的元素所对应的类,是一个需要深拷贝的类,并且该类实现了移动构造函数。否则调用emplace系列接口时,传入左值对象和传入右值对象的效果都是一样的,都需要调用一次构造函数和一次拷贝构造函数。

实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的,因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口,如果调用这些接口时如果传入的是右值对象,那么最终也是会调用对应的移动构造函数进行资源的移动的。

emplace接口的意义:

emplace系列接口最大的特点就是支持传入参数包,用这些参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么有人说emplace系列接口更高效的原因。
但emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是一样的。
emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝。

验证

如果要验证我们上述对emplace系列接口的说法,需要借助一个深拷贝的类,下面模拟实现了一个简化版的string类,类当中只编写了我们需要用到的成员函数。

代码如下:

namespace sherry
{class string{public://构造函数string(const char* str = ""){cout << "string(const char* str) -- 构造函数" << endl;_size = strlen(str); //初始时,字符串大小设置为字符串长度_capacity = _size; //初始时,字符串容量设置为字符串长度_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')strcpy(_str, str); //将C字符串拷贝到已开好的空间}//交换两个对象的数据void swap(string& s){//调用库里的swap::swap(_str, s._str); //交换两个对象的C字符串::swap(_size, s._size); //交换两个对象的大小::swap(_capacity, s._capacity); //交换两个对象的容量}//拷贝构造函数(现代写法)string(const string& s):_str(nullptr), _size(0), _capacity(0){cout << "string(const string& s) -- 拷贝构造" << endl;string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); //交换这两个对象}//移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}//拷贝赋值函数(现代写法)string& operator=(const string& s){cout << "string& operator=(const string& s) -- 深拷贝" << endl;string tmp(s); //用s拷贝构造出对象tmpswap(tmp); //交换这两个对象return *this; //返回左值(支持连续赋值)}//移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}//析构函数~string(){//delete[] _str;  //释放_str指向的空间_str = nullptr; //及时置空,防止非法访问_size = 0;      //大小置0_capacity = 0;  //容量置0}private:char* _str;size_t _size;size_t _capacity;};
}

由于我们在string的构造函数、拷贝构造函数和移动构造函数当中均打印了一条提示语句,因此我们可以通过控制台输出来判断这些函数是否被调用。

下面我们用一个容器来存储模拟实现的string,并以不同的传参形式调用emplace系列函数。比如:

int main()
{list<pair<int, cl::string>> mylist;pair<int, cl::string> kv(1, "one");mylist.emplace_back(kv);                              //传左值cout << endl;mylist.emplace_back(pair<int, cl::string>(2, "two")); //传右值cout << endl;mylist.emplace_back(3, "three");                      //传参数包return 0;
}

运行结果如下:
在这里插入图片描述

说明一下:

模拟实现string的拷贝构造函数时复用了构造函数,因此在调用string拷贝构造的后面会紧跟着调用一次构造函数。
为了更好的体现出参数包的概念,因此这里list容器中存储的元素类型是pair,我们是通过观察string对象的处理过程来判断pair的处理过程的。

这里也可以以不同的传参方式调用push_back函数,顺便验证一下容器原有的插入函数的执行逻辑。比如:

int main()
{list<pair<int, cl::string>> mylist;pair<int, cl::string> kv(1, "one");mylist.push_back(kv);                              //传左值cout << endl;mylist.push_back(pair<int, cl::string>(2, "two")); //传右值cout << endl;mylist.push_back({ 3, "three" });                  //列表初始化return 0;
}

运行结果如下:
在这里插入图片描述

总结:

今天我们学习了C++11中的可变参数模板,了解了一些有关的底层原理。接下来,我们将继续进行C++11的学习。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

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

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

相关文章

【深度学习实验】循环神经网络(四):基于 LSTM 的语言模型训练

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. RNN与梯度裁剪 2. LSTM模型 3. 训练函数 a. train_epoch b. train 4. 文本预测 5. GPU判断函数 6. 训练与测试 7. 代码整合 经验是智慧之父&#xff0c;记忆…

电脑提示Explorer.exe系统错误该怎么办?

平时我们在使用电脑时&#xff0c;系统有时会提示Explorer.exe系统错误&#xff0c;很多用户在遇到这类问题时不知道该怎么办。遇到Explorer.exe系统错误&#xff0c;该怎么办呢&#xff1f;下面我们一起来了解一下。 怎么修复Explorer.exe系统错误&#xff1f; Explorer.exe是…

MySQL多表查询面试题一

其中分析题意&#xff0c;学生表student是与成绩表score关联&#xff0c;课程表course与教师表teacher关联&#xff0c;由此可以先确定关联关系&#xff0c;学生表为s&#xff0c;课程表为c&#xff0c;教师表为t&#xff0c;成绩表为o。s.s_ido.s_id&#xff0c;c.t_idt.t_id …

【AI】深度学习——循环神经网络

神经元不仅接收其他神经元的信息&#xff0c;也能接收自身的信息。 循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;是一类具有短期记忆能力的神经网络&#xff0c;可以更方便地建模长时间间隔的相关性 常用的参数学习可以为BPTT。当输入序列比较…

【数字IC设计】DC自动添加门控时钟

简介 数字电路的动态功耗主要是由于寄存器翻转带来的&#xff0c;为了降低芯片内部功耗&#xff0c;门控时钟的方案应运而生。作为低功耗设计的一种方法&#xff0c;门控时钟是指在数据无效时将寄存器的时钟关闭&#xff0c;以此来降低动态功耗。 在下图中&#xff0c;展示了…

【自然语言处理】— 隐马尔可夫模型详解、例解

【自然语言处理】— 隐马尔可夫模型 【自然语言处理】— 隐马尔可夫模型引例隐马尔可夫模型概念隐马尔可夫模型的关键隐马尔可夫模型的数学表示隐含状态与观测结果状态转移矩阵观测概率矩阵初始状态概率向量 小结 【自然语言处理】— 隐马尔可夫模型 引例 假设有三种不同的骰…

17 - 并发容器的使用:识别不同场景下最优容器

在并发编程中&#xff0c;我们经常会用到容器。今天我要和你分享的话题就是&#xff1a;在不同场景下我们该如何选择最优容器。 1、并发场景下的 Map 容器 假设我们现在要给一个电商系统设计一个简单的统计商品销量 TOP 10 的功能。常规情况下&#xff0c;我们是用一个哈希表…

【UBOOT】1-使用与烧写

​一、uboot简介 1&#xff09;uboot是一个裸机程序&#xff0c;比较复杂 2&#xff09;最主要的作用是引导Linux内核启动&#xff1b; 初始化DDR&#xff1b; 因为Linux是运行在DDR里面的&#xff1b;而Linux镜像&#xff08;zImage或uImagedtb&#xff09;一般存放在SD EMM…

理解LoadRunner,基于此工具进行后端性能测试的详细过程(下)

5、录制并增强虚拟用户脚本 从整体角度看&#xff0c;用LoadRunner 开发虚拟用户脚本主要包括下面四步骤&#xff1a; 识别测试应用使用的协议 录制脚本 完善录制得到的脚本 验证脚本的正确性 识别被测应用使用的协议 如果明确知道了被测系统所采用的协议&#xff0c;可…

网卡介绍篇

目录 1.什么是网卡&#xff1f; 2.网卡的演进 3.网卡的主要功能 4.服务器网卡介绍 5.服务器网卡的分类 6.网卡接口介绍 7.业界网卡主流厂商 8.SmartNIC概念介绍 9.SmartNIC产业发展趋势 10.SmartNIC实现 10-1.实现形式 10-2.SmartNIC不同实现技术对比 11.Mellanox…

怎么把flac音频变为mp3?

怎么把flac音频变为mp3&#xff1f;FLAC音频格式在许多平台和应用程序中都得到支持和应用。FLAC音频格式被广泛支持和应用。许多平台、设备和应用程序都支持FLAC格式&#xff0c;如Windows、macOS和Linux操作系统、各种音乐播放器软件、智能手机和平板电脑、在线音乐平台和流媒…

python+django学生选课管理系统_wxjjv

1&#xff09;前台&#xff1a;首页、课程信息、校园论坛、校园公告、个人中心、后台管理。 &#xff08;2&#xff09;管理员&#xff1a;首页、个人中心、学生管理、教师管理课、程信息管理、课程分类管理、选课信息管理、作业信息管理、提交作业管理、学生成绩管理、校园论…

Linux常见的指令合集

Linux指令合集 认识linuxlinux基础指令1.pwd 命令2. ls 命令3.cd 命令4. man 命令5. grep 命令6. ps 命令7. kill 命令8. netstat 命令9. date 查看当前系统时间10. echo 打印选项 -e linux文件操作指令1. mkdir 命令2. rmdir 命令3. touch 命令4. rm 命令5. mv 命令6. cp 命令…

ImgPlus:基于CodeFormer的图片增强

背景 最近参与了华为云开发者大会AI赛道&#xff0c;做了一个AI图片增强作品&#xff0c;本片文章来简单介绍一下。 正文 作品名称&#xff1a;ImgPlus 赛题技术领域选择&#xff1a; AI&#xff0c;图片增强 使用技术名称&#xff1a; CodeFormer&#xff0c;ECS&#xff0…

Puppeteer结合测试工具jest使用(四)

Puppeteer结合测试工具jest使用&#xff08;四&#xff09; Puppeteer结合测试工具jest使用&#xff08;四&#xff09;一、简介二、与jest结合使用&#xff0c;集成到常规测试三、支持其他的几种四、总结 一、简介 Puppeteer是一个提供自动化控制Chrome或Chromium浏览器的Node…

session认证

目录 前言 http协议的无状态性 session的工作原理 在express中使用session认证 在session中存数据 在session中取数据 清空session 结尾 前言 session是一种记录客户状态的机制&#xff0c;客户端浏览器法访问服务器的时候&#xff0c;服务器把客户端信息以某种形式记录…

WLAN 无线案例(华为AC控制器配置模板)

实验说明&#xff1a; 无线用户VLAN 30 192.168.30.0/24 AP和AC用VLAN 20 192.168.20.0/24 有线网段 VLAN 10 192.168.10.0/24 步骤一&#xff1a;全网互通 sw1&#xff1a; sysname sw1 # vlan batch 10 20 30 # dhcp enable # ip pool 20 gateway-list 192.168.20.1…

JAVA学习(6)-全网最详细~

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

UDP与TCP协议

很抱歉&#xff0c;我之前写好的UDP与TCP文章不小心被删了&#xff0c;所以&#xff0c;这篇文章只有一半&#xff0c;后面我会尽快补全。 在完成HTTPS的学习后&#xff0c;我们就完成了应用层的所有讲解&#xff0c;下面我们开始讲解传输层&#xff0c;这一层常用的协议为TCP…

MySQL的index merge(索引合并)导致数据库死锁分析与解决方案 | 京东云技术团队

背景 在DBS-集群列表-更多-连接查询-死锁中&#xff0c;看到9月22日有数据库死锁日志&#xff0c;后排查发现是因为mysql的优化-index merge&#xff08;索引合并&#xff09;导致数据库死锁。 定义 index merge(索引合并)&#xff1a;该数据库查询优化的一种技术&#xff0…