STL序列式容器之list的使用及实现

news2024/11/15 6:48:03

 std::list 和 std::vector 是两种不同的数据结构,std::vector 是基于数组的动态数组,而 std::list 是基于双向链表的数据结构。list适用于需要在序列中频繁执行插入和删除操作的场景。

1.list的特性

双向链表: list是一个双向链表,允许在序列的两端和中间执行高效的插入和删除操作。

不支持随机访问: 与vector不同,list不支持通过索引进行常量时间内的随机访问。要访问list中的元素,必须通过迭代器进行。

动态内存管理: list的内部实现使用节点,每个节点都包含一个元素和指向前后节点的指针。这种结构使得list在执行插入和删除操作时能够更好地管理内存。

保持迭代器有效性: list在进行插入和删除操作时,能够更好地保持迭代器的有效性。这意味着在进行这些操作后,不会导致所有迭代器失效。

高效的插入和删除操作: 由于list是双向链表,插入和删除操作在两端和中间都是常量时间的,使其成为处理这类操作的理想容器。

2.list的性能考虑

插入和删除操作: 如果主要进行频繁的插入和删除操作,并且不需要随机访问元素,list可能比vector更为高效。

随机访问: 如果需要通过索引进行随机访问元素,使用vector可能更为合适,因为它提供了常量时间的随机访问。

内存使用: 由于list使用了链表结构,可能引入一些额外的内存开销。在内存使用方面,vector可能更为紧凑。

3.C++标准库中list的基本用法

3.1 头文件

要使用list,首先需要包含相关的头文件:

#include <list>

3.2 声明list对象

std::list<int> myList;

3.3 list的构造函数

list的构造函数包括4种:

//1、构造的list中包含n个值为val的元素
list (size_type n, const value_type& val = value_type())

//2、构造空的list
list()

//3、拷贝构造函数
list (const list& x)

//4、用[first, last)区间中的元素构造list
list (InputIterator first, InputIterator last)

除了上述4种构造函数外,还可以使用数组作为迭代器区间构造链表,具体用法如下述代码所示:

