[C++STL] :vector的简介和使用
vector的特性与优点
C++ 中的 vector 是一种序列容器,它允许你在运行时动态地插入和删除元素。
vector 是基于数组的数据结构,但它可以自动管理内存(析构函数的好处),这意味着你不需要手动分配和释放内存。
vector 是 C++ 标准模板库(STL)的一部分,提供了灵活的接口和高效的操作。
动态大小:vector 的大小可以根据需要自动增长和缩小。
物理连续(注:有些容器只是逻辑连续,物理上并不连续):vector 中的元素在内存中是连续存储的,这使得访问元素非常快速。
支持迭代器:vector 可以被迭代,你可以使用循环(如 for 循环)来访问它的元素(也可以使用范围for循环进行遍历)
泛式实现:基于模版的好处,vector 可以存储任何类型的元素,包括内置类型、对象、指针等。
使用场景
当你需要一个可以动态增长和缩小的数组时。
当你需要频繁地在序列的末尾添加或移除元素时(O(1)复杂度)。
当你需要一个可以高效随机访问元素的容器时(物理连续,重载operator[],所以支持随机访问)。
vector常用API
常用API
#include<iostream>
#include<vector>
int main()
{
//构造方法:
std::vector<int> myVector; // 创建一个存储整数的空 vector
std::vector<int> myVector2(5); // 创建一个包含 5 个整数的 vector,每个值都为默认值(0)
std::vector<int> myVector3(5, 10); // 创建一个包含 5 个整数的 vector,每个值都为 10
std::vector<int> vec; // 默认初始化一个空的 vector
std::vector<int> vec2 = { 1, 2, 3, 4 }; // 初始化一个包含元素的 vector
//push_back函数
myVector.push_back(7); // 将整数 7 添加到 vector 的末尾
myVector.push_back(8); // 将整数 8 添加到 vector 的末尾
myVector.push_back(5); // 将整数 5 添加到 vector 的末尾
myVector.push_back(3); // 将整数 3 添加到 vector 的末尾
//operator[] 与 at 函数 ,体系了迭代性
int x = myVector[0]; // 获取第一个元素
int y = myVector.at(1); // 获取第二个元素
//注意operator[] 和 at的显著区别就是 ,at会抛出异常
//这是面向对象处理异常的方法,异常篇章会详细讲解
//size函数
int size = myVector.size(); // 获取 vector 中的元素数量
//vector支持迭代器访问
for (auto it = myVector.begin(); it != myVector.end(); ++it) {
std::cout << *it << " ";
}
//范围for循环(范围for循环本质上基于迭代器实现)
for (int element : myVector) {
std::cout << element << " ";
}
//clear函数
myVector.clear(); // 清空 vector
return 0;
}
完整的使用实例
#include <iostream>
#include <vector>
int main() {
// 创建一个空的整数向量
std::vector<int> myVector;
// 添加元素到向量中
myVector.push_back(3);
myVector.push_back(7);
myVector.push_back(11);
myVector.push_back(5);
// 访问向量中的元素并输出
std::cout << "Elements in the vector: ";
for (int element : myVector) {
std::cout << element << " ";
}
std::cout << std::endl;
// 访问向量中的第一个元素并输出
std::cout << "First element: " << myVector[0] << std::endl;
// 访问向量中的第二个元素并输出
std::cout << "Second element: " << myVector.at(1) << std::endl;
// 获取向量的大小并输出
std::cout << "Size of the vector: " << myVector.size() << std::endl;
// 删除向量中的第三个元素
myVector.erase(myVector.begin() + 2);
// 输出删除元素后的向量
std::cout << "Elements in the vector after erasing: ";
for (int element : myVector) {
std::cout << element << " ";
}
std::cout << std::endl;
// 清空向量并输出
myVector.clear();
std::cout << "Size of the vector after clearing: " << myVector.size() << std::endl;
return 0;
}
上面列举的是vector最常用的api
更多api手册在cplusplus官方网站可以找到
链接: cplusplus
vector部分实现原理分析
底层存储:vector不仅仅逻辑上是连续,物理上也是连续的
所以本质上,在构造函数中,会new出一段空间作为初始空间
在析构函数中delete 释放这些空间
增容原理 :vector是可变大的弹性数组,所以当空间满了又要插入新元素时,就要进行增容。
实现 : 开辟new 一个原大小数倍的新数组,使用这个新数组,把原数组的值赋值给新数组,然后detele新数组
拷贝构造函数和operator=赋值函数。vector的拷贝构造函数和operator=赋值函数必定是 深拷贝 ,不然就会导致两个vector实例对象指向同一个位置,导致析构函数重复释放同一个位置
其他api,有了前3点实现vector的核心思想,要实现push_back,pop_back等api就非常简单了。
注 :如果实在不理解深拷贝的原理,可以参考我的博客了解深拷贝和浅拷贝
[C++类和对象] :类的默认成员函数
#include <iostream>
using namespace std;
template<class T>
class MyVector {
public:
// 1. 构造函数
// 默认构造:初始化为空
MyVector() : _data(nullptr), _size(0), _capacity(0) {}
// 带初始大小的构造:创建n个元素的空间
MyVector(size_t n)
{
_data = new T[n]; // 物理连续空间
_size = n; // 元素个数为n
_capacity = n; // 容量为n
}
// 2. 拷贝构造函数(深拷贝)
MyVector(const MyVector<T>& other)
{
_capacity = other._capacity; // 复制容量
_size = other._size; // 复制元素个数
if (_capacity > 0)
{
_data = new T[_capacity]; // 新开辟独立空间
// 拷贝所有元素
for (size_t i = 0; i < _size; ++i)
{
_data[i] = other._data[i];
}
} else
{
_data = nullptr; // 空vector处理
}
}
// 3. 赋值运算符重载(深拷贝)
MyVector<T>& operator=(const MyVector<T>& other)
{
// 防止自赋值(如v1 = v1)
if (this != &other)
{
// 释放当前空间
delete[] _data;
// 复制容量和元素个数
_capacity = other._capacity;
_size = other._size;
if (_capacity > 0)
{
_data = new T[_capacity]; // 新开辟独立空间
// 拷贝所有元素
for (size_t i = 0; i < _size; ++i)
{
_data[i] = other._data[i];
}
}
else
{
_data = nullptr; // 空vector处理
}
}
return *this;
}
// 4. 析构函数:释放空间
~MyVector()
{
delete[] _data; // 释放连续空间
_data = nullptr; // 避免野指针
_size = 0;
_capacity = 0;
}
// 5. 增容机制:当空间不足时扩容
void reserve(size_t new_capacity)
{
if (new_capacity > _capacity) {
// 仅当新容量更大时扩容
T* new_data = new T[new_capacity]; // 开辟新空间
// 拷贝旧元素到新空间
if (_data != nullptr)
{
for (size_t i = 0; i < _size; ++i) {
new_data[i] = _data[i];
}
delete[] _data; // 释放旧空间
}
_data = new_data; // 指向新空间
_capacity = new_capacity; // 更新容量
}
}
// 6. 尾插元素
void push_back(const T& val)
{
// 若空间满了则扩容(原容量0则扩为1,否则扩为2倍)
if (_size >= _capacity)
{
size_t new_capacity = (_capacity == 0) ? 1 : _capacity * 2;
reserve(new_capacity);
}
_data[_size] = val; // 插入新元素
_size++; // 更新元素个数
}
// 7. 基本访问接口
size_t size() const { return _size; } // 获取元素个数
size_t capacity() const { return _capacity; } // 获取容量
T& operator[](size_t index) { return _data[index]; } // 访问元素
const T& operator[](size_t index) const { return _data[index]; }
private:
T* _data; // 指向物理连续的存储空间
size_t _size; // 当前元素个数
size_t _capacity; // 容量(最大可存储元素个数)
};
vector的缺点
空间浪费 :vector的空间满则扩容的机制注定了他有浪费空间的风险。取一个极端案例。vector原始容量是100个元素,但是我现在要插入101个元素,vector会扩容到200个元素的容量。这样一来就浪费了99单位空间,几乎是浪费了50%
头插和中间插入效率低 :要在头上插入,或者中间插入,后面的元素必须整体挪一个位置。复杂度为O(N)
插入一个元素复杂度就是O(N),那么插入N个复杂度就来到了恐怖的
O(N^2),这会极大降低效率。
这就是为何C++官方没有支持vector的push_front
扩容过程的性能开销大 :
vector 扩容时需要经历「开辟新空间→拷贝旧元素→释放旧空间」三个步骤。对于存储大量元素或大对象(如大型结构体、类实例)的 vector,拷贝旧元素的过程会产生巨大的性能开销(时间复杂度为 O (N))。尤其是在频繁插入元素导致多次扩容时,这种开销会被放大,可能引发程序运行时的性能波动(比如突然的卡顿)
更多推荐
所有评论(0)