(C++)STL:vector的认识与使用全解析
vector的使用全解析
- vector的基本特性与理解
- 类模板
- 使用
- 默认成员函数
- 构造函数 初始化
- 析构
- 赋值重载
- 三种遍历方法
- 迭代器
- 容量操作(成员函数)
- size_type size() const 获得大小
- size_type capacity() const 获得容量
- bool empty() const; 探空
- size_type max_size() const
- void reserve (size_type n)
- void resize (size_type n, value_type val = value_type())
- 元素访问
- 修改
- insert
- erase
- swap
- clear
- push_back
- pop_back
- 实战练习
- 只出现一次的数
- 杨辉三角
本文章基于https://cplusplus.com/reference/vector/vector/理解编写
vector的基本特性与理解
根据此段文字可以总结出vector的以下特性:
- 动态可变大小:vector 是一种可以动态改变大小的序列容器,与数组不同,它能够自动管理存储空间。
- 连续存储:vector 内部使用连续的存储空间来存储元素,因此可以通过指针偏移高效地访问元素,与数组类似。
- 自动扩容:当向 vector 中添加元素时,如果当前存储空间不足,它会自动重新分配更大的存储空间。这个过程包括分配新数组并将所有元素移动到新数组中,代价较高,因此 vector 会尽量减少重新分配的频率。
- 预留额外空间:为了避免频繁扩容,vector 通常会分配比当前元素数量更多的存储空间,其实际容量可能大于当前元素数量(即 size())。
- 高效访问元素:由于存储空间是连续的,vector 提供与数组类似的高效随机访问能力。
高效的尾部插入和删除:在 vector 的尾部添加或删除元素是相对高效的,因为不需要移动其他元素。 - 内存占用较大:为了实现动态扩容和高效的尾部操作,vector 通常会占用比实际元素所需的存储空间更多的内存。
- 迭代器和引用的不稳定性:在进行插入或删除操作(尤其是扩容时),vector 的迭代器和引用可能会失效,因为元素可能被移动到新的存储位置。
- 与其他动态容器的比较:与 deque、list 和 forward_list 等其他动态序列容器相比,vector 在随机访问元素时效率更高,但在非尾部的插入或删除操作中性能较差,且迭代器和引用的稳定性不如 list 和 forward_list。
类模板
vector是一个类模板,第一个模板参数为T,第二个模板参数可缺省,有关内存池的相关操作。
vector 是类模板的原因在于它需要支持不同类型的元素存储。通过使用模板,vector 可以在编译时动态地适应不同的数据类型,而无需为每种类型单独编写代码。这大大提高了代码的复用性和灵活性。
所以根据之前类模板的知识,我们需要注意vector初始化的时候需要显示实例化,这点在构造函数的说明中也可以体现出来。
使用
有了string类的基础以及上文对vector特性的理解,vector的使用也是非常容易掌握的。
默认成员函数
构造函数 初始化
类型与参数解析:
成员类型 size_type 是vector中定义的无符号整型size_t的别名
成员类型 value_type 是容器中元素的类型,在 vector 中定义为其第一个模板参数 (T) 的别名。
成员类型 allocator_type 是容器使用的内部分配器类型,在 vector 中定义为其第二个模板参数 (Alloc) 的别名。
如果 allocator_type 是默认分配器(没有状态)的实例化,则无关紧要。
value_type() ”类型名()“是一种值初始化的语法,会将对应类型的变量初始化为默认值。对于 std::vector 的上下文,value_type 是 std::vector 的一个类型别名,表示存储在 vector 中的元素类型。因此,value_type() 表示对该类型的值初始化。
也就是说,如果一个容器类型为int,即value_type是int的别名,因为对于 int 类型,其默认值是 0
所以const value_type& val = value_type()就是const int& val = int()=0;
参数n 初始容器大小 (即构造时容器中的元素数)
参数val 用于填充容器的值。容器中的 n 个元素中的每个元素都将初始化为此值的副本。
first、last
将迭代器输入到范围中的初始和最终位置。使用的范围是 [first,last),它包括 first 和 last 之间的所有元素,包括 first 指向的元素,但不包括 last 指向的元素。
函数模板参数 InputIterator 应为输入迭代器类型,该类型指向可从中构造value_type对象的类型的元素。
对应构造函数功能
(1)默认构造
(2)构造一个包含 n 个元素的容器。每个元素都是 val 的副本。
类似于string(size_t n,char c)
(3)构造一个容器,其中包含与范围 [first,last] 一样多的元素,每个元素都按相同的顺序从该区域中的相应元素构造。
(4)拷贝构造
(5)C++11中增加了支持列表构造
vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());vector<int> v2={3,4,2,1,4};
使用练习
#include <iostream>
#include <vector>
using namespace std;void test1() {vector<int> first;//调用默认构造vector<int> second(4, 100);//调用(2)vector<int> third(second.begin() + 1, second.end() - 1);//调用迭代器构造vector<int> forth(third);//拷贝构造cout << "调试结束" << endl;
}
网站代码示例:
13-15行的示例还说明了数组构造顺序表的方式:如果有一个数组,可以通过数组的首地址和尾地址(或末尾元素的下一位置)作为迭代器来构造一个 vector,从而实现用数组构造顺序表(即 vector)。
析构
自动调用,不需要关注
赋值重载
使用非常自然
void test1() {vector<int> first;//调用默认构造vector<int> second(4, 100);//调用(2)vector<int> third(second.begin() + 1, second.end() - 1);//调用迭代器构造vector<int> forth(third);//拷贝构造first = second;//调用赋值重载cout << "调试结束" << endl;
}
三种遍历方法
由于遍历方法后面打印代码结果经常使用,且方法与string完全类似,故先讲解示范
void test2() {int arr[] = { 1,33,42,44,11 };vector<int> v(arr, arr + sizeof(arr) / sizeof(arr[0]));cout << "operator[]" << endl;for (int i = 0;i < v.size();i++) {cout << v[i]<< " ";}cout << endl;cout << "迭代器" << endl;vector<int>::iterator rit = v.begin();while (rit != v.end()) {cout << *rit << " ";rit++;}cout << endl;cout << "范围for" << endl;for (auto e : v) {cout << e<<" ";}cout << endl;
}
迭代器
与string中迭代器的分类、含义、用法完全相同,不再多作讲解
有需要可以参照此篇文章
https://blog.csdn.net/fly_jiangwb/article/details/148347116?spm=1001.2014.3001.5502
容量操作(成员函数)
基本也与string相同
size_type size() const 获得大小
size_type capacity() const 获得容量
bool empty() const; 探空
size_type max_size() const
返回最大大小
基本不会用这个
因为返回的这个最大大小也不一定能够真正达到,没什么使用意义。
void reserve (size_type n)
请求修改容量,扩容操作
实现机制相对于string有优化,对于n<capacity的情况做出了明确的“不可缩容”的界定
- 请求容量调整:该函数请求 vector 的容量至少能够容纳 n 个元素。
- 扩容行为:
如果 n 大于当前 vector 的容量,函数会触发容器重新分配存储空间,将容量增加到 n(或更大)。
不触发扩容的情况:
如果 n 小于或等于当前容量,函数不会触发重新分配,vector 的容量保持不变。 - 对 vector 大小和元素的影响:
该函数不会改变 vector 的大小(即 size()),也不会修改其元素内容。
void resize (size_type n, value_type val = value_type())
和string完全相同,实际操作取决于 n size capacity的关系
需要进行填充时,若没指定val,调用对应类型的默认值,指定了就填充指定的val
元素访问
同样与string用法完全一致,详见
https://blog.csdn.net/fly_jiangwb/article/details/148347116?spm=1001.2014.3001.5502
修改
insert
在position前插入值
支持的插入方式和string相比有所减少,但是完全够用
参数的提供思路是 插入位置+值
注意相比于string中的size_t pos,这里的position是迭代器类型
对于 C++98
(1)在position前插入一个val,并返回一个迭代器,指向新插入的元素val。
(2)在position前插入n个val
(3)在position前插入对应迭代器范围 [first,last)的元素
返回值问题
C++98只对插入单个元素有迭代器返回值,而C++11中都补充了返回值
统一说明返回的是指向新插入的第一个元素的迭代器。
代码示例
#include <iostream>
#include <vector>int main ()
{std::vector<int> myvector (3,100);//myvector此时为100,100,100std::vector<int>::iterator it;it = myvector.begin();//使迭代器it指向myvector的第一个元素it = myvector.insert ( it , 200 );//在首元素前插入200,并更新迭代器it指向新插入的元素(插入单个元素有返回值!)it指向的仍是首元素//myvector此时为200,100,100,100myvector.insert (it,2,300);//在首元素前插入300//myvector此时为300,300,200,100,100,100//在进行此类插入或删除操作时,vector 的迭代器可能会失效,因为元素可能被移动到新的存储位置。//所以这里要更新一下迭代器再继续插入it = myvector.begin();//使it再次指向了首元素std::vector<int> anothervector (2,400);myvector.insert (it+2,anothervector.begin(),anothervector.end());//在myvector的第三个元素前插入anothervector 中从begin()到end()指向的内容//myvector此时为300,300,400,400,200,100,100,100//返回的迭代器指向第三个元素(返回指向新插入的第一个元素的迭代器),但是这里没有接收这个返回值int myarray [] = { 501,502,503 };myvector.insert (myvector.begin(), myarray, myarray+3);// 在 myvector头插入myarray//myvector此时为501,502,503,300,300,400,400,200,100,100,100std::cout << "myvector contains:";for (it=myvector.begin(); it<myvector.end(); it++)std::cout << ' ' << *it;std::cout << '\n';return 0;
}
erase
1.从 vector 中移除单个元素(位置 position)或一段范围的元素值(范围 [first, last))。
2.这实际上通过移除的元素数量减少了容器的大小,并且这些元素会被销毁。
3.由于 vector 使用数组作为其底层存储,因此在 vector 的末尾之外的位置删除元素会导致容器重新定位被删除段之后的所有元素到它们的新位置。与其他类型的序列容器(如 list 或 forward_list)相比,这通常是一个效率较低的操作。
swap
标准库中的 std::swap
标准库中的 std::swap 是一个通用函数,它能够交换几乎所有类型的对象。它的实现通常依赖于对象的类型:
如果对象类型提供了自己的 swap 成员函数,std::swap 会调用该成员函数。
否则,它会使用对象的拷贝构造函数和赋值操作符来交换两个对象。
std::vector 的 swap 成员函数(此处讨论)
std::vector 的 swap 成员函数是专门为 vector 类型设计的,因此它可以利用 vector 的内部实现来优化交换过程。通常,这个函数会直接交换 vector 的内部数据指针、元素数量和容量等成员变量,而不需要复制元素。
clear
行为:
- 移除元素:用于移除容器中的所有元素,并将容器的大小减少到 0。
- 销毁元素:在移除元素的过程中,容器中的元素会被销毁(调用它们的析构函数)。
- 不改变容量:调用 clear 方法后,容器的容量(capacity)可能不会改变。这意味着容器仍然保留之前分配的内存,以便可以快速添加新元素,而不需要重新分配内存。
- 如果希望在清空容器的同时强制重新分配内存(即减少容器的容量),可以使用 swap 技巧,
让x与一个空的对象交换 即:
vector<T> ().swap(x);
注意点:
clear 操作会导致 std::vector 中的迭代器和指针失效。这是因为 clear 函数移除了容器中的所有元素,从而改变了容器的内部状态,包括元素的存储位置。
迭代器失效
当 std::vector 被 clear 后,指向已删除元素的所有迭代器都将失效。这是因为 clear 操作会将容器的大小设置为 0,并且销毁所有元素,导致原有的迭代器不再指向有效的元素。
指针和引用失效
同样,指向 std::vector 中元素的指针也会在 clear 操作后失效。这是因为 clear 操作可能会改变容器的内存布局,即使容器的容量没有改变,原有的指针也不再指向有效的数据。
如果程序中存在指向 std::vector 元素的指针或引用,在容器被 clear 或者容器的元素被移除后,这些指针或引用可能会指向无效的内存区域。为了避免悬空指针的问题,应该将这些指针显式地置为 nullptr(对于指针),而对于悬空引用,由于引用指向的对象无法更改,所以悬空引用一旦产生就难以消除,建议在会产生悬空引用的场景中不要使用引用。
#include <iostream>
#include <vector>int main() {std::vector<int> vec = {1, 2, 3, 4, 5};// 使用指针int* ptr = &vec[0];std::cout << "*ptr = " << *ptr << std::endl;// 清空 vectorvec.clear();// 将指针置为 nullptrptr = nullptr;return 0;
}
push_back
尾插入
pop_back
尾删除
实战练习
只出现一次的数
https://leetcode.cn/problems/single-number/submissions/
利用异或运算的性质
0^a=a
a^a=0
运算顺序可交换
也就是说出现两次的数必然置0
class Solution {
public:int singleNumber(vector<int>& nums) {int value = 0;for(auto e : nums){value ^= e;}return value;
}
};
杨辉三角
可视化实现
相当于二维对象数组的操作
// 涉及resize / operator[]
// 核心思想:找出杨辉三角的规律,发现每一行头尾都是1,中间第[j]个数等于上一行[j-1]+
[j]
class Solution {
public:vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for(int i = 0; i < numRows; ++i){vv[i].resize(i+1, 1);}for(int i = 2; i < numRows; ++i){for(int j = 1; j < i; ++j){vv[i][j] = vv[i-1][j] + vv[i-1][j-1];}}return vv;}
};