【C++11】右值引用和移动语义 {左值引用和右值引用;移动语义;解决函数传值返回的深拷贝问题;完美转发}

news2024/11/25 13:25:07

一、左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

  • 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取左值的地址,可以对左值赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边。定义const修饰的左值,不能赋值,但是可以取地址。能取地址的就是左值

  • 左值引用就是给左值的引用,给左值取别名。

  • 左值引用只能引用左值,不能引用右值。

  • 但是const左值引用既可引用左值,也可引用右值。

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
    
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
    
    // 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
    
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

什么是右值?什么是右值引用?

  • 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(传值返回) 等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址,不能赋值

  • 右值引用就是对右值的引用,给右值取别名。

  • 右值引用只能引用右值,不能引用左值。

  • 但是右值引用可以引用move以后的左值。

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	
    // 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
 
    //右值不能取地址,不能赋值
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    &10;
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
    
    // 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	//int a = 10; 
	//int&& r2 = a;	// error:无法从“int”转换为“int &&”;无法将左值绑定到右值引用
    
	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}

二、右值引用的使用场景

左值引用既可以引用左值和又可以引用右值(const左值引用),那为什么C++11还要提出右值引用呢?

  • 左值引用做函数参数可以减少拷贝次数,提高效率。但有时我们需要区分函数参数到底是左值引用还是右值引用。这是const左值引用无法做到的。

  • C++11提出右值引用之后,我们就可以重载一个参数是右值引用的函数,与左值引用进行区分处理。

  • const左值引用可以引用左值和也可以引用右值,但如果有专门的右值引用函数,编译器会优先选择后者。

区分出是右值引用后要干什么呢?

  • 之前我们只有左值引用,由于无法区分引用对象是左值还是右值,所以如果函数内涉及到对复杂类型的拷贝工作时,我们只能统统进行深拷贝,效率底下。
  • 而实际上右值对象通常都是一些匿名对象、函数返回值、表达式返回值等临时对象。其内部资源会在完成拷贝工作后立即被销毁。所以对右值复杂对象的深拷贝其实是一种浪费。
  • 右值引用的应用场景主要是移动语义。简单来说,移动语义就是将原本对右值复杂类型(涉及资源申请)的深拷贝工作,转变为直接移动其内部资源(主要指动态内存)。 移动语义的实现,减少了拷贝次数,提高了程序效率。
  • 移动语义包括:移动构造,移动赋值,移动插入。同时移动构造和移动赋值的实现解决了函数传值返回的深拷贝问题。

2.1 移动构造

以之前模拟实现的string类为例:【STL】模拟实现string类-CSDN博客

在拷贝构造的过程中:

  • 如果拷贝对象是左值,则必须进行深拷贝。

  • 但如果拷贝对象是右值,可以进行移动构造,提高效率。因为右值对象(又叫将亡值)会在完成构造后自动销毁,所以我们可以将右值对象的资源直接拿来占用,免去了开空间和拷贝数据的工作。

class string{
private:
	char *_str = nullptr; //注意!一定要将指针初始化为nullptr,防止野指针错误。
	size_t _size = 0;
	size_t _capacity = 0;
	
public:
  //拷贝构造
  Mystring(const Mystring &str){ //左值引用
    _size = str._size;
    _capacity = str._capacity;
    _str = new char[_capacity+1];
    memcpy(_str, str._str, str._size+1);
  }  
    
  //移动构造
  Mystring(Mystring &&str){ //右值引用
    swap(str);
  }  
    
  void swap(Mystring &str){
    ::swap(_str, str._str); 
    ::swap(_size, str._size);
    ::swap(_capacity, str._capacity);
  }
};

int main(){
	string str1 = "abc"; //构造(隐式类型转换)
    string str2 = str1; //左值构造——拷贝构造
    string str3 = str1 + str2; //右值构造——移动构造
  	return0;  
}

提示:

  • 注意!一定要将指针初始化为nullptr,防止野指针错误。

  • 在拷贝构造中,左值引用加const,只是为了保证对象在拷贝过程中不被修改。

  • 在移动构造中,右值引用不能加const,因为要在构造时移动右值对象的内部资源。


2.2 移动赋值

移动赋值也是同样的道理:

  • 如果拷贝对象是左值,则必须进行深拷贝。

  • 但如果拷贝对象是右值,可以进行移动赋值。因为右值对象(又叫将亡值)会在完成赋值操作后自动销毁,所以我们可以将右值对象的资源直接拿来占用,同时将赋值对象的原数据交换给右值对象让其帮助销毁。

