1. 基础概念

1.1 vector是什么,它与数组的区别

vector 是 C++ 标准模板库(STL)中的一种序列容器,它可以被看作是一个动态大小的数组。下面是对 vector 和传统数组的基本介绍,以及它们之间的主要区别:

Vector

类型安全:

vector 是泛型的,可以存储任意类型的元素,例如 vector<int>, vector<string> 等。

内存管理:

vector 在内部自动处理内存分配和释放,使得使用更加方便。

附加功能:

提供了丰富的成员函数,如 push_back(), pop_back(), insert(), erase() 等,方便操作元素。

迭代器支持:

支持迭代器,可以与 STL 算法一起使用。

数组

固定大小:

数组的大小在声明时确定,之后不能改变。

类型特定:

数组只能存储声明时指定的类型。

内存管理:

数组不会自动处理内存分配和释放,需要程序员手动处理动态分配的数组(如使用 new 和 delete)。

直接访问:

提供了直接通过索引访问元素的简单方式。

效率:

在某些情况下,由于没有动态大小的开销,数组可能比 vector 更高效。

主要区别

大小灵活性:

vector 可以在运行时改变大小,而数组的大小在编译时固定。

易用性:

vector 提供了更多的辅助函数和自动的内存管理,使得它在使用上更为方便。

性能:

对于固定大小的数据集,数组可能更有效率;但当需要动态大小时,vector 的优势更明显。

安全性:

vector 的成员函数如 at() 提供边界检查,而数组则没有这样的安全性保障。

1.2 总结

总的来说,vector 提供了更多的灵活性和安全性,尤其是在处理大小可变的数据集时。而数组在处理固定大小和类型的数据时可能更为高效。

2. Vector基本操作

2.1 包含头文件

要在 C++ 程序中使用 vector,需要包含 C++ 标准模板库中的 头文件。这样做可以访问 std::vector 类。
在 C++ 源文件的开始部分,添加以下预处理器指令:

#include <vector>

一旦包含了 头文件,就可以在程序中声明和使用 vector 类型了。这里是一个基本的使用示例:

#include <iostream>
#include <vector>

int main() {
    // 创建一个 int 类型的 vector
    std::vector<int> myVector;

    // 向 vector 添加元素
    myVector.push_back(10);
    myVector.push_back(20);

    // 遍历 vector 并打印元素
    for (int elem : myVector) {
        std::cout << elem << std::endl;
    }

    return 0;
}

在这个示例中,首先包含了 <vector><iostream> 头文件,然后在 main 函数中创建了一个类型为 int 的 vector。我们向这个 vector 中添加了一些元素,并使用范围基 for 循环遍历并打印这些元素。

记住,vector 是在 std 命名空间中定义的,所以需要使用 std::vector 来引用它,除非使用了 using namespace std; 语句。不过,出于代码清晰性和避免命名冲突的考虑,建议直接使用 std:: 前缀。

2.2 声明和初始化

在 C++ 中,声明一个 vector 需要指定其存储元素的类型。vector 是模板类,因此可以声明任何类型的 vector。以下是 vector 声明的基本语法及一些常见示例。

声明

std::vector<ElementType> vectorName;

ElementType 是你打算在 vector 中存储的数据类型。
vectorName 是你给 vector 命名的名称。
示例:
声明一个整数类型的 vector:

std::vector<int> myIntVector;

声明一个双精度浮点数类型的 vector:

std::vector<double> myDoubleVector;

声明一个字符串类型的 vector:

std::vector<std::string> myStringVector;

声明一个自定义类型的 vector (例如,假设有一个 Employee 类):

std::vector<Employee> myEmployeeVector;

初始化

除了基本的声明之外,vector 也可以在声明时进行初始化。

默认初始化 (创建一个空的 vector):
std::vector<int> myVector;
初始化为特定大小 (例如,大小为 10):
std::vector<int> myVector(10);
初始化为特定大小并赋予初始值 (例如,10 个元素,每个元素的值为 0):
std::vector<int> myVector(10, 0);
使用初始化列表:
std::vector<int> myVector = {1, 2, 3, 4, 5};

2.3 访问元素

在 C++ 中,vector 提供了多种方法来访问其元素。最常用的方法包括使用下标运算符 [] 和成员函数 at(),以及通过迭代器。下面是每种方法的详细介绍:

使用下标运算符 []

这是访问 vector 元素最直接的方法。语法类似于数组,使用索引来访问元素。下标从 0 开始,最大为 vector 的大小减一。使用下标运算符时,不会进行边界检查,因此超出范围的访问可能导致未定义行为。

