C++ vector的详细用法和底层原理

一、介绍

  • vector是STL容器中的一种常用的容器,和数组类似,由于其大小(size)可变,常用于数组大小不可知的情况下来替代数组。
  • vector是为了实现动态数组而产生的容器,然而向量这个名字是STL编写者取名没区好,因为在数学上的向量在几何中是矢量,两者名字相同而意义大相径庭。
  • vector也是一种顺序容器,在内存中连续排列,因此可以通过下标快速访问,连续排列也意味着大小固定,数据超过vector的预定值时vector将自动扩容。

二、vector的创建和方法

首先,使用vector时需包含头文件:

#include <vector>

创建vector

vector本质是类模板,可以存储任何类型的数据。数组在声明前需要加上数据类型,而vector则通过模板参数设定类型。

比如,声明一个int型的vector数组。

vector<int> arr1;								//一个空数组
vector<int> arr2 {1, 2, 3, 4, 5};				//包含1、2、3、4、5五个变量
vector<int> arr3(4);							//开辟4个空间,值默认为0
vector<int> arr4(5, 3);							//5个值为3的数组
vector<int> arr5(arr4);							//将arr4的所有值复制进去,和arr4一样
vector<int> arr6(arr4.begin(), arr4.end());		//将arr4的值从头开始到尾复制
vector<int> arr7(arr4.rbegin(), arr4.rend());	//将arr4的值从尾到头复制

方法

iterators(迭代器)

名字描述
begin返回指向容器中第一个元素的迭代器。
end返回指向容器最后一个元素所在位置后一个位置的迭代器
rbegin返回容器逆序的第一个元素的迭代器
rend返回容器逆序的最后一个元素的前一个位置的迭代器
cbegin和begin()功能相同,在其基础上增加了 const 属性,不能用于修改元素。
cend和end()功能相同,在其基础上增加了 const 属性,不能用于修改元素。
crbegin和rbegin()功能相同,在其基础上增加了 const 属性,不能用于修改元素。
crend和rend()功能相同,在其基础上增加了 const 属性,不能用于修改元素。

Capacity(容量)

名字描述
size返回实际元素的个数
capacity返回总共可以容纳的元素个数
max_size返回元素个数的最大值。这个值非常大,一般是2^32-1
empty判断vector是否为空,为空返回true否则false
resize改变实际元素的个数,对应于size
reserve增加容器的容量,控制vector的预留空间
shrink_to_fit减少capacity到size的大小,即减小到size的大小

Element access(元素访问)

名字描述
operator[]vector可以和数组一样用[]访问元素
atvector.at(i)等同于vector[i],访问数组下表的元素
front返回第一个元素
back返回最后一个元素
data返回指向容器中第一个元素的指针

Modifiers(修改器)

名字描述
push_back在容器的尾部插入元素
pop_back删除最后一个元素
insert插入元素
erase删除元素
clear清除容器内容,size=0,存储空间不变
swap交换两个元素的所有内容
assign用新元素替换原有内容。
emplace插入元素,和insert实现原理不同,速度更快
emplace_back在容器的尾部插入元素,和push_back不同

三、vector的具体用法

3.1 遍历vector

3.1.1 迭代器访问
  • 通过迭代器访问从begin()到end(),需要定义iterator,当然可以用auto替代。
  • begin()表示第一个元素,而end()不是最后一个元素,end()是最后一个元素的前一个位置。
//正向迭代器:vector<int>::iterator
for (vector<int>::iterator it = arr.begin(); it != arr.end(); it++)
{
    cout << *it << endl;
}
//反向迭代器:vector<int>::reverse_iterator
for (vector<int>::reverse_iterator it = arr.rbegin(); it != arr.rend(); it++)
{
    cout << *it << endl;
}
3.1.2 下标访问

和数组类似,从下标0开始遍历,而不到size的大小。

for (int i = 0; i < arr.size(); i++)
{
	cout << arr[i] << endl;
}
3.1.3 范围for循环

C++11的特性,范围for,遍历元素十分方便。

for (auto num : arr)
{
	cout << num << endl;
}

3.2 vector 容量和大小