class string{
  //拷贝赋值
  Mystring& operator=(const Mystring &str){ //左值引用
    if(this != &str) 
    {
      char *tmp = new char[str._capacity+1]; 
      memcpy(tmp, str._str, str._size+1);
      
      delete[] _str; 
      _str = tmp;
      _size = str._size;
      _capacity = str._capacity;
    }
    return *this; 
  }
	
  //移动赋值
  Mystring& operator=(Mystring &&str){ //右值引用 
    swap(str);
    return *this; 
  }
};

int main(){
    string str1 = "abc";
    string str2 = "def";
    str1 = str2; //左值赋值——拷贝赋值
    str1 = "ghi"; //右值赋值——移动赋值    
}

如果想让左值进行移动构造或者移动赋值怎么办?用move()!

在这里插入图片描述

move是一个函数模版,返回指定对象的右值引用,用于将左值临时转换为右值

int main(){
	string str1 = "abc"; 
    string str2 = str1; //左值构造——拷贝构造
    string str3 = move(str1); //move将str1临时转为右值——移动构造。
    //完成移动构造之后,str1中的资源就被转移走了,此时str1为空。
  	return 0}

2.3 移动插入

C++11以后,STL中的所有容器都增加了移动插入接口。

  • 原来C++98中的插入接口其实都是拷贝插入,即不管要插入的元素是左值还是右值都统统需要重新开空间并进行数据拷贝。

  • 而C++11中的移动插入接口则不同,如果插入的元素是右值,则直接移动其资源,无需进行拷贝,提高效率。

在这里插入图片描述

以list为例:

int main(){
    list<string> ls;
    string str = "hello world!";
    ls.push_back(str); //插入左值——拷贝插入
    ls.push_back(move(str)); //move将左值临时转为右值——移动插入
    ls.push_back("china"); //插入右值——移动插入
}

提示:list移动插入的模拟实现在【完美转发的使用场景】部分介绍。


2.4 解决传值返回的深拷贝问题

首先在讲解这个问题的解决方法之前,我们需要先回顾一下编译器是如何优化连续的构造和拷贝构造的:

【Object-Oriented C++】类和对象(下) {初始化列表,explicit关键字,匿名对象,static成员,友元,内部类,优化连续的构造和拷贝构造}_芥末虾的博客-CSDN博客

  • 在一条语句中,连续的构造和拷贝构造一般都会被编译器优化,将两个过程合二为一
  • 但是编译器不会对连续的拷贝构造和赋值重载进行优化,不能将两个过程合二为一。

因此当函数传值返回时,构造接收返回值和赋值接收返回值的优化结果是不同的,因该一分为二的看待。

2.4.1 构造接收

如果函数的返回值是一个局部对象,出了函数作用域就会被销毁,就不能使用引用返回,只能传值返回。

例如:在bit::string to_string(int value)函数中可以看到,这里只能使用传值返回。传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造),效率较低。

在这里插入图片描述

但C++11引入了右值引用之后,传值返回的深拷贝问题得到了彻底的解决。

在bit::string类中增加移动构造函数,再去调用bit::to_string(1234)

在这里插入图片描述

  1. 编译器会在返回对象进行销毁之前(调用析构函数),先将其临时转换为右值(类似于move());
  2. 然后调用移动构造,将返回对象中的资源直接移动到接收对象中,完成接收对象的构造。
  3. 最后才销毁返回对象,释放空间 。

注意:

  • 编译器在优化传值返回时,对析构函数的调用顺序做了特殊调整。

  • 不能显示的返回局部对象的右值引用。如果是显示返回,会先析构,再返回。在函数外访问时,空间已经被销毁。


2.4.2 赋值接收

再在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动赋值。

注意:编译器不会对连续的拷贝构造和赋值重载进行优化,不能将两个过程合二为一。

在这里插入图片描述

  1. 这里运行后,我们会发现调用了一次移动构造和一次移动赋值。
  2. 因为如果是用一个已经存在的对象赋值接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象。
  3. 但是我们可以看到,编译器很聪明的在这里把str转换成了右值,调用移动构造来构造临时对象。
  4. 然后再把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的是移动赋值。