std::vector<int> v = {10, 20, 30, 40};
std::cout << v[2];  // 输出第三个元素,结果为 30

使用成员函数 at()

at() 方法提供了边界检查。如果索引超出范围,它会抛出一个 std::out_of_range 异常。这种方法比 [] 运算符更安全,特别是在调试阶段。

std::vector<int> v = {10, 20, 30, 40};
std::cout << v.at(2);  // 输出第三个元素,结果为 30
try {
    std::cout << v.at(4);  // 超出范围,将抛出异常
} catch (const std::out_of_range& e) {
    std::cerr << "Error: " << e.what() << std::endl;
}

使用迭代器

vector 支持迭代器,这是一种更通用和灵活的访问方式。迭代器可以用来遍历 vector 的所有元素。

std::vector<int> v = {10, 20, 30, 40};
for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << std::endl;  // 输出当前元素
}

使用范围基 for 循环(C++11 及以后版本)

这是一种简洁的遍历 vector 的方法。自动处理迭代和访问。

std::vector<int> v = {10, 20, 30, 40};
for (int num : v) {
    std::cout << num << std::endl;  // 输出当前元素
}

注意事项

在使用下标运算符 [] 时,要确保不会访问超出 vector 范围的元素。
如果程序的健壮性是首要考虑,使用 at() 方法更为安全,因为它会进行边界检查。
使用迭代器或范围基 for 循环可以更加优雅地遍历 vector。

2.4 修改元素

在 C++ 中,vector 提供了多种方式来修改其元素。这些方法包括直接访问和修改特定元素、添加新元素、删除现有元素,以及其他更复杂的操作。以下是这些方法的详细介绍:

直接修改元素

你可以使用下标运算符 [] 或 at() 方法来直接访问并修改 vector 中的元素。这种方法类似于操作普通数组。

std::vector<int> v = {10, 20, 30, 40};
v[2] = 100;  // 使用下标运算符修改第三个元素
v.at(3) = 200;  // 使用 at() 方法修改第四个元素

添加元素

push_back(value): 在 vector 的末尾添加一个新元素。
emplace_back(args…): 类似于 push_back,但可以直接构造元素,避免复制或移动操作。
insert(position, value): 在指定位置插入一个或多个新元素。

std::vector<int> v = {10, 20, 30};
v.push_back(40);  // 现在 v = {10, 20, 30, 40}
v.emplace_back(50);  // 现在 v = {10, 20, 30, 40, 50}
v.insert(v.begin() + 2, 15);  // 在第三个位置插入 15,现在 v = {10, 20, 15, 30, 40, 50}

删除元素

pop_back(): 移除 vector 末尾的元素。
erase(position) 或 erase(start, end): 移除指定位置或范围内的元素。
clear(): 清空整个 vector。

std::vector<int> v = {10, 20, 30, 40, 50};
v.pop_back();  // 移除最后一个元素,现在 v = {10, 20, 30, 40}
v.erase(v.begin() + 2);  // 移除第三个元素,现在 v = {10, 20, 40}
v.clear();  // 清空整个 vector

其他修改操作

resize(n, value): 改变 vector 的大小。如果新大小大于当前大小,新元素将被初始化为 value(如果提供了的话)。
swap(vector): 与另一个 vector 交换内容。

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = {4, 5, 6};
v1.resize(5, 100);  // 现在 v1 = {1, 2, 3, 100, 100}
v1.swap(v2);  // 现在 v1 = {4, 5, 6}, v2 = {1, 2, 3, 100, 100}

注意事项

在使用 [] 或 at() 修改元素时,确保索引在 vector 的有效范围内。
在添加或删除元素时,vector 的大小会自动调整。
使用 emplace_back 而非 push_back 可以提高效率,尤其是对于复杂类型的对象。
insert, erase, 和 resize 等操作可能影响 vector 的性能,特别是在处理大量数据时。

2.5 迭代器

迭代器类型

正向迭代器:

允许你从 vector 的开头向末尾遍历。

反向迭代器:

允许你从 vector 的末尾向开头遍历。

常量迭代器:

不允许修改它们所指向的元素,可以是正向或反向。

获取迭代器

使用 begin() 和 end() 成员函数获得指向 vector 开头和末尾的正向迭代器。
使用 rbegin() 和 rend() 获得反向迭代器。
cbegin()、cend()、crbegin() 和 crend() 分别用于获取常量正向和反向迭代器(C++11 及以后版本)。

