经验告诉我们在C语言编程中数组是经常使用的的一种类型之一,但是在C++中却用得不多,这是因为在C++中有了更好的选择,那就是今天的主角vector
。
在C++中,vector是一个典型的类模板,它是一个非常强大且常用的容器类,vector是一种序列容器,它提供了动态数组的功能,并具备自动扩容的特性。 与C语言中的静态数组相比,vector具有更高的灵活性和安全性。
关于Vector的介绍和相关API我们可以参考:cppreference
又或者这个也行cplusplus
实现原理
vector底层使用动态数组来存储元素对象,同时使用size和capacity记录当前元素的数量和当前动态数组的容量。如果持续的push_back(emplace_back)元素,当size大于capacity时,需要开辟一块更大的动态数组,并把旧动态数组上的元素搬移到当前动态数组,然后销毁旧的动态数组。
那么问题来了,当capacity不足时,新开辟的动态数组的容量是就数组的多少倍比较合适?
这个值在不同的编译器上不是固定的。例如MSVC上是1.5,而GCC则是2。
对于Vector的常见操作的复杂度(效率)如下:
scss
随机访问 - 常数𝓞(1)。
最后插入或删除元素 - 常数𝓞(1)。
元素的插入或删除 - 与向量𝓞(n)末尾的距离呈线性关系。
使用场景
因为vector内部是基于数组实现的,因此它的特点就是查询速度快,在尾部插入速度也快。它们的时间复杂度都是常数。
vector具有快速的随机访问性能,时间复杂度为常数。因此尤其适用于需要频繁随机访问和动态调整大小的场景。 但是由于vector的内存连续性要求,当需要大量的插入和删除操作时,可能会影响性能(因为涉及到扩容和元素的拷贝移动),此时可以考虑使用list或deque等容器作为替代。
基本操作
vector内部的存储是自动处理的,并根据需要进行扩展。vector通常比静态数组占用更多的空间,因为会分配更多的内存来处理未来元素的增长。 这样,在向vector插入新的元素时避免每次都进行扩容现象。
开发者可以使用capacity()
函数查询分配的内存总量。额外的内存可以通过调用shrink_to_fit()
返还给系统,以达到节省内存的目的。
就性能而言,重新分配通常是成本高昂的操作。如果元素数量事先已知,则reserve()
函数可用于消除重新分配。
vector的遍历
既然是容器,那么使用最多的一个功能应该就是容器的遍历了,对vector的进行遍历大概有四种方式。
- 迭代器的方式遍历
c
std::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
for (auto it = vec.begin(); it != vec.end(); it++){
std::cout << *it << std::endl;
}
- 传统的size方式数组下标方式
c
std::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
for (std::vector<int>::size_type i = 0; i != vec.size(); i++)
{
std::cout << vec[i] << std::endl;
}
- C++11之后的范围for循环
这应该是使用最多的方式之一了吧。
c
std::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
for (auto const &value : vec)
{
// 因为是引用,甚至还能快速修改value的值
std::cout << value << std::endl;
}
- 使用std::for_each函数
c
std::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
std::for_each(vec.begin(),vec.end(),[](const int &value){
std::cout << value << std::endl;
});
在遍历vector容器时,尽量使用迭代器而不是索引来访问元素。使用迭代器遍历容器具有更好的可读性和灵活性,并且可以避免越界访问的风险。
构造函数
众所周知,构造函数就是用来构造一个vector的,常用的构造函数大概有一下几种,我们代码注释说话:
arduino
std::vector<int> vec; //默认无参构造
vector<int> v1{10,20,30};//指定元素构造
vector<int> v2(2, 10);//将n个elem拷贝为元素构造
vector<int> v3(v1);//拷贝另一个vector构造
vector<int> v4(v1.begin(), v1.end());//区间构造,将v1区间的begin()到end()的元素赋值给v4;
容量与大小
我们先来看一段代码:
c
std::vector<int> v(10,1); //默认无参构造
v.empty();//用来判空
std::cout << "empty:" << v.empty() << std::endl;
v.capacity();//用来获取容器中元素个数的大小
std::cout << "capacity:" << v.capacity() << std::endl;
v.size();//用来获取容器当前元素个数
std::cout << "size:" << v.size() << std::endl;
v.resize(5);//用来重新指定容器的长度
std::cout << "resize后capacity:" << v.capacity() << std::endl;
std::cout << "resize后size:" << v.size() << std::endl;
v.shrink_to_fit();
std::cout << "shrink_to_fit后capacity:" << v.capacity() << std::endl;
std::cout << "shrink_to_fit后size:" << v.size() << std::endl;
v.reserve(60);//预先分配内存
std::cout << "reserve后capacity:" << v.capacity() << std::endl;
std::cout << "reserve后size:" << v.size() << std::endl;
运行打印结果:
根据注释大家应该都容易明白每个容量相关api的使用,在这里我想说的一点是resize和reserve的区别。
resize()
函数用于改变vector的大小,即向其中增加或减少元素的数量。如果指定的大小比当前大小大,vector将会增加元素,并用默认值进行初始化;如果指定的大小比当前大小小,vector将会减少元素数量。
而reserve()
函数用于预先分配内存,但不改变vector的大小。它用于在知道vector可能需要存储大量元素时,避免因为增加元素导致不断重新分配内存,从而提高性能。
访问与赋值操作
vector可以理解为一种高级的数组,既然可以作为数组使用,那么肯定就是可以通过下标访问符号[]
进行访问了。还可以通过函数at
进行访问, 但是使用at时需要特别注意检查是否越界。
另外除了下标访问符外,我们还可以通过assign
函数进行访问与赋值,示例代码如下:
c
std::vector<int> v(10,1); //默认无参构造
v[2] = 20; // 通过下标访问符进行访问赋值
v.at(2) = 20;
for (const auto &value:v) {
std::cout << "value:" << value << std::endl;
}
std::cout << "size:" << v.size() << std::endl; // 输出10
v.assign(3,30); // 用assign来区间赋值 将3个元素30,赋值给v,之前哪些值会被清理掉
std::cout << "assign--size:" << v.size() << std::endl; // 输出3
for (const auto &value:v) {
// 输出三个30
std::cout << "assign--value:" << value << std::endl;
}
需要注意的是使用assign时会将原来的元素清空,务必谨慎。
删除与插入
对于元素的添加我们可以使用push_back
、 insert
、 emplace_back
等函数。那么经典的问题又来了同样时在尾部添加元素,push_back
、emplace_back
的区别是什么呢?
一句话来说就是emplace_back
比push_back
更高效智能,能用emplace_back
就别用push_back
。这主要和类的几个构造函数有关,感兴趣的同学们可以尝试对比下 emplace_back
比push_back
都调用了类的哪些函数即可。
对于元素的删除可以使用函数pop_back
、 erase
等函数。
需要注意的一点是std::remove
并不是用来删除元素的,而是用来移动元素的,不要被字面意思所搞懵逼了...
示例代码:
c
int main() {
std::vector<int> v(6,2); //默认无参构造
v.push_back(30); //
v.insert(v.end(),40);
v.emplace_back(50);
for (const auto &value:v) {
std::cout << "value:" << value << std::endl;
}
v.pop_back(); // 删除最后一个
for (const auto &value:v) {
std::cout << "pop_back--value:" << value << std::endl;
}
// 删除特定位置
v.erase(v.begin());
// 区间删除
v.erase(v.begin(),v.begin() + 2);
// 清空
v.clear();
return 0;
}
在对vector进行插入和删除操作时,会导致迭代器失效,因此在使用迭代器进行遍历时,需要谨慎注意插入和删除操作,以免导致迭代器失效。
以下是一些可能导致迭代器失效的函数:
markdown
1. swap、std::swap
2. clear、operator=、assign
3. reserve、shrink_to_fit resize
4. erase push_back、emplace_back
5. insert、emplace pop_back
清空vector
面试时面试官很喜欢问的一个问题:如何快速的清空vector容器并释放vector容器所占用的内存?
常见的有两种方法,第一种,使用clear方法清空所有元素。然后使用shrink_to_fit方法把capacity和size(0)对齐,达到释放内存的作用:
c
using namespace std;
int main() {
std::vector<int> v(10,1); //默认无参构造
v[2] = 20; // 通过下标访问符进行访问赋值
for (const auto &value:v) {
std::cout << "value:" << value << std::endl;
}
std::cout << "capacity:" << v.capacity() << std::endl; // 输出10
std::cout << "size:" << v.size() << std::endl; // 输出10
v.clear();
v.shrink_to_fit();
std::cout << "shrink_to_fit--capacity:" << v.capacity() << std::endl; // 输出0
std::cout << "shrink_to_fit--size:" << v.size() << std::endl; // 输出0
return 0;
}
第二种,就是使用swap方法;
c
using namespace std;
int main() {
std::vector<int> v(10,1); //默认无参构造
v[2] = 20; // 通过下标访问符进行访问赋值
for (const auto &value:v) {
std::cout << "value:" << value << std::endl;
}
std::cout << "capacity:" << v.capacity() << std::endl; // 输出10
std::cout << "size:" << v.size() << std::endl; // 输出10
vector<int>().swap(v);
std::cout << "swap--capacity:" << v.capacity() << std::endl; // 输出0
std::cout << "swap--size:" << v.size() << std::endl; // 输出0
return 0;
}
笔者更加喜欢第一种,因为可读性更强,也没必要引入新的vector。
vector与bool
vector<bool>
的实现方式与普通的std::vector有所不同,它内部的元素并不是单独的bool值,而是被压缩成一个bit,这样可以在内存上节省空间。每个元素被当作一个位而不是一个字节存储。 这导致我们不能直接访问该元素,也无法对每个元素取地址(8个元素可能在同一个字节中存储)。所以不建议使用vector,必要时可以使用std::bitset替代。
同时std::vector返回的迭代器也不符合标准的迭代器要求,这意味着无法将其用于标准库中一些算法和一些需要完整迭代器的操作。