总的来说,不管是构造接收还是赋值接收,不管会不会进行合并优化。由于移动构造和移动赋值的实现,使得复杂函数的传值返回不再需要进行深拷贝,大大提高了传值返回的效率。因此,STL中几乎所有的容器都增加了移动构造和移动赋值。


2.5 总结

左值引用和右值引用都是通过减少拷贝来提高效率的。

  • 左值引用:
    • 左值引用传参,左值引用返回,可以直接减少拷贝。
    • 漏洞一:没有解决用右值(将亡值)进行构造、赋值、插入时的对象拷贝问题。
    • 漏洞二:没有解决局部对象传值返回的深拷贝问题。
  • 右值引用:
    • 对于内置类型和没有动态内存申请的复杂类型,移动语义没有什么意义。
    • 但是对于有动态内存申请的复杂类型,移动语义可以间接减少拷贝构造(针对左值引用的漏洞进行了补充)。
    • 补丁一:如果是右值引用传参,则构造、赋值、插入不再进行深拷贝,而是直接移动资源,提高效率。
    • 补丁二:如果有资源申请的复杂类型实现了移动构造和移动赋值,在函数中返回该类型的局部对象时,会将其资源直接移动到外部接收对象中(赋值接收需要移动两次),无需进行拷贝,提高效率。

三、完美转发

3.1 完美转发的概念

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值:

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }

//模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
template<typename T>
void PerfectForward(T&& t) //模板中的&&——万能引用
{
	Fun(t);
}

int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

运行结果:

在这里插入图片描述

为什么全都调用的是左值引用版本的Fun函数呢?

给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,并对值进行修改

  • 例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。

  • 如果不想rr1被修改,可以用const int&& rr1 去引用,const右值引用可以取地址,不可修改。

  • 可以这么理解:右值取右值引用后变为了左值,这么设计是因为要使用右值引用移动右值对象的资源,而移动资源就意味着要修改右值(矛盾),所以要将右值转为左值。

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	rr2 = 5.5; // 报错,const右值引用不能修改
	return 0;
}

那么如何在内外层函数传递参数的过程中保持参数的原生类型属性呢?这时就需要用到新语法:完美转发

//同样还是上面的代码,加入完美转发
template<typename T>
void PerfectForward(T&& t) //模板中的&&——万能引用
{
    // std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
	Fun(std::forward<T>(t));
}

再次运行:

在这里插入图片描述

注意:在多层嵌套调用时,要想在内外层函数传递参数的过程中保持参数的原生类型属性,需要在所有的传参位置进行完美转发。


3.2 完美转发的使用场景

以之前模拟实现的list和string为例:

  • 【STL】模拟实现list类模版 {深度剖析list迭代器,实现list深拷贝}_芥末虾的博客-CSDN博客

  • 【STL】模拟实现string类-CSDN博客(已实现移动构造)

下面我们实现Mylist的移动插入:

template <class T>    
struct list_node{
  T _data;                         
  list_node *_next;        
  list_node *_prev;    
  //节点的构造 
  list_node(const T &val = T()) //左值引用    
    :_data(val), //调用存储类型的拷贝构造             
    _next(nullptr),    
    _prev(nullptr)    
  {}  
  //重载了右值引用版本                         
  list_node(T &&val = T()) //右值应用    
    :_data(forward<T>(val)), //完美转发3-->调用存储类型的移动构造  
    _next(nullptr),    
    _prev(nullptr)                 
  {}                         
};                             

template <class T>
class List{
  //拷贝插入 
  iterator insert(iterator pos, const T &val){
    Node *cur = pos._pnode;
    Node *prev = cur->_prev;
    Node *newnode = new Node(val);
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    return iterator(newnode);
  }
    
  //移动插入  
  iterator insert(iterator pos, T &&val){
    Node *cur = pos._pnode;
    Node *prev = cur->_prev;
    //需要在所有的传参位置进行完美转发
    Node *newnode = new Node(forward<T>(val)); //完美转发2
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    return iterator(newnode);
  }
  //拷贝插入 
  void push_back(const T &val){  
    insert(end(), val); //val是左值引用,调用拷贝插入insert             
  }                                
  //移动插入                     
  void push_back(T &&val){               
    //insert(end(), val); //右值引用将val转换为左值,所以也调用拷贝插入insert  
    insert(end(), forward<T>(val)); //完美转发1
  }    
};

测试代码:

#include <iostream>    
#include "list.hpp"    
#include "string.hpp"    
using namespace std;    
    