vector<int> arr;//创建了一个名为arr的整数数据,但此时数组为空。
//将arr的大小调整为 4,这些元素的初始值取决于它们的类型(对于整数类型,初始值通常为 0)
arr.resize(4);
//预先分配可以容纳 6 个元素的内存空间,但不会改变实际大小。此时的大小仍然是 4,在不重新分配内存的情况下容纳更多的元素,达到容量 6
arr.reserve(6);
// 输出的大小和容量。在经过resize和reserve操作后,输出大小为 4,容量为 6
cout << arr.size() << " " << arr.capacity() << endl;
cout << "##########################" << endl;
//将容量调整为与当前大小匹配,即释放未使用的内存空间。
arr.shrink_to_fit();
//再次输出大小和容量。在调用shrink_to_fit后,容量会减小,但大小仍然是 4。输出大小为 4,容量为 6。
cout << arr.size() << " " << arr.capacity() << endl;

3.3 vector 常用算法

3.3.1 push_back、pop_back 和 emplace_back
  • push_back和pop_back用法简单
vector<int> arr;
for (int i = 0; i < 5; i++)
{
    arr.push_back(i);// 使用一个循环将整数 0 到 4 依次添加到向量arr中
}
for (int i = 0; i < 5; i++)
{
    arr.pop_back();//再次使用一个循环,每次调用pop_back函数从向量arr的末尾移除一个元素
}
  • emplace_back的效果和push_back一样
arr.emplace(10);

都是尾部插入元素,两者的差别在于底层实现的机制不同push_back将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。所以emplace_back的速度更快。 

3.3.2 insert 和 emplace

insert有三种用法:

  • 在指定位置插入值为val的元素。

    //在arr的头部插入值为10的元素
    vector<int> arr;
    arr.insert(arr.begin(), 10);
    
  • 在指定位置插入n个值为val的元素

    //从arr的头部开始,连续插入3个值为10的元素
    vector<int> arr;
    arr.insert(arr.begin(), 3, 10);
    
  • 在指定位置插入区间[start, end]的所有元素

    //从arr的头部开始,连续插入arrs区间[begin, end]的所有元素
    vector<int> arr;
    vector<int> arrs = { 1, 2, 3, 4, 5 };
    arr.insert(arr.begin(), arrs.begin(), arrs.end());
    

emplace和insert同为插入元素,不过emplace只能插入一个元素:

//在arr的头部插入值为10的元素
vector<int> arr;
arr.emplace(arr.begin(), 10);

insert和emplace的区别和上面类似,就是一个是拷贝和复制的过程,而另一个则是直接创建一个新元素。

3.3.3 erase

erase通过迭代器删除某个或某个范围的元素,并返回下一个元素的迭代器

vector<int> arr{1, 2, 3, 4, 5};
//删除arr开头往后偏移两个位置的元素,即arr的第三个元素,3
arr.erase(arr.begin() + 2);
//删除arr.begin()到arr.begin()+2之间的元素,删除两个;即删除arr.begin()而不到arr.begin()+2的元素
arr.erase(arr.begin(), arr.begin() + 2);
3.3.4 assign

assign修改vector,和insert操作类似,不过insert是从尾部插入,而assign则将整个vector改变。

  • 将整个vector修改为n个值为val的容器

    //将arr修改为3个值为5的vector。
    vector<int> arr = {5, 4, 3, 2, 1};
    arr.assign(3, 10);
    
  • 将整个vector修改为某个容器[start, end]范围内的元素

    //将arr修改为范围[arrs.begin, arrs.end]内的元素
    vector<int> arr = {5, 4, 3, 2, 1};
    vector<int> arrs = { 1, 2, 3, 4, 5 };
    arr.assign(arrs.begin(), arrs.end());
    
  • 用数组的值进行范围修改

    //将arr替换为数组arrs
    vector<int> arr = {5, 4, 3, 2, 1};
    int arrs[5] = { 1, 2, 3, 4, 5 };
    arr.assign(arrs, arrs + 5);
    
3.3.5 swap 和 clear

swap将两个vector进行交换。

vector<int> arr = {5, 4, 3, 2, 1};
vector<int> arrs = { 1, 2, 3, 4, 5 };
arr.swap(arrs);

clear清空整个vector,size变为0,但空间仍然存在。

arr.clear();

3.4 vector二维操作

实际上,二维vector其实就是嵌套定义vector,那么对其进行操作我们可以从嵌套的vector得到单层的vector,就可以调用其方法了。

定义
vector<vector<int>> arr;						//定义一个空的二维vector
vector<vector<int>> arr(5, vector<int>(3, 1));	//定义一个5行3列值全为1的二维vector
访问