迭代器操作

遍历:通过递增(++)和递减(- -)迭代器来遍历 vector。
访问元素:使用解引用操作符(*)来访问迭代器所指向的元素。
比较:迭代器可以使用比较操作符(如 == 和 !=)来判断是否到达 vector 的末尾或特定位置。
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    // 使用正向迭代器遍历
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 使用反向迭代器遍历
    for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    return 0;
}
注意事项

在使用迭代器时,确保不要越界访问。例如,不要解引用 end() 或 rend() 返回的迭代器。
修改 vector(如添加或删除元素)可能会使现有迭代器失效。在进行这类操作后,应该重新获取迭代器。
常量迭代器适用于不需要修改元素值的情况,增强代码的安全性。
通过使用迭代器,你可以编写更通用的代码,这些代码可以适用于多种不同的容器类型,不仅仅是 vector。

3. vector大小和容量详解

在 C++ 中,vector 的大小(size)和容量(capacity)是两个重要但不同的概念。理解这两者之间的区别对于有效地使用 vector 来说非常关键。

大小(Size)

定义:

vector 的大小是指它当前包含的元素数量。

获取方法:

使用 size() 成员函数。

动态变化:

当你添加或删除元素时,vector 的大小会相应增加或减少。

示例:

如果你向一个空的 vector 添加了三个元素,它的大小(size())将是 3。

容量(Capacity)

定义:

vector 的容量是指在需要重新分配内存之前它可以容纳的元素数量。

获取方法:

使用 capacity() 成员函数。

预先分配:

为了减少多次分配内存的开销,vector 通常会预先分配比当前大小更多的空间。这意味着容量通常会等于或大于大小。

自动增长:

当添加新元素导致当前容量不足时,vector 会自动增加其容量(通常是当前容量的两倍,但这取决于实现)。

示例:

一个大小为 3 的 vector 可能有一个容量为 6,表示它可以在不重新分配内存的情况下增加到 6 个元素。

重要操作

resize(n): 改变 vector 的大小为 n。如果 n 大于当前大小,会添加默认初始化的元素;如果 n 小于当前大小,多余的元素会被移除。

std::vector<int> v = {1, 2, 3};
v.resize(5);  // 大小改为 5,添加了两个值为 0 的元素
reserve(n): 保证 vector 至少有 n 的容量。如果 n 大于当前容量,会进行内存分配以增加容量;否则,该方法不会改变 vector 的容量。
std::vector<int> v;
v.reserve(10);  // 容量至少为 10,但大小仍为 0

注意事项

对于性能敏感的应用,合理管理 vector 的大小和容量是很重要的。频繁的内存分配和复制可能会导致性能问题。
使用 reserve() 预分配内存可以减少因添加新元素而导致的多次内存重新分配。
size() 和 capacity() 的值可以相同,但也可以不同。capacity() 永远不会小于 size()。
在处理大量数据时,了解和合理利用 vector 的大小和容量可以帮助优化程序的性能和内存使用。

4. 算法操作

C++ 中的 vector 容器与标准模板库(STL)提供的算法高度兼容。这种兼容性是通过迭代器实现的,使得 vector 可以与大多数通用算法一起使用。

算法概述

STL 算法库包括一系列标准化的函数,用于执行范围内的操作,如排序、搜索、转换、计数等。
这些算法通常通过一对迭代器来指定作用的范围(例如,begin() 和 end())。

常用算法示例

排序 (sort):

对 vector 中的元素进行排序。

std::vector<int> v = {4, 1, 3, 5, 2};
std::sort(v.begin(), v.end());  // 升序排序
查找 (find):

查找 vector 中的特定元素。

auto it = std::find(v.begin(), v.end(), 3);
if (it != v.end()) {
    std::cout << "Element found!" << std::endl;
}
反转 (reverse):

反转 vector 中的元素顺序。

std::reverse(v.begin(), v.end());
累加 (accumulate):

计算 vector 中所有元素的总和。

int sum = std::accumulate(v.begin(), v.end(), 0);
唯一化 (unique):

移除连续且相同的元素。

std::sort(v.begin(), v.end());  // unique 需要先排序
auto last = std::unique(v.begin(), v.end());
v.erase(last, v.end());  // 移除重复之后的多余元素

注意事项