//1、list的构造函数
void test01()
{
	list<int>l1;//构造一个空对象l1
	list<int>l2(4, 100);//链表l2中放入4个100
	list<int>l3(l2.begin(), l2.end());//用l2的[begin(),end() )左开右闭的区间构造l3
	list<int>l4(l3);//使用l3拷贝构造l4

	//使用数组作为迭代器区间构造链表l5
	int array[] = { 12,3,66,88 };
	list<int>l5(array, array + sizeof(array) / sizeof(int));

	//列表格式初始化C++11
	list<int>l6{ 2,4,6,8,10,12 };

	//用迭代器方式打印l5中的元素
	list<int>::iterator it = l5.begin();
	while (it != l5.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//范围for遍历--C++11
	for (auto e : l5)
	{
		cout << e << " ";
	}
	cout << endl;
}

3.3 list迭代器的使用

这里介绍list的正向迭代器、反向迭代器和const迭代器的使用。

//正向迭代器,对迭代器执行++操作,迭代器向后移动
iterator begin();//返回第一个元素的迭代器
iterator end();//返回最后一个元素下一个位置的迭代器

//反向迭代器,对迭代器执行++操作,迭代器向前移动
reverse_iterator rbegin();//返回第一个元素的reverse_iterator,即end位置
reverse_iterator rend();//,返回最后一个元素下一个位置的reverse_iterator,即begin位置

//const迭代器
const_iterator begin() const;//const对象只能调用const迭代器
 

具体用法如下:

//2、list的迭代器
//注意:链表的遍历只能用迭代器和范围for
void PrintList(const list<int>& l)
{
	list<int>::const_iterator it = l.begin();
	while (it != l.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

void test02()
{
	//1、使用数组作为迭代器区间构造链表l
	int array[] = { 12,33,45,67 };
	list<int>l(array, array+sizeof(array) / sizeof(array[0]));

	//2、使用正向迭代器正向遍历链表list中的元素
	//list<int>::iterator it = l.begin(); //c++98中的语法
	auto it = l.begin();//c++11之后的推荐写法
	while (it != l.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//3、使用反向迭代器逆向打印l中的数据
	//list<int>::reverse_iterator rit = l.rbegin();
	auto rit = l.rbegin();
	while (rit != l.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

3.4 list的插入和删除操作

list的插入和删除操作包括:尾插push_back、尾删pop_back、头插push_front、头删pop_front四个操作。

//1、尾插
void push_back (const value_type& val);

//2、尾删
void pop_back();

//3、头插
void push_front (const value_type& val);

//4、头删
void pop_front();

具体用法如下代码所示:

//3、list的插入和删除
//push_back/pop_back/push_front/pop_front
void test03()
{
	int array[] = { 12,13,14 };
	list<int>l(array, array + sizeof(array) / sizeof(array[0]));
	PrintList(l);
	
	//在list的尾部插入55,头部插入11
	l.push_back(55);
	l.push_front(11);
	PrintList(l);

	//删除list的尾结点和头结点
	l.pop_back();
	l.pop_front();
	PrintList(l);
}

3.5 在指定位置插入和删除结点

在指定位置插入和删除结点包括insert和erase两个操作。

//1、在position位置前插入值val的结点
iterator insert (iterator position, const value_type& val);

//2、在position位置前插入n个值为val的结点
void insert (iterator position, size_type n, const value_type& val);

//3、在position位置前插入[first,last)区间中的元素
template <class InputIterator>    
void insert (iterator position, InputIterator first, InputIterator last);

//4、删除position位置上的元素
iterator erase (iterator position);

//5、删除list中[first,last)区间中的元素
iterator erase (iterator first, iterator last);

具体使用案例如以下代码所示:

//4、在指定位置插入和删除结点
//insert/erase
void test04()
{
	int array[] = { 1,2,3 };
	list<int>l(array, array + sizeof(array) / sizeof(array[0]));

	//获取list中第二个结点
	auto pos = ++l.begin();
	cout << *pos << endl;

	//在pos前插入值为4的元素
	l.insert(pos,4);
	PrintList(l);

	//在pos前插入6个值为8的元素
	l.insert(pos, 6, 8);
	PrintList(l);

	//在pos前插入[v.begin(),v.end() )区间中的元素
	vector<int>v{ 10,11,12 };
	l.insert(pos, v.begin(), v.end());
	PrintList(l);

	//删除pos位置上的元素
	l.erase(pos);
	PrintList(l);

	//删除list中[begin(),end() )区间中的元素,这里即指删除list中的所有元素
	l.erase(l.begin(), l.end());
	PrintList(l);
}

3.6 list中size、swap、clear、empty的用法

//1、返回list中有效结点的个数
size_type size() const;

//2、交换两个list中的元素
void swap (list& x);

//3、清空list中的有效元素
void clear();

//4、检测list是否为空,是返回true,否则返回false
bool empty() const;

使用案例如以下代码所示:

//5、
//size:返回list中有效结点的个数
//swap:交换两个list中的元素
//clear:清空list中的有效元素
//empty:检测list是否为空,是返回true,否则返回false
void test05()
{
	int array[] = { 11,12,13,14,15 };
	list<int>l1(array, array + sizeof(array) / sizeof(array[0]));
	PrintList(l1);

	//交换l1和l2中的元素
	list<int>l2(2, 100);
	l1.swap(l2);
	PrintList(l1);
	PrintList(l2);

	//使用clear函数将l2中的元素清空
	//使用size函数返回l1和l2中有效结点的个数
	l2.clear();
	cout << l2.size() << endl;
	cout << l1.size() << endl;

	//使用empty函数返回链表是否为空
	list<int>l3;
	int sum(0);

	for (int i = 0; i <=10; ++i)
		l3.push_back(i);

	while (!l3.empty())
	{
		sum += l3.front();
		l3.pop_front();
	}
	cout << "total:" << sum << endl;
}

3.7 front和back的用法

//1、返回list的第一个结点中值的引用
reference front();
const_reference front() const;

//2、返回list的最后一个结点中值的引用
reference back();
const_reference back() const;

具体使用案例如下:

//6、front:返回list的第一个结点中值的引用
//   back:返回list的最后一个结点中值的引用
void test06()
{
	//front
	list<int>mylist;

	mylist.push_back(77);
	mylist.push_back(22);

	mylist.front() -= mylist.back();
	cout << "mylist.front() is now " << mylist.front() << endl;

	//back
	mylist.push_back(10);

	while (mylist.back() != 0)
	{
		mylist.push_back(mylist.back() - 1);
	}

	cout << "mylist contains: ";
	for (list<int>::iterator it = mylist.begin(); it != mylist.end(); ++it)
		cout << *it <<" ";
	cout << endl;
}

list的基本用法可参考:list的基本用法

4.list的模拟实现

4.1 list与vector的区别

如前文所述,list的模拟实现与vector相比,略复杂一点:

(1)list节点是一个结构体,包含数据域和指针域,将节点的相关操作放入节点类进行处理,逻辑更清晰;

(2)vector的元素在空间上是连续分布的,迭代器++就能指向下一个元素,但list的迭代器不行,它的每个元素在空间上都不连续,要访问下一个节点必须找到当前节点的next指针,因此list的迭代器必须重写。

4.2 list的具体框架

4.2 list节点类的实现

由上文中list的具体框架可知,list本身和list的节点是不同的结构,两者进行了分开设计,以下是list的节点(node)结构:

template<class T>
struct __list_node
{
	__list_node<T>* _next;//指向下一个结点的指针
	__list_node<T>* _prev;//指向上一个结点的指针
	T _data;//数据

	//构造函数
	__list_node(const T& val = T())
		:_next(nullptr)
		, _prev(nullptr)
		, _data(val)
	{}
};

4.3 list的迭代器

如前文所述,list不像vector一样以普通指针作为迭代器,因为其节点不保证在储存空间中连续存在。list迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取等操作。list的递增、递减、取值、成员存取操作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员。由于节点的指针原生行为不满足迭代器定义,这里的迭代器通过类来封装节点的指针重载运算符,如operator*、operator->、operator++等。

迭代器分为普通迭代器和const迭代器,对于__list_iterator类要实现两个版本,一个是普通的iterator,另一个是const版本的const_iterator。区别在于:对于两个类中的部分函数有普通函数和const函数之分(如begin( )和end( )),其他并无区别。因为这两个类的大部分代码相似,会造成代码冗余,如何解决代码冗余的问题呢?

对于T&,类模板实例化出两个类,一个是T&类,一个是const T&类,同理,T*也一样。使用类模板就会实例化出来两个类,一个是普通的不带const的T,T&, T*,另一个是带const的T,const T&, const T*,其中Ref是引用,Ptr是指针,该类模板实例化了以下这两个类模板:

template<class T,class Ref,class Ptr>
__list_iterator<T,T&,T*>  对应的是一个普通的迭代器
__list_iterator<T, const T&, const T*> 对应的是一个const迭代器 const_iterator

4.3.1 __list_iterator类

迭代器操作list的节点,需要一个指向链表节点的指针。

//list迭代器
//__list_iterator<T,T&,T*>  对应的是一个普通的迭代器
//__list_iterator<T, const T&, const T*> 对应的是一个const迭代器 const_iterator
template<class T,class Ref,class Ptr>
struct __list_iterator
{
	typedef __list_node<T> node;
	typedef __list_iterator<T,Ref, Ptr> Self;
	node* _node;//指向链表节点的指针
}

4.3.2 迭代器类的构造函数

构造函数初始化指向节点的指针。

__list_iterator(node* node)
	:_node(node)
{}

4.3.3 operator*运算符重载

//*it 解引用
Ref operator*() 
{
	return _node->_data;
}

4.3.4 operator-> 运算符重载

//it->
Ptr operator->()
{
	return &_node->_data;
}

4.3.5 前后置++、--

//++it  迭代器前置++,返回++后的迭代器
Self operator++()
{
	_node = _node->_next;
	return *this;
}

//it++ 后置++返回的是++之前的值
Self operator++(int)
{
	Self tmp(*this);
	//_node = _node->_next;
	++(*this);

	return tmp;
}

//--it  迭代器前置--,返回--后的迭代器
Self operator--()
{
	_node = _node->_prev;
	return *this;
}

//it-- 后置--返回的是--之前的值
Self operator--(int)
{
	Self tmp(*this);
	//_node = _node->_prev;
	--(*this);

	return tmp;
}

4.3.6 operator==、operator!=重载

//it!=end() 当前迭代器it与迭代器end()比较
bool operator!=(const Self& it)
{
	return _node != it._node;
}

//it==end() 当前迭代器it与迭代器end()比较
bool operator==(const Self& it)
{
	return _node == it._node;
}

4.3.7 list迭代器失效

4.4 list类的实现

list的成员需要一个头节点,并通过迭代器访问其他节点元素。

template<class T>
class MyList
{
	typedef __list_node<T> node;

public:
	//普通迭代器
	typedef __list_iterator<T, T&, T*> iterator;
	
	//const_iterator迭代器的实现
	typedef __list_iterator<T, const T&, const T*> const_iterator;

private:
    node* _head;
};

4.4.1 迭代器

//迭代器
//begin()是双向循环链表头结点下一个位置的节点
iterator begin()
{
	return iterator(_head->_next);
}

//end()是链表最后一个节点的下一个位置,即头节点的位置
iterator end()
{
	return iterator(_head);
}

//const迭代器
const_iterator begin()const
{
	return const_iterator(_head->_next);
}

//const迭代器
const_iterator end()const
{
	return const_iterator(_head);
}

4.4.1 构造函数

//1、带头双向循环链表,构造函数
MyList()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}

4.4.2 拷贝构造函数

//3、拷贝构造 lt2(lt1)
MyList(const MyList<T>& lt)
{
	//先创建一个新的只有头结点的链表
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;

	//将链表lt各结点中的数据插入新创建的链表中
	/*const_iterator it = lt.begin();
	while (it != lt.end())
	{
		push_back(*it);
		++it;
	}*/

	//也可将以上迭代器循环换成范围for循环
	for (auto e : lt)
	{
		push_back(e);
	}
}

4.4.3 operator=赋值运算符重载

//4、赋值 lt1=lt3
//写法1
MyList<T>& operator=(const MyList<T>& lt)
{
	if (this != &lt)
	{
		for (auto e : lt)
		push_back(e);
	}

	return *this;
}

//5、写法2:赋值的常用写法 lt1=lt3
MyList<T>& operator=(MyList<T> lt)
{	
	swap(_head, lt._head);
	return *this;
}

4.4.4 clear()

clear函数只清除链表中所有节点内容,不删除头节点,如果删除头节点那么链表就不存在了,这是链表的析构函数完成的操作。

//清理链表,保留头结点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		erase(it++);
	}
}

4.4.5 erase()

移除pos所指节点,并更新指针的指向,即指向pos所指节点的下一个节点。

//4、erase() 在指定位置删除数据
iterator erase(iterator pos)
{
	//注意不能删除头结点
	assert(pos != end());

	node* cur = pos._node;
	node* curPrev = cur->_prev;
	node* curNext = cur->_next;
	delete cur;

	curPrev->_next = curNext;
	curNext->_prev = curPrev;

	return iterator(curNext);
}

4.4.6 insert()

insert函数,在pos之前插入节点newNode。

//5、insert函数,在pos之前插入结点newNode
void insert(iterator pos, const T& val)
{
	node* cur = pos._node;
	node* prev = cur->_prev;
	node* newNode = new node(val);

	prev->_next = newNode;
	newNode->_prev = prev;
	newNode->_next = cur;
	cur->_prev = newNode;
}

4.4.7 push_back()

在链表的尾部插入一个节点。

//6、尾插
void push_back(const T& val)
{
	node* tail = _head->_prev;
	node* newNode = new node(val);

	tail->_next = newNode;
	newNode->_prev = tail;

	_head->_prev = newNode;
	newNode->_next = _head;
}

也可以复用insert()函数来实现push_back()函数。

void push_back(const T& val)
{	
	//end()是链表结尾的下一个位置,即头结点的位置
	insert(end(), val);
}

4.4.8 push_front()

 在链表头部插入数据,可以直接复用insert()函数。

//7、push_front头插
void push_front(const T& val)
{
	insert(begin(), val);
}

4.4.9 pop_back()

尾删

//8、尾删
void pop_back()
{
	//两种写法
	//erase(iterator(_head->_prev));
	erase(--end());
}

4.4.10 pop_front()

头删

//9、头删
void pop_front()
{
	erase(begin());
}

4.4.11 empty()

判空

//10、判空empty()
bool empty()
{
	return begin() == end();
}

4.4.12 求链表节点的个数

//11、求链表的节点个数
size_t size()
{
	size_t count = 0;
	iterator it = begin();
	while (it != end())
	{
		++it;
		++count;
	}

	return count;
}

完整代码可参考:list的模拟实现

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

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

相关文章

R 语言学习教程,从入门到精通,R的安装与环境的配置(3)

1、R 基础语法 一门新的语言学习一般是从输出 “Hello, World!” 程序开始&#xff0c;R 语言的 “Hello, World!” 程序代码如下&#xff1a; myString <- "Hello, World!" print ( myString )以上示例将字符串 “Hello, World!” 赋值给 myString 变量&#x…

【hive】HiveSQL中两个json解析函数的使用json路径定位小工具

文章目录 1.HiveSQL中两个json解析函数1&#xff09;get_json_object2&#xff09;json_tuple 2.json中key所在层级路径定位小工具 关于json&#xff1a; https://blog.csdn.net/atwdy/article/details/124668815 1.HiveSQL中两个json解析函数 1&#xff09;get_json_object …

使用 Squid 搭建 Http 代理服务器隐藏 IP

在一些情况下&#xff0c;需要变更自己的访问 IP&#xff0c;可以通过 Squid 搭建代理服务器实现。 本文使用的是 CentOS 7.6 系统。 一、部署 Squid 安装 Squid。 yum install squid -y启动服 systemctl start squid二、访问控制 总有刁民想害郑&#xff0c;疯狂访问朕的…

C++ 最小生成树 洛谷

介绍&#xff1a; 最小生成树是个啥&#xff1f;其实就像杨志一行人押送生辰纲。抛开最后生辰纲被抢的结局不谈&#xff0c;杨志他们需要到好几个地方&#xff0c;每个地方都需要花点过路费给梁山好汉们打点。比如下面就是一张城市地图&#xff1a; 其中每两个图之间的路径长就…

【保姆级系列:锐捷模拟器的下载安装使用全套教程】

保姆级系列&#xff1a;锐捷模拟器的下载安装使用全套教程 1.介绍2.下载3.安装4.实践教程5.验证 1.介绍 锐捷目前可以通过EVE-NG来模拟自己家的路由器&#xff0c;交换机&#xff0c;防火墙。实现方式是把自己家的镜像导入到EVE-ng里面来运行。下面主要就是介绍如何下载镜像和…

【Unity】3D功能开发入门系列(三)

Unity3D功能开发入门系列&#xff08;三&#xff09; 一、运动脚本&#xff08;一&#xff09;物体的运动&#xff08;二&#xff09;相对运动&#xff08;三&#xff09;运动的方向 二、旋转脚本&#xff08;一&#xff09;物体的旋转&#xff08;二&#xff09;相对旋转&…

图像相关的基础知识【RGB和RGBA】—附python代码实现

文章目录 1、图像基础知识2、像素和通道的理解3、RGB和RGBARGB (Red, Green, Blue)RGBA (Red, Green, Blue, Alpha)应用场景 4、H,W,C5、小结 &#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#x…

aurora8b10b ip的使用(framing接口下的数据回环测试)

文章目录 一、Aurora8B/10B协议二、时钟、复位与状态指示1、时钟2、复位3、状态指示 三、数据发送、接受接口&#xff08;1&#xff09;AXI4-Stream位排序&#xff08;2&#xff09;Streaming接口&#xff08;3&#xff09;Framing接口&#xff08;帧传输接口&#xff09; 四、…

C++ | Leetcode C++题解之第319题灯泡开关

题目&#xff1a; 题解&#xff1a; class Solution { public:int bulbSwitch(int n) {return sqrt(n 0.5);} };

Python | Leetcode Python题解之第318题最大单词长度乘积

题目&#xff1a; 题解&#xff1a; class Solution:def maxProduct(self, words: List[str]) -> int:masks defaultdict(int)for word in words:mask reduce(lambda a, b: a | (1 << (ord(b) - ord(a))), word, 0)masks[mask] max(masks[mask], len(word))return…

索引:SpringCloudAlibaba分布式组件全部框架笔记

索引&#xff1a;SpringCloudAlibaba分布式组件全部框架笔记 一推荐一套分布式微服务的版本管理父工程pom模板&#xff1a;Springcloud、SpringCloudAlibaba、Springboot二SpringBoot、SpringCloud、SpringCloudAlibaba等各种组件的版本匹配图&#xff1a;三SpringBoot 3.x.x版…

字符串相关函数

文章目录 &#x1f34a;自我介绍&#x1f34a;strcpy 字符串拷贝函数&#x1f34a;strcat 字符串连接函数&#x1f34a;strlen 字符串长度计算函数&#x1f34a;strcmp 字符串比较函数 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&…

C++ | Leetcode C++题解之第318题最大单词长度乘积

题目&#xff1a; 题解&#xff1a; class Solution { public:int maxProduct(vector<string>& words) {unordered_map<int,int> map;int length words.size();for (int i 0; i < length; i) {int mask 0;string word words[i];int wordLength word.s…

[Git][基本操作]详细讲解

目录 1.创建本地仓库2.配置 Git3.添加文件1.添加文件2.提交文件3.其他 && 说明 4.删除文件5.跟踪修改文件6.版本回退7.撤销修改0.前言1.未add2.已add&#xff0c;未commit3.已add&#xff0c;已commit 1.创建本地仓库 创建⼀个Git本地仓库&#xff1a;git init运行该命…

模型 ESBI(财富四象限)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。财富自由之路的4个阶段。 1 ESBI模型的应用 1.1 一名工程师的财富自由之路 有一个名叫张伟的软件工程师&#xff0c;他在一家大型科技公司工作&#xff08;E象限&#xff09;。随着时间的推移&#…

模型量化技术综述:揭示大型语言模型压缩的前沿技术

大型语言模型&#xff08;LLMs&#xff09;通常因为体积过大而无法在消费级硬件上运行。这些模型可能包含数十亿个参数&#xff0c;通常需要配备大量显存的GPU来加速推理过程。 因此越来越多的研究致力于通过改进训练、使用适配器等方法来缩小这些模型的体积。在这一领域中&am…

electron-updater实现electron全量更新和增量更新——渲染进程交互部分

同学们可以私信我加入学习群&#xff01; 正文开始 前言更新功能所有文章汇总一、监听页面渲染完毕1.1 myApi.handleCheckPcUpdate检查更新1.2myApi.onPcUpdateProgress接收下载信息1.3myApi.onPcDownloaded监听下载完毕事件 二、立即更新三、跳过更新四、打开更新模块总结 前言…

打卡第32天------动态规划

坚持了一个月了,骑马找马,要坚持不懈呀✊ 一、动态规划理论基础 1、什么是动态规划?英文:Dynamic Programming,简称DP。 如果某一问题有很多重叠子问题,使用动态规划是最有效的。 所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有…

JVM—虚拟机类加载时机与过程

参考资料&#xff1a;深入理解Java虚拟机&#xff1a;JVM高级特性与最佳实践&#xff08;第3版&#xff09;周志明 1. 类加载的时机 一个类型从被加载到虚拟机内存开始&#xff0c;到卸载出内存为止&#xff0c;它的生命周期会经历加载、验证、准备、解析、初始化、使用、卸载…

netapp内网穿透

1. 注册netapp账号 NATAPP-内网穿透 基于ngrok的国内高速内网映射工具 2. 购买隧道&#xff0c;要求不高的话可以使用这个免费的 3.设置隧道 主要设置你想通过公网访问你的本地端口号 4.点击我的隧道&#xff0c;注意这里的authtoken&#xff0c;后面会用到 5.本地下载netap…