和二维数组一样通过 [] [] 访问即可。

for (int i = 0; i < arr.size(); i++)
{
	for (int j = 0; j < arr[0].size(); j++)//注意如果arr为空不可直接arr[0]
	{
		cout << arr[i][j] << endl;
	}
}

或者用范围for:

for (auto nums : arr)
{
    for (auto num : nums)
    {
    	cout << num << endl;
    }
}
resize操作
vector<vector<int>> arr;
arr.resize(5);
for (auto num : arr)
{
    num.resize(3);
}

3.4 vector二维操作

实际上,二维vector其实就是嵌套定义vector,那么对其进行操作我们可以从嵌套的vector得到单层的vector,就可以调用其方法了。

定义
vector<vector<int>> arr;						//定义一个空的二维vector
vector<vector<int>> arr(5, vector<int>(3, 1));	//定义一个5行3列值全为1的二维vector
访问

和二维数组一样通过 [] [] 访问即可。

for (int i = 0; i < arr.size(); i++)
{
	for (int j = 0; j < arr[0].size(); j++)//注意如果arr为空不可直接arr[0]
	{
		cout << arr[i][j] << endl;
	}
}

或者用范围for:

for (auto nums : arr)
{
    for (auto num : nums)
    {
    	cout << num << endl;
    }
}
resize操作
vector<vector<int>> arr;
arr.resize(5);
for (auto num : arr)
{
    num.resize(3);
}

四、vector扩容原理

前面我们提到,vector作为容器有着动态数组的功能,当加入的数据大于vector容量(capacity)时会自动扩容,系统会自动申请一片更大的空间,把原来的数据拷贝过去,释放原来的内存空间。

看以下一段代码:

vector<int> arr;
for (int i = 0; i < 20; i++)
{
    arr.push_back(i);
    cout << arr.size() << " " << arr.capacity() << endl;
}

在这里插入图片描述

在VS中运行以上代码测试扩容,发现:

  • 初始时capacity和size都是零;
  • 开始capacity和size大小一致,在size=5时,capacity从4 -> 6,即发生了扩容:4 * 1.5 = 6,以1.5倍开始扩容。同样,在9、13、19时均是以1.5倍的方式扩容,向下取整。
  • 其实,在capacity等于size时,下一次插入操作时vector就以1.5倍开始扩容。开始时,0 * 1.5 = 1(需要), 1 * 1.5 = 2(此时需要),2 * 1.5 = 3, 3 * 1.5 = 4,4 * 1.5 = 6,6 * 1.5 = 9 。。。

可以看到,理论上每次都是1.5扩容,但是遇到一些特殊情况如:0、1或者一次性插入多个元素时,也许1.5扩容就无法满足了。其实很简单,按照我们自己的思路,这无非是程序健壮性的体现,加一句判断语句即可。

看如下VS中vector扩容的源码:

size_type _Calculate_growth(const size_type _Newsize) const {
    
    const size_type _Oldcapacity = capacity();//获取当前容器的容量。
    const auto _Max              = max_size();//获取容器允许的最大大小。
    //如果当前容量加上当前容量的一半超出最大容量(1.5倍扩容),则返回最大容量_Max。这是为了确保在扩容过程中不会超出容器允许的最大大小限制。
    if (_Oldcapacity > _Max - _Oldcapacity / 2) {
        return _Max; 
    }
	//采取当前容量加上当前容量扩容(1.5倍扩容)
    const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;
	//扩容后仍然小于新加入元素后的大小,以新加入元素后的大小为准,确保容器有足够的空间容纳新元素
    if (_Geometric < _Newsize) {
        return _Newsize; 
    }
    return _Geometric;
}

由此可见,确实是以1.5倍扩容,并且还有需要判断:是否超过max_size,以及是否小于newsize。

而其实,扩容时在插入时元素需要进行判断的,所以在vector的方法如:push_back、insert中都有用到扩容。

五、查找排序

std::find 和 std::sort 函数来执行查找和排序操作。

std::find 函数用于在向量中查找特定值,并返回指向找到的元素的迭代器,如果未找到,则返回尾后迭代器。

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto it = std::find(vec.begin(), vec.end(), 3);
    if (it != vec.end()) {
        std::cout << "Found element: " << *it << std::endl; // Found element: 3
    } else {
        std::cout << "Element not found" << std::endl;
    }
    return 0;
}

