文章目录
- 基本原理
- 1. 数据存储结构
- 2. 内存管理机制
- 3. 迭代器实现原理
- 4. 元素访问原理
- 5. 插入和删除元素原理
- 常见用法
- 1. 概述
- 2. 包含头文件
- 3. 定义和初始化
- 4. 常用成员函数
- 5. 迭代器
- 6. 内存管理与性能特点
- 7. 应用场景
基本原理
以下是关于 std::vector
的基本原理讲解:
1. 数据存储结构
std::vector
在内部通常是使用一段连续的内存空间来存储元素,这一点和普通的C风格数组类似。这种连续存储的方式使得它能够实现快速的随机访问,因为可以通过简单的指针运算(基于元素类型的大小)来定位到任意位置的元素,例如,对于一个存储 int
类型元素的 vector
,如果知道首地址,要访问索引为 n
的元素,在内存中就是首地址加上 n * sizeof(int)
的偏移量。
2. 内存管理机制
-
初始内存分配:
当创建一个vector
时,它并不会立即分配很大的内存空间,通常会分配一个较小的初始容量(不同的标准库实现可能初始容量不同,有些可能是0,有些可能是一个较小的固定值,比如4或者8等)。例如,定义std::vector<int> myVec;
时,它可能刚开始并没有分配实际可存储元素的内存块,只是进行了一些必要的内部状态初始化。 -
扩容机制:
随着不断向vector
中添加元素(通过push_back
等操作),当元素个数达到当前容量上限时,vector
就需要进行扩容操作以容纳更多元素。扩容过程一般涉及以下几个步骤:- 新内存分配:它会按照一定的策略分配一块更大的连续内存空间,常见的扩容倍数是1.5倍或者2倍左右(具体倍数由不同的STL实现决定)。例如,当前
vector
的容量是4,已经存储了4个元素,当再调用push_back
添加第5个元素时,如果按照2倍扩容策略,就会另外开辟一块能容纳8个元素的新内存空间。 - 元素复制或移动:接着把原来内存空间中存储的所有元素复制或者移动(如果元素类型支持移动语义,在C++11及以后通常优先采用移动,以提高效率)到新分配的内存空间中。这一步涉及对每个元素的操作,如果元素类型的拷贝构造函数或者移动构造函数开销较大,那么扩容操作的整体开销也会相应增大。
- 释放旧内存:在成功将元素迁移到新内存空间后,会释放原来的那块内存空间,以避免内存泄漏。
- 新内存分配:它会按照一定的策略分配一块更大的连续内存空间,常见的扩容倍数是1.5倍或者2倍左右(具体倍数由不同的STL实现决定)。例如,当前
-
缩容机制(可选):
有些vector
的实现提供了手动缩容或者在特定条件下自动缩容的机制,不过不是所有标准库都保证有自动缩容功能。比如,当调用vector
的clear
函数清空所有元素后,部分实现可能会自动释放多余的内存空间,将容量减小;也可以通过类似shrink_to_fit
这样的函数(如果标准库支持)来显式要求vector
释放多余内存,使其容量尽量接近当前元素个数,不过这个函数只是一个建议性的操作,具体是否能真正缩容以及缩容到何种程度还是依赖具体的实现。
3. 迭代器实现原理
vector
的迭代器本质上是一种类似指针的对象,它重载了一些必要的运算符(如 *
(解引用)、++
(前置和后置自增)、--
(前置和后置自减)、!=
(不等比较)等),使其能够像指针一样遍历 vector
中的元素。
-
普通迭代器(
begin
、end
等返回的迭代器):
其内部通常就是直接持有指向vector
中元素所在内存位置的指针。例如,begin
函数返回的迭代器指向vector
的第一个元素所在内存地址,end
函数返回的迭代器指向最后一个元素后面的内存位置(形成左闭右开区间用于遍历等操作符合STL的通用约定)。当对迭代器进行自增操作时,就是按照元素类型的大小在内存中向后移动指针到下一个元素的位置,自减操作则相反,向前移动指针。 -
反向迭代器(
rbegin
、rend
等返回的迭代器):
反向迭代器的实现相对复杂一些,它一般会对普通迭代器进行适配,内部通过一些机制来实现从后往前遍历的效果。实际上,它可以看作是对普通迭代器的一种“包装”,在自增、自减等操作的语义上进行了反转,使其能够从vector
的末尾元素开始,向开头方向依次访问元素。
4. 元素访问原理
-
下标访问(
operator[]
):
这个操作符重载的实现就是基于vector
内部元素连续存储的特性,通过简单的指针算术运算来实现快速访问。它接收一个索引值作为参数,在内部会将这个索引值与元素类型的大小相乘(例如对于int
类型就是乘以sizeof(int)
),然后加上首元素的地址(也就是vector
内部管理的那块连续内存空间的起始地址),最后返回这个计算得到的内存地址对应的元素的引用,不过它不会进行边界检查,所以如果使用不当,访问越界就会导致未定义行为。 -
at
函数访问:
at
函数和operator[]
类似也是用于访问元素,但它内部会首先进行边界检查,判断传入的索引值是否在合法的范围(即大于等于0且小于vector
的大小)内,如果越界则会抛出std::out_of_range
类型的异常,避免了因越界访问带来的潜在错误隐患,相对更加安全。
5. 插入和删除元素原理
-
末尾插入(
push_back
):
在末尾添加元素时,如果当前vector
还有剩余空间(即元素个数小于容量),那么直接在已有内存空间的末尾位置构造新元素即可,这个过程相对简单高效;但如果已经满容量需要扩容,就会触发上述的扩容流程后再添加元素。 -
中间或开头插入(
insert
):
当要在vector
的中间或者开头位置插入元素时,需要先将插入位置及之后的所有元素依次向后移动,为新插入的元素腾出空间,然后再在腾出的位置构造新元素。元素移动的过程同样涉及到对每个元素的拷贝或移动操作,所以在元素个数较多的vector
中进行这类插入操作相对来说效率较低,时间复杂度是线性的O(n)
,其中n
是vector
的元素个数。 -
删除元素(
pop_back
、erase
等):
pop_back
操作相对简单,只是销毁vector
末尾的元素(调用其析构函数,如果有的话),然后将元素个数减1即可;而erase
操作在删除指定位置的元素时,需要将该位置后面的所有元素依次向前移动来填补删除元素后留下的空位,同样涉及到多个元素的拷贝或移动,效率方面也会受vector
元素个数影响,在中间或开头位置删除元素时间复杂度为O(n)
。
总之,std::vector
通过巧妙的内存管理、迭代器设计以及对各种操作的底层实现,为开发者提供了一个方便、灵活且功能强大的动态数组容器,在了解其基本原理后能更好地掌握它的使用场景、性能特点以及避免一些因不当使用而可能出现的问题。 以下是对C++ STL(标准模板库)中vector
的详细讲解:
常见用法
1. 概述
vector
是C++ STL中非常常用的一个容器类,它提供了动态大小的数组功能,能够自动管理所存储元素的内存分配与释放,并且支持快速的随机访问。简单来说,你可以把它看作是一种可以根据需要自动扩容和缩容的数组,使用起来比普通的C风格数组更加方便、安全和灵活,广泛应用于各种需要存储和操作一组同类型元素的场景中。
2. 包含头文件
要使用vector
容器,需要在代码中包含<vector>
头文件,例如:
#include <vector>
3. 定义和初始化
- 基本定义:
定义一个vector
对象的语法形式通常为std::vector<元素类型> 变量名;
,这里的“元素类型”可以是C++中的各种基本数据类型(如int
、double
、char
等),也可以是自定义的结构体、类等类型。例如,定义一个存储整数的vector
:
std::vector<int> myVector;
- 初始化方式:
- 默认初始化:像上面那样定义后,
vector
会被默认初始化为空,即其中不包含任何元素,其大小(通过size()
成员函数获取)为0。 - 指定初始大小和初始值:可以在定义时指定
vector
的初始大小以及每个元素的初始值,语法为std::vector<元素类型> 变量名(元素个数, 初始值);
。例如,创建一个包含5个初始值都为0的整数的vector
:
- 默认初始化:像上面那样定义后,
std::vector<int> anotherVector(5, 0);
此时,anotherVector
的大小为5,并且每个元素的值都是0。
- 使用初始化列表初始化:利用花括号{}
包裹元素来初始化vector
,这种方式在C++11及以后的版本中更加常用和方便。例如:
std::vector<int> initListVector = {1, 2, 3};
这个vector
包含了3个元素,分别是1、2和3。
- 从其他vector
复制或移动初始化:可以通过已有的vector
对象来初始化一个新的vector
,实现复制或移动语义(如果适用)。例如:
std::vector<int> sourceVector = {1, 2, 3};
// 复制初始化
std::vector<int> copiedVector(sourceVector);
// 移动初始化(假设C++11及以上,利用std::move函数实现移动语义)
std::vector<int> movedVector(std::move(sourceVector));
复制初始化会创建一个新的vector
,其元素与源vector
完全相同;而移动初始化则是将源vector
的资源(如内存空间等)转移给新的vector
,源vector
在移动后通常处于一种有效但已转移资源的特殊状态(比如大小变为0等)。
4. 常用成员函数
- 添加和删除元素相关函数:
push_back()
:用于在vector
的末尾添加一个元素。例如:
std::vector<int> numbers;
numbers.push_back(5); // 在末尾添加整数5
numbers.push_back(10); // 再添加整数10,此时numbers包含两个元素,分别是5和10
- **`pop_back()`**:与`push_back()`相反,它用于删除`vector`末尾的一个元素。例如:
std::vector<int> nums = {1, 2, 3};
nums.pop_back(); // 删除末尾的元素3,此时nums包含元素1和2
- **`insert()`**:可以在指定位置插入一个或多个元素。它有多种重载形式,常见的一种是 `iterator insert(iterator position, const T& value);`,其中`position`是指向插入位置的迭代器(可以通过`begin()`、`end()`等函数获取迭代器来指定位置),`value`是要插入的元素值。例如:
std::vector<int> vec = {1, 3};
auto it = vec.begin() + 1; // 指向索引为1的位置(也就是元素3所在位置)
vec.insert(it, 2); // 在索引为1的位置插入元素2,此时vec变为{1, 2, 3}
还可以一次插入多个相同元素,比如 iterator insert(iterator position, size_type n, const T& value);
形式可以在指定位置插入n
个相同的value
元素。
- erase()
:用于删除指定位置的一个或多个元素,同样有多种重载形式。例如,删除单个元素可以这样用:iterator erase(iterator position);
,删除一段元素区间可以用 iterator erase(iterator first, iterator last);
,其中first
和last
分别是要删除区间的起始和结束迭代器(左闭右开区间)。例如:
std::vector<int> v = {1, 2, 3, 4};
auto it = v.begin() + 1; // 指向元素2
v.erase(it); // 删除元素2,此时v变为{1, 3, 4}
删除一段元素示例:
std::vector<int> ve = {1, 2, 3, 4, 5};
auto start = ve.begin() + 1; // 指向元素2
auto end = ve.begin() + 3; // 指向元素4后面的位置(左闭右开区间概念)
ve.erase(start, end); // 删除元素2和3,此时ve变为{1, 4, 5}
- 访问元素相关函数:
at()
:用于安全地访问vector
中的元素,它会进行边界检查,如果访问越界会抛出std::out_of_range
异常。语法为reference at(size_type n);
,其中n
是要访问元素的索引(从0开始计数)。例如:
std::vector<int> values = {10, 20, 30};
try {int element = values.at(1); // 获取索引为1的元素,即20std::cout << element << std::endl;
} catch (const std::out_of_range& e) {std::cerr << "访问越界: " << e.what() << std::endl;
}
- **`operator[]`(下标运算符)**:也用于访问元素,它与数组的下标访问方式类似,但不会进行边界检查,如果访问越界会导致未定义行为,所以使用时要确保索引在合法范围内。例如:
std::vector<int> data = {5, 10};
int element = data[0]; // 获取第一个元素5,注意这里没有边界检查,需谨慎使用
- **`front()`**:返回`vector`的第一个元素的引用,可以用于获取或修改第一个元素的值。例如:
std::vector<int> firstVector = {1, 2};
int firstElement = firstVector.front(); // 获取第一个元素1
firstVector.front() = 10; // 将第一个元素修改为10,此时firstVector变为{10, 2}
- **`back()`**:与`front()`相对,返回`vector`的最后一个元素的引用,同样可用于获取或修改最后一个元素的值。例如:
std::vector<int> lastVector = {3, 4};
int lastElement = lastVector.back(); // 获取最后一个元素4
lastVector.back() = 40; // 将最后一个元素修改为40,此时lastVector变为{3, 40}
- 获取容器信息相关函数:
size()
:返回vector
中当前元素的个数,返回值类型是std::vector<元素类型>::size_type
(通常是无符号整数类型)。例如:
std::vector<int> sizeVector = {1, 2, 3};
std::vector<int>::size_type elementCount = sizeVector.size(); // 获取元素个数,这里elementCount的值为3
- **`capacity()`**:返回`vector`当前已经分配的内存空间能够容纳的元素个数,也就是它的容量。一般来说,随着不断向`vector`中添加元素,当元素个数接近容量时,`vector`会自动进行扩容操作来分配更多的内存空间。例如:
std::vector<int> capVector;
std::cout << "初始容量: " << capVector.capacity() << std::endl; // 初始容量可能为0或者一个较小的值,取决于实现
capVector.push_back(1);
capVector.push_back(2);
std::cout << "添加元素后的容量: " << capVector.capacity() << std::endl; // 容量可能已经自动扩容了
- **`empty()`**:判断`vector`是否为空,如果为空(即元素个数为0)则返回`true`,否则返回`false`。例如:
std::vector<int> emptyVector;
if (emptyVector.empty()) {std::cout << "该vector为空" << std::endl;
}
std::vector<int> nonEmptyVector = {1};
if (!nonEmptyVector.empty()) {std::cout << "该vector不为空" << std::endl;
}
- 其他常用函数:
clear()
:用于清空vector
中的所有元素,将其大小变为0,但不会释放已分配的内存空间(容量可能不会改变,除非后续有添加或删除元素等操作触发了内存重新分配)。例如:
std::vector<int> clearVector = {1, 2, 3};
clearVector.clear();
if (clearVector.empty()) {std::cout << "已清空vector,现在为空" << std::endl;
}
- **`resize()`**:可以改变`vector`的大小,如果新大小大于当前大小,会根据元素类型的默认构造函数(对于内置类型可能会进行未定义初始化,对于类类型会调用默认构造函数)或者指定的初始值来填充新增的元素;如果新大小小于当前大小,则会删除多余的元素。例如:
std::vector<int> resizeVector(3, 1); // 初始化为{1, 1, 1}
resizeVector.resize(5, 0); // 扩大为{1, 1, 1, 0, 0},新增元素初始化为0
resizeVector.resize(2); // 缩小为{1, 1},删除后面的元素
5. 迭代器
vector
支持迭代器来遍历容器中的元素,迭代器就像是指向容器中元素的指针,可以通过它来依次访问每个元素。常用的迭代器相关操作有:
begin()
和end()
:begin()
返回指向vector
第一个元素的迭代器,end()
返回指向vector
最后一个元素后面位置的迭代器(形成一个左闭右开的区间概念,用于循环遍历等操作符合STL的通用设计模式)。例如,使用迭代器遍历vector
的常见方式如下:
std::vector<int> iterVector = {1, 2, 3};
for (auto it = iterVector.begin(); it!= iterVector.end(); ++it) {std::cout << *it << " "; // 输出每个元素,*it用于解引用迭代器获取元素值
}
// 输出: 1 2 3
rbegin()
和rend()
:这两个函数分别返回反向迭代器,用于从后往前遍历vector
,rbegin()
指向最后一个元素,rend()
指向第一个元素前面的位置(同样是左闭右开区间概念)。例如:
std::vector<int> reverseIterVector = {4, 5, 6};
for (auto rit = reverseIterVector.rbegin(); rit!= reverseIterVector.rend(); ++rit) {std::cout << *rit << " "; // 从后往前输出元素,这里输出: 6 5 4
}
6. 内存管理与性能特点
- 内存自动管理:
vector
会自动处理内存的分配和释放,当添加元素导致当前分配的内存空间不足时,它会自动进行扩容操作,通常是按照一定的倍数(不同的STL实现可能有不同的扩容策略,常见的是1.5倍或2倍等)分配新的更大的内存空间,然后将原有元素复制或移动到新空间中,并释放原来的内存。这种自动管理机制方便了开发者,但也可能在频繁扩容时带来一定的性能开销(尤其是元素类型的拷贝或移动成本较高时)。 - 随机访问性能好:由于
vector
内部元素在内存中是连续存储的,类似于普通的数组,所以它支持快速的随机访问,通过下标运算符或者迭代器解引用访问元素的时间复杂度为常数时间O(1)
。 - 插入和删除元素的性能特点:在
vector
末尾添加或删除元素(通过push_back()
、pop_back()
)的操作通常比较高效,时间复杂度接近常数时间(平均情况,不考虑偶尔的扩容操作带来的影响);但在中间或者开头位置插入或删除元素(通过insert()
、erase()
)可能相对较慢,因为需要移动插入或删除位置后面的所有元素,时间复杂度为线性时间O(n)
,其中n
是vector
中元素的个数。
7. 应用场景
- 存储一组同类型的数据:比如存储游戏中的玩家分数列表、学生的成绩数组等,方便进行数据的添加、删除、修改以及遍历统计等操作。
- 作为函数参数传递数组:在函数间传递数组时,如果数组大小不确定或者可能会变化,使用
vector
作为参数传递要比传递普通C风格数组更加方便、安全,避免了传递数组长度等额外参数以及数组越界等风险。例如:
void processVector(const std::vector<int>& vec) {// 在这里可以安全地遍历vec等操作,不用担心越界问题for (int element : vec) {std::cout << element << " ";}
}
int main() {std::vector<int> data = {1, 2, 3};processVector(data);return 0;
}
- 配合其他STL算法使用:
vector
可以很好地与STL中的各种算法(如排序算法std::sort
、查找算法std::find
等)配合使用,实现更复杂的数据处理功能。例如:
#include <algorithm>
std::vector<int> numbers = {5, 3, 1, 4, 2};
std::sort(numbers.begin(), numbers.end()); // 对vector中的元素进行排序
总之,vector
是C++ STL中一个功能强大、使用方便的容器,掌握它的各种特性和使用方法对于高效地进行C++编程非常有帮助,能够满足很多常见的动态数组相关的编程需求。