掌握Vector容器:C++中最强大的动态数组
- 引言
- 一、vector容器概述
- 二、vector的数据结构
- 三、vector常用的API操作
- 3.1、vector构造函数
- 3.2、vector常用的赋值操作
- 3.3、vector的大小操作
- 3.4、vector存取数据操作
- 3.5、vector插入和删除操作
- 四、vector的未雨绸缪机制
- 五、巧用swap()收缩空间
- 六、vector的运用实例
- 6.1、vector的嵌套
- 6.2、使用STL算法对vector排序
- 6.3、vector存放自定义的数据
- 总结
引言
💡 作者简介:一个热爱分享高性能服务器后台开发知识的博主,目标是通过理论与代码实践的结合,让世界上看似难以掌握的技术变得易于理解与掌握。技能涵盖了多个领域,包括C/C++、Linux、Nginx、MySQL、Redis、fastdfs、kafka、Docker、TCP/IP、协程、DPDK等。
👉
🎖️ CSDN实力新星、CSDN博客专家
👉
🔔 专栏介绍:从零到c++精通的学习之路。内容包括C++基础编程、中级编程、高级编程;掌握各个知识点。
👉
🔔 专栏地址:C++从零开始到精通
👉
🔔 博客主页:https://blog.csdn.net/Long_xu
🔔 上一篇:【038】解码C++ STL:探索string容器的无限可能与鲜为人知的技巧
一、vector容器概述
vector的数据安排以及操作方式与array非常相似,两者的唯一差别在于空间的运用的灵活性。Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,是被允许的,但是一切琐碎得由自己来,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。
vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此 vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,再也不必害怕空间不足而一开始就要求一个大块头的array了。
vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率,一旦vector l旧空间满了,如果客户每新增一个元素,vector内部只是扩充一个元素的空间,实为不智,因为所谓的扩充空间(不论多大),一如刚所说,是"配置新空间-·数据移动-释放旧空间"的大工程时间成本很高,应该加入某种未雨绸缪的空间配置策略。
- v.begin():获取容器的起始迭代器(指向第一个元素)。
- v.end():获取容器的结束迭代器(指向最后一个元素的下一个元素位置)。
特点和优势:
-
动态大小:vector可以在运行时根据需要自动调整大小,无需手动管理内存。它可以根据元素的插入或删除自动扩展或收缩。
-
随机访问:通过索引可以快速随机访问vector中的元素。这使得vector非常适用于需要频繁访问、查找和修改元素的场景。
-
连续内存存储:vector的元素在内存中以连续的方式存储,这样可以提高缓存命中率,加快元素的访问速度。
-
元素的添加和删除:vector支持在尾部添加元素(push_back)和删除尾部元素(pop_back),以及在指定位置插入元素和移除元素。相对于数组,这些操作更加灵活方便。
-
内存管理:vector会自动管理内存,当元素数量超过当前容量时,会重新申请一块更大的内存,并将元素从旧内存复制到新内存中。这减少了我们手动进行内存管理的工作量。
-
可以存储任意类型:vector可以存储任何C++数据类型,包括基本类型、自定义结构体和类对象等。
使用vector时,需要包含头文件 #include <vector>
。
二、vector的数据结构
Vector 所采用的数据结构非常简单,线性连续空间,它以两个迭代器Myfirst和Mylast分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器_Myend指向整块连续内存空间的尾端。为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求大一些,以备将来可能的扩充,这边是容量的概念。换句话说,一个vector的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再有新增元素,整个vector容器就得另觅居所。
注意:所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后尚有可配置的空间),而是一块更大的内存空间,然后将原数据拷贝新空间,并释放原空间。因此,对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就都失效了。这是程序员容易犯的一个错误,务必小心。
C++中的vector容器通常使用动态数组来实现。其数据结构可简单描述为:
-
指向存储数据的动态数组的指针:vector在内部使用一个指针来引用动态分配的连续内存块,该内存块用于存储元素。
-
大小和容量:vector包含两个重要的成员变量,即元素数量(Size)和容量(Capacity)。元素数量表示当前vector中实际存储的元素个数,而容量表示vector当前分配的内存空间能容纳的元素个数。
-
元素访问:通过索引访问vector中的元素是常数时间复杂度(O(1)),因为元素在内存中是连续存储的。
-
动态扩展:当vector中的元素数量超过当前容量时,vector会重新申请更大的内存,并将旧内存中的元素复制到新内存中。扩展策略可以保证高效的插入和删除操作。
-
迭代器支持:vector提供正向迭代器,使得可以方便地遍历容器中的元素。
C++的vector是通过动态分配的连续内存块来存储元素,通过索引访问和修改元素,并根据需要自动调整内存大小以容纳更多元素。这种设计使得vector成为一个灵活、高效的动态数组容器,适用于各种常见的数据管理和操作场景。
三、vector常用的API操作
3.1、vector构造函数
C++的vector容器提供了多个构造函数来创建和初始化vector对象。以下是一些常用的vector构造函数原型:
-
默认构造函数:创建一个空的vector对象。
vector();
-
带初始大小和值的构造函数:
vector(size_type count, const T& value = T());
创建一个包含
count
个元素的vector对象,每个元素都被初始化为value
的副本。 -
带初始范围的构造函数:
template <class InputIterator> vector(InputIterator first, InputIterator last);
创建一个包含范围在
[first, last)
内的元素的vector对象。可以使用迭代器、指针或者数组来指定范围。 -
拷贝构造函数:
vector(const vector& other);
根据另一个vector对象
other
创建一个新的vector对象,包含与other
相同的元素。 -
移动构造函数:
vector(vector&& other);
根据另一个可移动的vector对象
other
创建一个新的vector对象,other
中的元素会被移动到新的vector对象中,原vector对象将处于有效但未指定状态。 -
初始化列表构造函数:
vector(std::initializer_list<T> init);
使用初始化列表初始化一个vector对象,列表中的元素被复制到vector中。
3.2、vector常用的赋值操作
-
使用等号进行赋值:
vector<T>& operator=(const vector<T>& other);
示例:
vector<int> v1; vector<int> v2 = {1, 2, 3}; v1 = v2; // 将v2中的元素赋值给v1 // 输出v1的内容 for (int num : v1) { cout << num << " "; } // 输出结果: 1 2 3
-
使用assign()函数进行赋值:
void assign(InputIt first, InputIt last); void assign(size_type count, const T& value);
示例1:
vector<int> v1; vector<int> v2 = {4, 5, 6}; v1.assign(v2.begin(), v2.end()); // 将v2中指定范围内的元素赋值给v1 // 输出v1的内容 for (int num : v1) { cout << num << " "; } // 输出结果: 4 5 6
示例2:
vector<int> v1; v1.assign(3, 100); // 将v1中的元素数量设置为3,并将每个元素的值设置为100 // 输出v1的内容 for (int num : v1) { cout << num << " "; } // 输出结果: 100 100 100
-
使用拷贝构造函数进行赋值:
vector(const vector& other);
示例:
vector<int> v2 = {7, 8, 9}; vector<int> v1(v2); // 创建一个新的vector v1,并将v2中的元素拷贝给v1 // 输出v1的内容 for (int num : v1) { cout << num << " "; } // 输出结果: 7 8 9
-
使用初始化列表进行赋值:
vector(std::initializer_list<T> init);
示例:
vector<int> v1 = {10, 11, 12}; // 使用初始化列表赋值给v1 // 输出v1的内容 for (int num : v1) { cout << num << " "; } // 输出结果: 10 11 12
其中,T代表vector中存储的元素类型。具体说明如下:
- 等号赋值操作符(=):将另一个vector的内容赋值给当前的vector。
- assign()函数:接受输入迭代器范围或元素数量与值,并用其进行赋值。
- 拷贝构造函数:使用另一个vector创建一个新的vector,并将其元素拷贝到新的vector中。
- 初始化列表构造函数:使用初始化列表创建一个新的vector,并将列表中的元素赋值给它。
3.3、vector的大小操作
在C++中,vector提供了以下几个与大小相关的函数:
-
size()函数:
size_type size() const;
示例:
vector<int> v = {1, 2, 3, 4, 5}; cout << "Size of v: " << v.size() << endl; // 输出:Size of v: 5
size()函数返回当前vector中元素的数量。
-
empty()函数:
bool empty() const;
示例:
vector<int> v = {1, 2, 3}; cout << (v.empty() ? "Vector is empty" : "Vector is not empty") << endl; // 输出:Vector is not empty
empty()函数用于检查当前vector是否为空。如果vector为空,则返回true;否则返回false。
-
resize()函数:
void resize(size_type count); void resize(size_type count, const T& value);
示例1:
vector<int> v = {1, 2, 3}; v.resize(5); // 调整vector的大小为5 cout << "Size of v after resize: " << v.size() << endl; // 输出:Size of v after resize: 5 cout << "New elements in v:"; for (int num : v) { cout << " " << num; } // 输出:New elements in v: 1 2 3 0 0
示例2:
vector<int> v = {1, 2, 3}; v.resize(5, 100); // 调整vector的大小为5,并将新增元素的值设为100 cout << "Size of v after resize: " << v.size() << endl; // 输出:Size of v after resize: 5 cout << "New elements in v:"; for (int num : v) { cout << " " << num; } // 输出:New elements in v: 1 2 3 100 100
resize()函数用于调整vector的大小。第一个版本调整vector的大小为指定的count,并在新增的位置上默认填充零值。第二个版本在新增的位置上以特定的value填充。
-
capacity()函数的原型:
size_type capacity() const;
capacity()
函数返回vector
当前所分配的存储空间的容量(即可以容纳的元素数量),而不是实际存储的元素数量。一般情况下,容量会大于或等于size()
函数返回的大小。 -
reserve()函数的原型:
void reserve(size_type new_capacity);
reserve()
函数用于修改vector
的容量,确保存储空间至少能容纳指定的元素数量new_capacity
。如果new_capacity
小于当前容量,则函数调用没有任何效果。如果new_capacity
大于当前容量,则会重新分配足够的存储空间以适应新的容量。
下面是使用示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums;
std::cout << "Initial size: " << nums.size() << std::endl; // 输出:Initial size: 0
std::cout << "Initial capacity: " << nums.capacity() << std::endl; // 输出:Initial capacity: 0
nums.reserve(10); // 修改容量为10,预留足够的空间
std::cout << "After reserve(10) - capacity: " << nums.capacity() << std::endl; // 输出:After reserve(10) - capacity: 10
nums.push_back(1);
nums.push_back(2);
nums.push_back(3);
std::cout << "After push_back - size: " << nums.size() << std::endl; // 输出:After push_back - size: 3
std::cout << "After push_back - capacity: " << nums.capacity() << std::endl; // 输出:After push_back - capacity: 10
nums.shrink_to_fit(); // 收缩存储空间以适应实际大小
std::cout << "After shrink_to_fit - capacity: " << nums.capacity() << std::endl; // 输出:After shrink_to_fit - capacity: 3
return 0;
}
在示例中,首先创建了一个空的vector
,初始大小和容量都为0。然后使用reserve()
函数将容量修改为10,预留足够的空间。接下来,使用push_back()
函数向vector
中添加元素,此时size()
逐渐增加,但容量仍保持不变。最后,使用shrink_to_fit()
函数将容量收缩到与实际大小相匹配。
3.4、vector存取数据操作
在C++中,vector
提供了几种数据存取操作函数,可以用来访问和修改容器中的元素。
-
at()
函数的原型:reference at(size_type pos); const_reference at(size_type pos) const;
at()
函数返回指定位置pos
处的元素的引用。如果pos
超出了有效范围(即大于等于size()
),则抛出std::out_of_range
异常。示例:
#include <iostream> #include <vector> int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; for (size_t i = 0; i < nums.size(); ++i) { std::cout << "Element at position " << i << ": " << nums.at(i) << std::endl; } return 0; }
输出:
Element at position 0: 1 Element at position 1: 2 Element at position 2: 3 Element at position 3: 4 Element at position 4: 5
-
operator[]
函数的原型:reference operator[](size_type pos); const_reference operator[](size_type pos) const;
operator[]
函数返回指定位置pos
处的元素的引用。与at()
函数不同的是,若pos
超出有效范围,则行为是未定义的。也就是说at
越界时会抛出异常,二[]
越界不会抛出异常。示例:
#include <iostream> #include <vector> int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; for (size_t i = 0; i < nums.size(); ++i) { std::cout << "Element at position " << i << ": " << nums[i] << std::endl; } return 0; }
输出:
Element at position 0: 1 Element at position 1: 2 Element at position 2: 3 Element at position 3: 4 Element at position 4: 5
-
front()
和back()
函数的原型:reference front(); const_reference front() const; reference back(); const_reference back() const;
front()
函数返回容器中第一个元素的引用,而back()
函数返回容器中最后一个元素的引用。示例:
#include <iostream> #include <vector> int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; std::cout << "First element: " << nums.front() << std::endl; std::cout << "Last element: " << nums.back() << std::endl; return 0; }
输出:
First element: 1 Last element: 5
-
data()
函数的原型:T* data(); const T* data() const;
data()
函数返回指向容器存储数据的指针。这个指针允许对容器中的元素进行修改。示例:
#include <iostream> #include <vector> int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; int* ptr = nums.data(); for (size_t i = 0; i < nums.size(); ++i) { std::cout << "Element at position " << i << ": " << *(ptr + i) << std::endl; } return 0; }
输出:
Element at position 0: 1 Element at position 1: 2 Element at position 2: 3
3.5、vector插入和删除操作
在C++中,vector
提供了多个用于插入和删除元素的操作函数。下面是其中一些常用函数的原型和使用示例:
-
插入操作:
-
push_back()
:将元素添加到vector
的末尾。void push_back(const T& value);
示例:
std::vector<int> nums; nums.push_back(1); nums.push_back(2);
-
insert()
:在指定位置插入一个或多个元素。iterator insert(iterator position, const T& value); // 在指定位置插入单个元素 void insert(iterator position, size_type n, const T& value); // 在指定位置插入多个相同元素 template <class InputIterator> void insert(iterator position, InputIterator first, InputIterator last); // 在指定位置插入一个范围内的元素
示例:
std::vector<int> nums = {1, 2, 3}; auto it = nums.begin() + 1; // 在第二个位置插入元素 nums.insert(it, 4); // 插入单个元素 nums.insert(it, 2, 5); // 插入多个相同元素
-
-
删除操作:
-
pop_back()
:删除vector
末尾的元素。void pop_back();
示例:
std::vector<int> nums = {1, 2, 3}; nums.pop_back(); // 删除末尾元素,vector变为 {1, 2}
-
erase()
:删除指定位置或范围内的元素。iterator erase(iterator position); // 删除单个元素 iterator erase(iterator first, iterator last); // 删除一个范围内的元素
示例:
std::vector<int> nums = {1, 2, 3, 4, 5}; auto it = nums.begin() + 2; // 删除第三个元素 nums.erase(it); nums.erase(nums.begin(), nums.begin() + 2); // 删除前两个元素,vector变为 {3, 4, 5}
-
clear()
:删除容器中所有的元素。仅清空了大小,容量还是存在的。
-
请注意,在使用这些函数时,需要确保迭代器的有效性或正确计算插入和删除位置。
四、vector的未雨绸缪机制
C++的vector是一种动态数组容器,它在内存管理上具有未雨绸缪的机制,以避免频繁的内存重新分配和数据复制操作。
当向vector添加元素时,如果当前元素数量超过了当前分配的内存空间(即容量),vector会执行以下步骤来进行未雨绸缪:
-
分配更大的内存空间:vector会根据需要自动申请一块更大的内存空间,通常是当前容量的两倍。这样可以确保将来有足够的空间来存储更多的元素。
-
复制元素:vector会将当前内存中的元素复制到新的内存空间中。这确保了数据在内存中的连续性,以便于支持随机访问和迭代器操作。
-
释放旧内存:vector会释放之前分配的较小内存空间,以避免内存泄漏。
通过执行未雨绸缪机制,vector能够有效地管理内存,避免反复的内存分配和数据复制,从而提高性能和效率。然而,如果预先知道vector可能存储的元素数量,可以使用reserve()
函数手动设置容量,以避免不必要的内存重新分配操作。
示例:
int main()
{
cout << "#########################" << endl;
vector<int> arr;
int i = 0;
int count = 0;// 统计开辟空间次数
vector<int>::iterator it;// 指向起始迭代器
cout << "空间:" << arr.capacity() << ",大小:" << arr.size() << endl;
for (i = 0; i < 1000; i++)
{
arr.push_back(i);
if (arr.begin()!=it)
{
count++;
cout << "第"<<count<<"次分配空间:" << arr.capacity() << endl;
it = arr.begin();
}
}
return 0;
}
五、巧用swap()收缩空间
resize()
只能修改大小,无法改变容量;如果要收缩容量,需要巧用swap()函数。
假设有1000个元素的容量需要收缩空间:
交换空间:
在C++中,vector
容器的大小会根据元素的增加和删除而动态改变。当删除元素后,vector
实际占用的内存空间可能大于容器当前的大小。如果想要减少内存的占用,可以使用 swap()
函数来进行收缩空间的操作。
swap()
函数通过交换容器内部的数据来实现收缩空间的效果。具体步骤如下:
- 使用
vector
容器的成员函数resize()
将容器的大小调整为实际需要的大小。 - 创建一个临时的空
vector
容器。 - 使用
swap()
函数将空vector
和原vector
进行交换。 - 临时
vector
自动销毁,释放其占用的内存空间。
以下是收缩空间的示例代码:
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
std::cout << "Before shrink: Size = " << nums.size() << " Capacity = " << nums.capacity() << std::endl;
nums.resize(nums.size()); // 调整容器大小为实际大小
std::vector<int>(nums).swap(nums); // 使用swap()进行空间收缩
std::cout << "After shrink: Size = " << nums.size() << " Capacity = " << nums.capacity() << std::endl;
return 0;
}
运行以上代码,输出结果为:
Before shrink: Size = 5 Capacity = 8
After shrink: Size = 5 Capacity = 5
可以看到,在执行了 swap()
操作之后,vector
容器的实际占用空间被收缩,与容器内元素数量相匹配。这样可以有效减少不必要的内存占用。
六、vector的运用实例
6.1、vector的嵌套
在C++中,vector
容器支持嵌套,也就是说可以创建存储其他容器的容器。这种嵌套使用可以帮助我们组织和管理多维数据结构,提供更灵活的数据存储方式。
#include <iostream>
#include <vector>
int main() {
// 创建一个二维vector
std::vector<std::vector<int>> matrix;
// 添加第一行数据
std::vector<int> row1 = {1, 2, 3};
matrix.push_back(row1);
// 添加第二行数据
std::vector<int> row2 = {4, 5, 6};
matrix.push_back(row2);
// 访问和修改元素
std::cout << "Element at matrix[0][1]: " << matrix[0][1] << std::endl;
matrix[1][2] = 7;
// 遍历打印二维vector
for (const auto& row : matrix) {
for (const auto& element : row) {
std::cout << element << " ";
}
std::cout << std::endl;
}
return 0;
}
运行以上代码,输出结果为:
Element at matrix[0][1]: 2
1 2 3
4 5 7
可以看到,创建了一个二维的vector
容器 matrix
,并通过向其中添加一维的vector
作为行数据来构建二维结构。
通过使用下标操作符 []
,我们可以访问和修改二维vector
中的元素。例如,matrix[0][1]
表示获取第一行第二个元素的值,并将其输出到控制台。
我们也可以使用嵌套的循环遍历打印整个二维vector
的内容。
需要注意的是,在嵌套的vector
容器中,每一行的长度可以不同,即每个子vector
的大小可以不一致。这是vector
相比于内置数组的一个优势之一。
6.2、使用STL算法对vector排序
使用STL算法对vector
进行排序非常简单,可以使用std::sort
函数。std::sort
函数是C++标准模板库中的一个排序算法,它对容器中的元素进行升序排序。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 6, 1, 3, 7, 4};
// 对vector进行排序
std::sort(numbers.begin(), numbers.end());
// 打印排序后的vector
for (const auto& num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
运行以上代码,输出结果为:
1 2 3 4 5 6 7 8
在上述示例中,我们创建了一个名为numbers
的vector
,其中包含一些整数。然后,使用std::sort
函数对numbers
进行排序,将其元素按升序排列。
最后,使用循环遍历打印排序后的vector
中的元素。
需要注意的是,std::sort
函数要求可以通过迭代器访问容器的数据,因此使用numbers.begin()
和numbers.end()
作为参数传递给std::sort
,表示需要对整个vector
范围内的元素进行排序。
通过使用STL算法,可以方便地对vector
进行排序,并且也可以使用自定义的比较函数或lambda表达式来实现不同的排序需求。
6.3、vector存放自定义的数据
在C++中,vector
可以存放自定义的类对象。只需确保自定义类满足以下要求:
- 类必须有默认构造函数和复制构造函数(可以是用户定义的或者编译器生成的)。
- 类可以被复制(满足复制语义)。
使用自定义类型,当排序时需要指定比较规则。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
// 自定义类
class MyClass {
public:
int data;
MyClass(int value) : data(value) {}
};
bool comapreObj(MyClass &ob1,MyClass &ob2)
{
return ob1.data<ob2.data;
}
int main() {
// 创建一个存放MyClass对象的vector
std::vector<MyClass> myVector;
// 添加对象到vector
myVector.push_back(MyClass(1));
myVector.push_back(MyClass(2));
myVector.push_back(MyClass(6));
myVector.push_back(MyClass(3));
// 遍历对象并打印数据
for (const auto& obj : myVector) {
std::cout << obj.data << " ";
}
std::sort(myVector.begin(),myVector.end(),comapreObj);
// 遍历对象并打印数据
for (const auto& obj : myVector) {
std::cout << obj.data << " ";
}
return 0;
}
运行以上代码,输出结果为:
1 2 6 3
1 2 3 6
需要注意的是,当将自定义类对象存放到 vector
中时,会调用复制构造函数来创建新的对象,并将其添加到容器中。如果类没有复制构造函数或者编译器生成的复制构造函数无法满足需求,可能需要手动定义复制构造函数来确保对象正确地进行复制和存储。
总结
Vector是C++标准库提供的一个非常强大的动态数组容器,可以灵活地管理元素,并且具有许多重要的特性。
-
Vector是一个动态数组容器,可以自动调整大小以适应存储元素的需求。它在内部使用连续的内存来存储元素。
-
Vector允许在尾部高效地添加和删除元素,通过使用
push_back
和pop_back
操作。 -
Vector支持随机访问元素,可像普通数组一样使用索引访问数据。使用
[]
运算符或at()
函数可以获得指定位置的元素。 -
Vector提供了很多有用的成员函数和算法,如
size()
获取元素个数、empty()
检查是否为空、clear()
清空容器等。 -
使用
resize()
函数可以改变容器的大小,使其包含指定数量的元素。 -
Vector是一个模板类,可以存储任意类型的对象,包括内置类型和自定义类。
-
Vector会自动处理内存分配和释放,不需要手动管理内存。
-
Vector的迭代器用于遍历容器中的元素,可以进行正向和反向遍历。
-
Vector可以通过拷贝构造函数和赋值运算符进行复制和赋值,使得可以方便地创建副本或对容器进行复制。
-
Vector的缺点是:在尾部添加或删除元素时效率较高,但在中间或开头位置插入或删除元素会导致数据的移动,影响性能。