std::sort 函数用于对向量中的元素进行排序,默认是升序排序。
Demo1:自定排序规则

struct MyStruct {
    int id;
    std::string name;
};

bool compareByName(const MyStruct& a, const MyStruct& b) {
    return a.name < b.name;
}

int main() {
    std::vector<MyStruct> vec = {
        {1, "John"},
        {2, "Alice"},
        {3, "Bob"}
    };
    std::sort(vec.begin(), vec.end(), compareByName);

    // ID: 2, Name: Alice
    // ID: 3, Name: Bob
    // ID: 1, Name: John
    for (const auto& item : vec) {
        std::cout << "ID: " << item.id << ", Name: " << item.name << std::endl;
    }
    return 0;
}

Demo2:逆序排序

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> vec = {5, 2, 8, 1, 6};

    // 使用 lambda 表达式定义自定义比较函数,实现逆序排序
    std::sort(vec.begin(), vec.end(), [](int a, int b) {
        return a > b; // 逆序排序
    });

    // 打印逆序排序后的结果
    for (const auto& item : vec) {
        std::cout << item << " "; // 8 6 5 2 1
    }
    std::cout << std::endl;

    return 0;
}
 

### C++ 中 `std::vector` 的底层实现原理 #### 1. **基本概念** `std::vector` 是 C++ 标准模板库 (STL) 提供的一种动态数组容器,它允许在运行时动态调整大小并支持高效的随机访问操作[^2]。尽管其表现类似于普通的数组,但在底层实现了更复杂的逻辑来管理内存。 --- #### 2. **数据结构剖析** `std::vector` 的核心是一个连续的内存块,通常由三个指针组成: - `_M_start`: 指向当前存储区域的第一个元素。 - `_M_finish`: 指向已使用的最后一个元素之后的位置(即有效范围之外的一个位置)。 - `_M_end_of_storage`: 指向整个分配空间的末尾位置。 这种设计使得 `std::vector` 能够高效地扩展容量以及执行插入删除操作[^3]。 以下是简化版的伪代码表示: ```cpp template<typename T> class vector { private: T* _M_start; T* _M_finish; T* _M_end_of_storage; public: // 构造函数其他成员方法... }; ``` --- #### 3. **动态扩容机制** 当尝试向 `std::vector` 插入新元素而现有容量不足时,会触发重新分配过程。具体步骤如下: 1. 分配一块更大的连续内存区域,通常是原容量的两倍(某些 STL 实现可能采用其他增长策略)。 2. 将原有数据复制或移动到新的内存地址。 3. 释放旧的内存资源。 4. 更新 `_M_start`, `_M_finish`, `_M_end_of_storage` 指针指向新内存区域。 这种方法虽然可能导致额外的时间开销,但由于每次扩容量成倍增加,因此摊还复杂度仍保持较低水平。 --- #### 4. **构造与析构** `std::vector` 支持多种构造方式,包括但不限于以下几种: - 默认构造:创建一个空的 `std::vector` 容器。 - 初始化列表构造:通过一组初始值构建容器。 - 复制构造:基于已有容器实例生成副本。 - 自定义分配器构造:允许用户指定特定的内存分配策略[^4]。 析构函数负责清理所有占用的资源,确保不会发生内存泄漏。 --- #### 5. **性能特点** 由于采用了连续内存布局,`std::vector` 在以下几个方面表现出色: - 随机访问效率高:可以通过索引 O(1) 时间定位任意元素。 - 追加操作较快:如果未超出预留容量,则只需简单写入即可完成新增操作。 - 缺点在于频繁插删中间位置的操作成本较高,因为这涉及大量后续元素的整体位移。 --- #### 示例代码展示 下面是一段简单的程序演示如何使用 `std::vector` 并观察其行为变化: ```cpp #include <iostream> #include <vector> int main() { std::vector<int> vec {1, 2, 3}; std::cout << "Initial capacity: " << vec.capacity() << "\n"; for(int i = 0; i < 10; ++i){ vec.push_back(i); if(vec.capacity() != static_cast<size_t>(vec.size())){ std::cout << "Capacity increased to: " << vec.capacity() << "\n"; } } return 0; } ``` 上述例子展示了随着不断追加元素,`std::vector` 如何自动调整内部缓冲区尺寸以适应更多数据项的需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值