【C++】vector 使用和实现

作者主页:lightqjx

本文专栏:C++

目录

一、vector的简介

二、vector的使用

[1. vector的类型重定义](#1. vector的类型重定义)

[2. 构造函数](#2. 构造函数)

[3. 迭代器的使用](#3. 迭代器的使用)

[4. 与空间相关的使用](#4. 与空间相关的使用)

[5. 增删查改](#5. 增删查改)

三、vector的模拟实现

[1. 成员变量的解释](#1. 成员变量的解释)

[2. 模拟实现vector的总代码](#2. 模拟实现vector的总代码)

四、关于vector的常见问题

[1. 迭代器失效的问题](#1. 迭代器失效的问题)

(1)容量变化导致

(2)元素删除导致

[2. 关于memcpy的拷贝问题(深浅拷贝)](#2. 关于memcpy的拷贝问题(深浅拷贝))

[3. 模拟vector的构造函数重载的问题](#3. 模拟vector的构造函数重载的问题)


前言

vector是一个可变大小数组的序列容器,在我们学习vector时常常需要我们查阅它的文档:vector的文档链接

一、vector的简介

在C++中,std::vector 是标准模板库(STL)提供的一种动态数组容器 ,它能够自动管理内存,支持高效地随机访问、动态扩容和元素增删,换句话说,vector其实就相当于我们的数据结构中使用数组实现的动态顺序表

其实,在STL中的各个容器的设计都是非常相似的,比如:都具有构造,析构,增删查改等基本操作等等。

如果要简单理解的话,也可以这样说:C++其实是将数据结构给封装起来的而已。但这种说法并不全面,在C++中其实对于容器还有更多的高级特性,比如:内存管理、算法适配、接口标准化

因为vector就像一个动态的顺序表,所以它的内容是可以存储任何数据的,比如:int,char,double等内置类型;还可以存储自定义类型,比如:struct定义的类型,class定义的类型(比如string类)等等。所以我们来看它的实现都是使用模板来实现的。如图所示:

其中的 class T 就是一个模板,而class Alloc = allocator<T> 是一个空间配置器,内存池,在现阶段我们可以不用考虑这个。就只需要知道它是通过模板实现的就行了。

所以我们使用vector时就必须要指明类型:

cpp 复制代码
//vector v1;  //错误方法

//正确方法
vector<int> v1;                                
vector<char> v2;                                
vector<string> v3;
  • v1 的类型是 std::vector<int>,即一个存储 int 的动态数组。

  • v2 的类型是 std::vector<char>,即一个存储 char的动态数组。

  • v3 的类型是 std::vector<string>,即一个存储 string的动态数组。
    这时我们既然知道了vector是通过模板实现的,那么它是不是可以代替任何类型呢?其实这是不行的。比如它是不可以完全代替string类的。其原因有两个:

  • string要求最后有\0,可以更好兼容C语言接口

  • string有很多他的专用接口函数

所以我们对于不同类型的数据使用vector时都必须要注意vector本身的使用。


二、vector的使用

1. vector的类型重定义

在认识vector时我们肯定会去查阅文档的,如果我们直接去看vector的各个构造函数可能是看不懂的,比如有许多看不懂的类型,这其实是因为vector类中通过typedef封装了其他类型。

其中的value_type就是第一个模板参数(T),allocator_type就是第二个模板参数 (Alloc)。这些类型定义会在vector的许多成员函数中使用到。

2. 构造函数

下图是vector的几个构造函数声明:

其中我们当下阶段可以不用考虑:const allocator_type& alloc = allocator_type(),后面会详细讲解。当然我们也可以了解一下(本文内容不在此)它的意思:调用构造函数时没有显式提供 alloc 参数,则使用默认构的 allocator_type 对象。

所以我们可以将这里的构造函数重新总结一下:

|---|-----------------------------------------------------------------------------------|--------------|
| 1 | vector() | 无参构造 |
| 2 | vector (size_type n, const value_type& val = value_type()) | 构造并用n个val初始化 |
| 3 | template <class InputIterator> vector (InputIterator first, InputIterator last) | 使用迭代器进行初始化构造 |
| 4 | vector (const vector& x) | 拷贝构造 |

使用实例:

cpp 复制代码
#include<vector>
using namespace std;
void test1()
{
	vector<int> v1;                                // 创建一个空的 vector
	vector<int> v2(4, 100);                        // 创建一个包含 4 个整型元素的 vector,每个元素都是 100
	vector<int> v3(v2.begin(), v2.end());          // 使用v2的迭代器范围 [first, last) 内的元素初始化 v3
	vector<int> v4(v3);                            // 使用v3拷贝构造一个v4                    
}

这里我们可以补充一下:

当然对象会在出函数作用域时自动调用析构函数的。

3. 迭代器的使用

迭代器有两种:一种是正向的迭代器,一种是反向的迭代器。正向的迭代器通过迭代器移动可以从前往后遍历vector对象。反向的迭代器通过迭代器移动可以从后往前遍历vector对象。

在vector中,迭代器其实就是指针

|--------|----------|-----------------------------------------------------------------------------------------------|
| 正向的迭代器 | begin() | 获取第一个数据位置。它有两个函数: iterator begin(); 可读可写 const_iterator begin() const; 只能读 |
| 正向的迭代器 | end() | 获取最后一个数据的下一个位置。它有两个函数: iterator end(); 可读可写 const_iterator end() const; 只能读 |
| 反向的迭代器 | rbegin() | 获取最后一个数据位置。它有两个函数: reverse_iterator rbegin(); 可读可写 const_reverse_iterator rbegin() const; 只能读 |
| 反向的迭代器 | rend(() | 获取第一个数据前一个位置。它有两个函数: reverse_iterator rend(); 可读可写 const_reverse_iterator rend() const; 只能读 |

所以迭代器都是前开后闭的区间。即:加入v是vector对象,则 [v.begin(), v.end())[v.rbegin(), v.rend())

使用示例:

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
void test2()
{
	vector<int> v1;
	// push_back是逐步向v1中尾插入数据的函数
	v1.push_back(1); 
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	
	//正向输出
	vector<int>::iterator it = v1.begin();
	while (it != v1.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;

	//反向输出
	vector<int>::reverse_iterator rit = v1.rbegin();
	while (rit != v1.rend())
	{
		cout << *rit << ' ';
		rit++;
	}
	cout << endl;
}

所以我们vector的遍历方法也就有三种,如下所示:

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
void test3()
{
	vector<int> v1;
	// push_back是逐步向v1中尾插入数据的函数
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	//遍历方法1:for循环
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << ' ';
	}
	cout << endl;

	//遍历方法2:迭代器输出
	vector<int>::iterator it = v1.begin();
	//auto it = v1.begin(); //常常是这样写的,因为比较简单
	while (it != v1.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;

	//遍历方法3:范围for
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
}

4. 与空间相关的使用

vector中有几个与空间相关的比较常用的函数:

|--------------------------------------------------------------|-------------------|
| size_type size() const; | 获取数据个数 |
| size_type capacity() const; | 获取容量大小 |
| bool empty() const; | 判断是否为空 |
| void resize(size_type n, value_type val = value_type()); | 改变vector的size |
| void reserve(size_type n); | 改变vector的capacity |

前面的size、capacity、empty都比较简单,只是获取数据即可。后面的resize、reserve需要重点了解。

需要注意的有以下几点:

  • vector 的扩容策略因编译器 STL 实现而异,如 VS 中 capacity 通常按 1.5 倍增长,而 G++ 则按 2 倍增长,故不应固化扩容倍数认知。
  • reserve(n) 可预先分配至少容纳 n 个元素的内存,可以避免频繁扩容带来的性能损耗,适用于我们预先知道元素数量的场景。
  • resize(n) 则会调整 vector 的 size 为 n,新增元素会被初始化,多余元素会被销毁,其操作比 reserve 更复杂。

使用示例:

cpp 复制代码
void test4()
{
	vector<int> v1;
	cout << "empty:" << v1.empty() << endl;

	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	cout << "empty:" << v1.empty() << endl;

	cout << "size:" << v1.size() << endl;
	cout << "capacity:" << v1.capacity() << endl;

	v1.resize(20);
	cout << "size:" << v1.size() << endl;
	cout << "capacity:" << v1.capacity() << endl;

	v1.reserve(100);
	cout << "size:" << v1.size() << endl;
	cout << "capacity:" << v1.capacity() << endl;
}

5. 增删查改

关于增删改查的常用函数有以下几种:

|---|---------------|--------------------------------|
| 增 | push_back | 尾插 |
| 增 | insert | 在position之前插入val |
| 删 | pop_back | 尾删 |
| 删 | erase | 删除position位置的数据 |
| 改 | operator[ ] | 像数组一样访问 |
| 改 | swap | 交换两个vector的数据空间 |
| 查 | find | 查找。(注意这个是算法模块实现,不是vector的成员接口) |

vector的尾插使用比较简单,

cpp 复制代码
void test5()
{
	vector<int> v1;
	v1.push_back(1); // 尾插1
	v1.push_back(2); // 尾插2
	v1.push_back(3); // 尾插3
	v1.push_back(4); // 尾插4
	v1.push_back(5); // 尾插5
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
}

我们需要注意在vector中是没有头插的。但我们可以通过insert实现,但通过insert进行插入有多个函数重载需要注意。如图所示:

第一个是在position位置插入val,第二个是在position位置插入n个val,第三个是在position位置插入一个由迭代器范围 [ first, last ) 定义的元素序列。要注意上面的参数都是迭代器参数。

cpp 复制代码
void test6()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2); 
	v1.push_back(3); 
	v1.push_back(4); 
	v1.push_back(5); 
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;

	vector<int>::iterator it=v1.begin();
	v1.insert(v1.begin()+2, 6);               //在第2个位置之前插入6
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;

	v1.insert(v1.begin()+2, 6, 10);           //在第2个位置之前插入6个10
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;

	int arr[] = { 11,12,13,14 };
	v1.insert(v1.begin()+2, arr, arr+3);       //在第2个位置之前插入arr的[arr,arr+3)的数据
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
}

进行删除操作时,尾删比较好使用,就不做解释了。

对于erase有两个函数重载,它们都是传的迭代器参数:

使用示例:

cpp 复制代码
void test7()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;

	v1.erase(v1.begin());  //删除第一个元素  
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;

	v1.erase(v1.begin(),v1.begin()+2);  //删除当前的[v1.begin(),v1.begin())的两个元素
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
}

因为vector是使用数组实现的,,所以通过运算符重载operator[ ]可以实现对vector对象的访问及修改。这里就不多说了。

对于两个vector对象,使用swap就可以高效交换两个 vector 对象的内容。

cpp 复制代码
void test8()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	vector<int> v2(3,6);
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
	for (auto e : v2)
	{
		cout << e << ' ';
	}
	cout << endl;

	v1.swap(v2);  // 交换v1和v2
	for (auto e : v1)
	{
		cout << e << ' ';
	}
	cout << endl;
	for (auto e : v2)
	{
		cout << e << ' ';
	}
	cout << endl;
}

对于查找,vector里面没有提供,所以我们可以使用算法模块find实现。使用算法模块需要包含头文件:


三、vector的模拟实现

1. 成员变量的解释

在STL源码底层里面,实现vector主要是靠使用三个迭代器实现的,也可以是称为指针(因为vector的迭代器就是指针)。即:

  • start:指向第一个元素,标记数据起始。
  • finish:指向最后一个元素的下一个位置,标记数据结束。
  • end_of_storage:指向动态分配内存的末尾的下一个位置(即内存块的尾后位置),标记容量上限。

图示如下:

要注意我们模拟时需要我们自己来定义一个命名空间域,并且由于vector是可以存储多种类型的,所以我们需要使用模板实现,所以成员变量定义就可以如下所示:

cpp 复制代码
namespace MyTest
{
	template<class T>
	class vector
	{
	public:
        //重定义可以和库里的保持一致,便于理解
		typedef T* iterator;
		typedef const T* const_iterator;
	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

2. 模拟实现vector的总代码

cpp 复制代码
namespace MyTest
{
	template<class T>
	class my_vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//迭代器
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const iterator begin() const
		{
			return _start;
		}
		const iterator end() const
		{
			return _finish;
		}

		//构造
		my_vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{	}

		my_vector(size_t n, const T& val = T()) //如果没有传val,则val会去调用它的默认构造
		{
			resize(n, val);
		}

		my_vector(int n, const T& val = T())
		{
			resize(n, val);
		}

		template<class InputIterator>
		my_vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//拷贝构造 - 深拷贝
		my_vector(const my_vector<T>& v)
		{
			_start = new T[v.capacity()];
			for (size_t i = 0; i < v.size(); i++)
			{
				_start[i] = v._start[i];
			}
			_finish = _start + v.size();
			_end_of_storage = _start + v.capacity();
		}

		void swap(my_vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

		my_vector<T>& operator=(my_vector<T> v)//传值,会调用拷贝构造
		{
			swap(v);
			return *this;
		}

		//析构
		~my_vector()
		{
			//清除空间资源
			if (_start)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
			}
		}

		//空间相关操作
		//获取数据个数
		size_t size() const
		{
			return _finish - _start;
		}
		//获取容量
		size_t capacity() const
		{
			return _end_of_storage - _start;
		}

		//[]运算符重载
		T& operator[](size_t pos)
		{
			assert(pos < size()); //断言位置合理性
			return _start[pos];
		}
		const T& operator[](size_t pos) const
		{
			assert(pos < size()); //断言位置合理性
			return _start[pos];
		}

		//扩容(重要)
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();//这里需要保存一下原数据的个数
				T* tmp = new T[n];
				if (_start)
				{
					//如果vector不为空,则要拷贝数据
					for (int i = 0; i < size(); i++)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;//释放原空间
				}
				//对该对象进行赋值
				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}//扩容完成
		}

		//改变size
		void resize(size_t n, const T val = T())
		{
			if (n < size())
			{
				//如果n比当前size小,只需要移动finish即可
				_finish = _start + n;
			}
			else
			{
				//如果比size大,则需要先扩容,再补充元素
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}

		//在pos位置插入x
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);

			//判满
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
				pos = _start + len;//防止迭代器失效(因为扩容后空间地址会改变)
			}

			//移动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}
			//插入
			*pos = x;
			_finish++;
			return pos;
		}

		//尾插
		void push_back(const T& x)
		{
			insert(end(), x);
		}

		//删除pos位置的数据
		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}
			_finish--;
			return pos;
		}

	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

四、关于vector的常见问题

1. 迭代器失效的问题

迭代器失效是指迭代器指向的对象不再有效,继续使用该迭代器会导致未定义行为。对于vector而言,迭代器失效主要发生在以下两种情况:

  1. 容量变化导致的失效:当vector的容量(capacity)发生变化时,所有现有迭代器、指针和引用都会失效。
  2. 元素删除导致的失效:当删除某个元素时,指向该元素及其之后所有元素的迭代器都会失效。

(1)容量变化导致

原因

我们在进行插入数据时往往会执行扩容操作。由于 vector 在内存中是连续存储的,所以当空间不足时,它会分配一块更大的内存(通常是当前容量的2倍),将原有元素复制过去,然后释放旧内存。这个过程之后,如果之前使用了迭代器,则所有迭代器都会指向旧的内存地址,因此就会失效。

比如:在VS下的迭代器失效

cpp 复制代码
#include<vector>
#include<iostream>
using namespace std;
int main()
{
	vector<int> v = { 1, 2, 3 };
	auto it = v.begin(); // 指向第一个元素
	
	v.push_back(4);      // 可能触发扩容
	// 此时it可能已经失效
	
	cout << *it << endl; //此时迭代器就失效了,在VS下就是报错
	return 0;
}

运行后:

避免方法

  1. 预留空间:使用 reserve() 预先分配足够空间。
  2. 谨慎使用需要扩容的操作:在需要保持迭代器有效的操作中,避免使用可能导致扩容的操作。

(2)元素删除导致

原因

当使用 erase() 删除 vector 中的元素时,会导致后续元素向前移动,被删除元素之后的所有元素的迭代器都会失效,因为元素移动后,迭代器指向的位置可能被覆盖,从而失效,继续使用这些失效的迭代器会导致不可预测的程序行为,即未定义行为。

比如:

cpp 复制代码
#include<vector>
#include<iostream>
using namespace std;
int main()
{
	vector<int> v = { 1, 2, 3 };
	auto it = v.begin(); // 指向第一个元素

	v.erase(it);  // 删除操作后,就元素移动了

	cout << *it << endl; // 此时迭代器就失效了,这是未定义行为,在VS下会报错
	return 0;
}

运行后:

解决方法

每次删除时都返回新的迭代器。

总结一下

在vector中,通过插入或删除迭代器对象后,就不能再访问这个迭代器了。因为我们认为此时迭代器是失效的,访问时结果是未定义的,我们不能这么使用。

同时我们需要知道,不同编译器对迭代器失效的处理也是不同的,比如VS是强制检查,比较严格;g++的处理就比较宽松。

2. 关于memcpy的拷贝问题(深浅拷贝)

使用memcpy而出现的问题常常出现在我们来模拟实现vector时出现。比如我们在模拟实现vector的拷贝构造函数时,如果使用memcpy来进行拷贝,即:

cpp 复制代码
my_vector(const my_vector<T>& v)
{
	_start = new T[v.capacity()];

	memcpy(_start, v._start, sizeof(T) * v.size()); // 注意这样写是有问题的

	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

这样写会出现的问题是:深浅拷贝的问题。可以说这样写的效果就是浅拷贝的效果。所以当我们写一个自定义的类型的vector时,就会出现问题,比如string。

那么我们再执行以下代码:

cpp 复制代码
int main()
{
	MyTest::my_vector<string> v;
	v.push_back("11111");
	v.push_back("22222");
	v.push_back("33333");
	v.push_back("44444");
	MyTest::my_vector<string> v2(v);
	return 0;
}

这段代码的执行效果如下:

其中v和v2是指向同一个空间的。

原因分析

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。
  2. 如果拷贝的是内置类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

所以如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是

浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。那么我们对于上面的代码,要使自定义类型也可以使用的话就可以这样写:

cpp 复制代码
my_vector(const my_vector<T>& v)
{
	_start = new T[v.capacity()];
	for (size_t i = 0; i < v.size(); i++)
	{
		_start[i] = v._start[i];
	}
	/*memcpy(_start, v._start, sizeof(T) * v.size());*/
	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

这样就是深拷贝:

3. 模拟vector的构造函数重载的问题

我们在模拟实现构造函数时有三个函数的实现:

cpp 复制代码
//构造
my_vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{	}
//第二个
my_vector(size_t n, const T& val = T()) //如果没有传val,则val会去调用它的默认构造
{
	resize(n, val);
}
//第三个
template<class InputIterator>
my_vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

其中的第二个和第三个的构造函数实现是有问题的。因为它们是很像的。当我们定义vector<int>的对象时,编译器会调用最合适它的一个函数,比如这段代码:

cpp 复制代码
MyTest::my_vector<int> v(4, 6);  // 调用的是第三个构造函数

这里会优先调用的是第三个构造函数,但第三个函数是必须要用迭代器才能正常使用的,否则就不会正常运行:

所以在我们模拟实现vector的构造函数时,就需要注意这个情况。

在vector的库里面解决这个问题的方法就是将第二个函数进行了改进,就是将第二个函数进行重载一下,即以下代码:

cpp 复制代码
my_vector(size_t n, const T& val = T()) //如果没有传val,则val会去调用它的默认构造
{
	resize(n, val);
}
my_vector(int n, const T& val = T())
{
	resize(n, val);
}

template<class InputIterator>
my_vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

感谢各位观看!希望能多多支持!

相关推荐
蓝桉~MLGT2 小时前
Python学习历程——基础语法(print打印、变量、运算)
开发语言·python·学习
风起云涌~2 小时前
【Android】kotlin.flow简介
android·开发语言·kotlin
楼田莉子3 小时前
C++学习:C++类型转换专栏
开发语言·c++·学习
David WangYang3 小时前
便宜的自制 30 MHz - 6 GHz 矢量网络分析仪
开发语言·网络·php
诗句藏于尽头3 小时前
关于七牛云OSS存储的图片数据批量下载到本地
开发语言·windows·python
楼田莉子3 小时前
C++IO流学习
开发语言·c++·windows·学习·visual studio
-雷阵雨-3 小时前
数据结构——包装类&&泛型
java·开发语言·数据结构·intellij-idea
江拥羡橙3 小时前
JavaScript异步编程:告别回调地狱,拥抱Promise async/await
开发语言·javascript·ecmascript·promise·async/await
轩情吖3 小时前
Qt常用控件之QComboBox
开发语言·c++·qt·控件·下拉框·qcombobox·桌面级开发