在使用这些算法时,确保传递正确的迭代器范围。
某些算法(如 sort 和 unique)可能改变容器中元素的位置或数量。在使用这些算法之后,之前获取的迭代器可能会失效。
许多算法需要容器中的元素已经按照某种顺序排列,例如 unique 和 binary_search,在使用这些算法之前,需要确保容器已经正确排序。
accumulate 等算法可能需要额外的头文件(如 )。
通过将 vector 与 STL 算法结合使用,你可以编写出既简洁又高效的代码来处理复杂的数据集操作。这也是 C++ STL 强大和灵活的一个重要体现。

5. 性能考量

当使用 C++ 的 vector 容器时,考虑其性能特点是非常重要的。虽然 vector 提供了便利性和灵活性,但在某些情况下,它的行为可能对程序的性能产生影响。以下是需要考虑的主要性能方面:

5.1 动态数组的性能影响

内存分配和重新分配:

当向 vector 添加新元素而当前容量不足以容纳它们时,vector 需要进行内存重新分配。这个过程涉及分配新的更大的内存块、复制现有元素到新内存、并释放旧内存。这可能是一个相对昂贵的操作,特别是对于大型 vector 或包含复杂对象的 vector。

预分配容量:

通过 reserve() 方法预先分配足够的容量可以避免或减少内存重新分配的次数,从而提高性能。

5.2 元素访问和修改

随机访问:

vector 提供了快速的随机访问(即直接通过索引访问),这在性能方面是一个优势,特别是与一些其他容器(如 list 或 forward_list)相比。

插入和删除:

在 vector 的末尾添加或移除元素(例如使用 push_back() 和 pop_back())是非常快速的。但是在 vector 的开始或中间位置插入或删除元素可能会导致现有元素的移动,这可能是一个较慢的操作。

5.3 大小与容量管理

容量与大小:

vector 的容量可能大于其实际大小。这意味着 vector 可能会占用比实际需要的更多内存。使用 shrink_to_fit() 可以请求减少 vector 的容量以匹配其大小,尽管这种请求不一定总会被满足。

大小变更操作:

resize() 和 reserve() 可以用来管理 vector 的大小和容量。合理使用这些操作可以避免不必要的内存重新分配。

5.4 迭代器和算法

迭代器遍历:

使用迭代器遍历 vector 通常是高效的。但是,如果在遍历过程中修改 vector(例如添加或删除元素),可能会使迭代器失效,导致未定义行为。

5.5 总体性能

整体效率:

总的来说,vector 是一个高效的容器,特别是在需要快速随机访问元素时。对于大多数用途,它提供了良好的性能和方便的接口。

选择合适的容器:

虽然 vector 在很多情况下都是一个好的选择,但根据具体需求选择合适的容器类型是很重要的。例如,如果你需要频繁在容器中间插入或删除元素,list 或 deque 可能是更好的选择。

6. 高级特性

C++ vector 容器提供了一些高级特性,特别是在 C++11 和后续版本中。这些特性不仅增加了灵活性,还改善了性能和使用方便性。以下是一些重要的高级特性:

初始化列表 (C++11)

允许使用花括号 {} 初始化 vector,使代码更简洁。
例如:

std::vector<int> v = {1, 2, 3, 4, 5};

emplace_back 和 emplace (C++11)

这些函数在 vector 中直接构造元素,而不是先构造然后复制或移动到容器中。
这可以提高性能,特别是对于包含复杂对象的 vector。
例如:

v.emplace_back(3, "three"); //在 vector 的末尾直接构造一个元素。

范围构造器 (C++11)

允许使用两个迭代器作为参数来构造 vector,从而复制或移动另一个容器中的元素范围。
例如:

std::vector<int> v1 = {1, 2, 3}; 
std::vector<int> v2(v1.begin(), v1.end());

shrink_to_fit (C++11)

请求减少 vector 的容量以匹配其大小,有助于节省内存。
不保证容量一定会减少,但大多数实现都会尽量满足这个请求。

data 方法 (C++11)

提供对 vector 内部数组的直接访问。
可以用于与 C 风格的函数接口交互,需要一个指向数组的指针。
例如:

int* p = v.data();

移动语义 (C++11)

vector 支持移动构造和移动赋值,这意味着在某些情况下可以避免复制整个 vector 的内容,而是直接移动它。
对于包含大量元素的 vector,这可以大大提高性能。

noexcept 修饰符 (C++11)

一些 vector 操作被标记为 noexcept,表明它们不会抛出异常。
这对于某些需要异常安全保证的场景非常有用。

可以在 vector 上使用的 Lambda 表达式 (C++11)