int main(){        
  Mylist<Mystring> ls; //在创建头结点时会进行一次移动构造(用匿名对象初始化头结点)
  cout << "----------------------------------" << endl;    
  Mystring str1 = "abcd";    
  cout << "----------------------------------" << endl;    
  ls.push_back(str1); //插入左值——拷贝构造                                   
  cout << "----------------------------------" << endl;      
  ls.push_back(Mystring("qwer"));  //插入右值——移动构造        
  cout << "----------------------------------" << endl;      
  ls.push_back("1234"); //插入右值——移动构造                 
  cout << "----------------------------------" << endl;      
}             

完美转发前:

在这里插入图片描述

由于右值引用会将右值的属性转换为左值,所以也去调用了拷贝插入insert 。因此我们需要将移动插入过程中所有涉及的函数都实现一份右值引用版本,并在所有的传参位置进行完美转发,以保持参数的右值属性。

完美转发后:

在这里插入图片描述

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

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

相关文章

《数据结构、算法与应用C++语言描述》-栈的应用-开关盒布线问题

开关盒布线问题 问题描述 在开关盒布线问题中&#xff0c;给定一个矩形布线区域&#xff0c;其外围有若干管脚。两个管脚之间通过布设一条金属线路来连接。这条金属线路称为电线&#xff0c;它被限制在矩形区域内。两条电线交叉会发生电流短路。因此&#xff0c;电线不许交叉…

乐鑫 ESP-Mesh-Lite在windows下的开发环境搭建

ESP-Mesh-Lite的开发环境由于没有官方教程&#xff0c;折腾了好几天。环境搭建主要还是参考ESP-MDF环境搭建&#xff0c;特别注意的是必须要在CMD环境下操作&#xff0c;不能用POWER SHELL。 ESP-Mesh-Lite目前支持到5.1的SDK&#xff0c;当然4.4也是可以用的。首先上Gitee或G…

AI绘画本地部署Stable Diffusion web UI

AI绘画本地部署Stable Diffusion web UI 一 、项目介绍 A browser interface based on Gradio library for Stable Diffusion. 项目地址&#xff1a;GitHub - AUTOMATIC1111/stable-diffusion-webui: Stable Diffusion web UI 这是一个可以通过直接输入描述性文字文本来生成…

keil调试的时候没问题,下载时候没反应

今天遇到这样一个问题。我下载商家的代码例程后单片机没反应&#xff0c;进入调试的时候一切正常。很奇怪&#xff0c;在网上找了教程问题解决&#xff0c;总结一下。 原因在于程序下载进去后没有按下复位键&#xff0c;导致还是之前的程序。我之前设置的是下载后自动复位运行…

滴答定时器

1.定时与计数的本质 2.滴答定时器的原理 1.向下计数&#xff0c;24位的计数器。滴答定时器属于内核。 每来一个脉冲计数值减一。当为零时。继续把重载寄存器给计数值。然后每来一个脉冲减一。 可以不停重复次操作。 控制寄存器&#xff1a;时钟的选择(bit2&#xff09;&…

【C语言】通讯录系统实现 (保姆级教程,附源码)

目录 1、通讯录系统介绍 2、代码分装 3、代码实现步骤 3.1、制作菜单menu函数以及游戏运行逻辑流程 3.2、封装人的信息PeoInfo以及通讯录Contact结构体类型 3.3、初始化通讯录InitContact函数 3.4、增加联系人AddContact函数 3.5、显示所有联系人ShowContact函数 3.6、…

对比两个数组中 每个对应位置的元素大小 返回每个对比结果组成的列表 numpy.fmin()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 对比两个数组中 每个对应位置的元素大小 返回每个对比结果组成的列表 numpy.fmin() [太阳]选择题 请问代码中最后输出正确的是&#xff1f; import numpy as np a np.array([1, 3, 6, 8]) …

面试必考精华版Leetcode1372. 二叉树中的最长交错路径

题目&#xff1a; 代码&#xff08;首刷看解析)&#xff1a; class Solution { public:int maxAns;//dir 0 left,1 rightvoid dfs(TreeNode* root,bool dir,int len){maxAnsmax(maxAns,len);if(!dir){if(root->left) dfs(root->left,1,len1);if(root->right) dfs(roo…

给奶牛做直播之四