结合 STL 算法,可以用于强大的元素处理,如 std::for_each 或 std::transform。

支持非标准分配器

vector 可以使用自定义分配器,这对于特殊的内存管理需求很有用。
这些高级特性使得 vector 更加强大和灵活,同时也提高了其与现代 C++ 语言特性的兼容性。这些特性的使用可以使代码更加高效、简洁和功能强大。

7. vector动态大小详解

vector 可以根据需要自动调整大小,这意味着你可以在运行时向 vector 添加或删除元素,而它的大小会自动增长或缩减。

vector 的动态大小特性是其最重要的特点之一。这个特性允许 vector 在运行时根据需要自动调整其存储的元素数量。以下是对这一特性的详细介绍:

自动调整大小

当新元素被添加到 vector 中,而当前的存储空间不足以容纳更多元素时,vector 会自动扩展其容量。
通常,vector 会将其容量增加到当前大小的两倍,这种增长策略旨在平衡内存使用和重新分配的次数。不过,具体的增长策略可能因实现而异。

内存管理

vector 在内部管理一个动态数组,用于存储其元素。当 vector 的大小改变时,它可能需要分配一个更大的数组并将现有元素复制到新数组中,然后释放旧数组的内存。
这个过程是自动的,对使用者来说是透明的。

添加和删除元素

vector 提供了多种方法来添加和删除元素,如 push_back, pop_back, insert, erase 等。这些操作可能导致 vector 的大小变化。
当添加元素而容量不足时,vector 将自动扩容。当删除元素时,大多数实现不会减小 vector 的容量,除非显式调用 shrink_to_fit 或类似方法。

性能考虑

虽然动态大小提供了极大的灵活性,但可能导致性能开销,主要是由于内存的重新分配和元素的复制。
为了优化性能,可以使用 reserve 方法预先分配足够的空间,以减少内存重新分配的次数。

容量与大小

vector 的容量(capacity())和大小(size())是两个不同的概念。大小是 vector 当前含有的元素数量,而容量是它在不重新分配内存的情况下可以存储的元素的最大数量。
当 size() 超过 capacity() 时,vector 将增加其容量。

实际应用

动态大小特性使得 vector 成为处理大小不固定的数据集的理想选择,如从文件读取数据、用户输入处理等场景。
总之,vector 的动态大小特性提供了极大的灵活性和便利性,但也带来了一定的性能考虑。理解这一特性对于高效和正确地使用 vector 至关重要。

8. 应用场景

vector 在 C++ 中的应用非常广泛,因为它提供了一种灵活、动态的方式来存储和操作一系列元素。以下是一些 vector 的实际应用案例:

数据存储和处理

数值数据集:存储一系列数值,如温度读数、股票价格等,然后进行分析,如计算平均值、排序、查找最大/最小值等。
字符串列表:存储一系列字符串,如文件中的所有行、单词列表等,并进行搜索、排序或转换操作。

动态数组替代

图像处理:用于存储和操作像素值的动态数组,例如在图像处理或计算机视觉应用中。
音频数据处理:存储一系列音频样本,用于处理或分析,如声音编辑、音频信号处理等。

实现栈和队列

栈实现:虽然 C++ STL 提供了 stack 容器,但可以使用 vector 实现类似栈的结构,利用 push_back 和 pop_back 方法。
队列实现:同样,可以用 vector 实现简单的队列功能。

用作动态数组的传递参数

函数参数:将 vector 用作函数参数,方便地传递一组值给函数。
返回值:函数可以返回 vector,方便地返回一组计算结果。

存储自定义对象

对象集合:存储自定义类或结构体实例的集合,如员工名单、学生记录等。
游戏开发:在游戏开发中用于存储游戏对象,如敌人列表、可用道具集合等。

作为算法操作的基础

排序和搜索:使用 STL 提供的算法(如 sort, find)对 vector 中的元素进行排序和搜索。
转换和操作:使用 transform, for_each 等算法对 vector 中的元素进行操作。

图形和科学计算

图形数据:存储图形数据,如顶点列表、颜色信息等。
科学计算:用于存储和处理数值计算的结果,如数值分析、物理模拟等。

容器嵌套

二维数组:通过创建 vector 的 vector(即 vector<vector>)来实现类似于二维数组的结构。
vector 的这些应用展示了其灵活性和实用性,它是 C++ 编程中最常用的数据结构之一。根据具体的应用需求,开发者可以选择 vector 来实现高效、可读性强的代码。

01-06 11:13