一、前言 给奶牛做直播之三 我们讲了怎么搭建RTMP直播服务器&#xff0c;前几天在折腾Android直播推拉流一直没个结果&#xff0c;顺手整理了一下Android如何加载SO动态库&#xff0c;本想顺着这条路把JNI、NDK、JSBridge、Python调用SO这些一起整理出来 &#xff0c;这样搞就…

叶工好容6-自定义与扩展

本篇主要介绍扩展的本质以及CRD与Operator之间的区别&#xff0c;帮助大家理解相关的概念以及知道要进行扩展需要做哪些工作。 CRD&#xff08;CustomerResourceDefinition&#xff09; 自定义资源定义,代表某种自定义的配置或者独立运行的服务。 用户只定义了CRD没有任何意…

28296-2012 含镍生铁 思维导图

声明 本文是学习GB-T 28296-2012 含镍生铁. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了含镍生铁的技术要求、试验方法、检验规则以及包装、储运、标志和质量证明书。 本标准适用于炼钢、铸造或合金材料中作为镍元素添加剂的…

C++实现集群聊天服务器

C实现集群聊天服务器 JSON Json是一种轻量级的数据交换模式&#xff08;也叫做数据序列化方式&#xff09;。Json采用完全独立于编程语言的文本格式来存储和表示数据。见解和清晰的层次结构使得Json称为理想的数据交换语言。易于阅读和编写。同时也易于支持机器解析和生成&am…

28384-2012 平台式平型网版印刷机 阅读笔记

声明 本文是学习GB-T 28384-2012 平台式平型网版印刷机. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了平台式平型网版印刷机的术语和定义、型式与基本参数、要求、试验方法、检验规则、 标志、包装、运输与贮存。 本标准适用…

babel.config.js配置文件详解

文章目录 一、前言三、babel 详解四、拓展阅读 一、前言 项目开发阶段&#xff0c;使用可选链操作符 ?. 出现以下编译报错问题&#xff1a; 分析&#xff1a;由于可选链操作符 ?. 是ES2020&#xff08;即ES11&#xff09;中推出的新语法&#xff0c;允许我们不需要校验当前属…

【软考】系统集成项目管理工程师(五)项目立项管理【5分】

一、 项目建议书 1、定义 项目建议书又称为立项申请&#xff1b;建设单位向上级主管部门提交项目申请时所必须的文件&#xff0c;是对拟建项目提出的框架性的总体设想&#xff1b;是项目发展周期的初始阶段&#xff0c;是国家或上级主管部门选择项目的依据&#xff0c;也是可行…

取消excel表格中的隐藏行

Excel工作表中的行列隐藏了数据&#xff0c;如何取消隐藏行列呢&#xff1f;今天分享几个方法给大家 方法一&#xff1a; 选中隐藏的区域&#xff0c;点击右键&#xff0c;选择【取消隐藏】就可以了 方法二&#xff1a; 如果工作表中有多个地方有隐藏的话&#xff0c;还是建…

【python基础】复杂数据类型-列表类型(元组)

1.初识元组 列表非常适合用于存储在程序运行期间可能变化的数据集。列表是可以修改的。 然而,有时候需要创建一系列不可修改的元素,元组可以满足这种需求 python将不能修改的值称为不可变的,而不可变的列表被称为元组。 元组看起来犹如列表,但使用圆括号而不是方括号来标识…

苹果文件传到mac电脑用什么软件?

在数字化时代&#xff0c;文件传输已经成为我们日常生活中不可或缺的一部分。然而&#xff0c;苹果用户在将手机文件传输到电脑时&#xff0c;往往会面临一些困扰。曾经的“文件传输助手”并不能完全满足用户的需求。于是&#xff0c;很多人开始寻找更便捷的解决方案。在本文中…

Nginx 代理WebSocket

## √ map $http_upgrade $connection_upgrade {default upgrade; close; }## √ upstream websocket {server 127.0.0.1:9999 weight10 max_fails2 fail_timeout30s; }server {listen 8020;gzip on;gzip_min_length 1k;gzip_comp_level 9;gzip_types text/plain application/…

集合-set系列集合

系列文章目录 1.集合-Collection-CSDN博客​​​​​​ 2.集合-List集合-CSDN博客 3.集合-ArrayList源码分析(面试)_喜欢吃animal milk的博客-CSDN博客 4.数据结构-哈希表_喜欢吃animal milk的博客-CSDN博客 5.集合-set系列集合-CSDN博客 文章目录 目录 系列文章目录 文章目录…