C++ Vector使用指南:常用方法与技巧

C++ Vector 使用指南:常用方法与技巧

std::vector 是 C++ 标准模板库 (STL) 中最常用的序列容器之一。它提供了一种动态数组的功能,可以根据需要自动调整大小,同时保持高效的元素访问和管理。本文将深入探讨 std::vector 的各种用法、常用方法、技巧以及一些高级应用,帮助您充分掌握这个强大的工具。

1. 什么是 Vector?

std::vector 是一个封装了动态大小数组的模板类。与普通数组相比,vector 的优势在于:

  • 动态大小: vector 可以根据元素的添加和删除自动调整其大小,无需手动管理内存。
  • 内存连续: vector 中的元素在内存中是连续存储的,这使得访问元素非常高效(通过索引随机访问)并且与 C 风格数组兼容。
  • 丰富的接口: vector 提供了大量方便的方法来操作元素,如插入、删除、查找等。
  • 自动内存管理: vector 使用 RAII(Resource Acquisition Is Initialization)原则,在对象生命周期结束时自动释放其占用的内存,防止内存泄漏。

2. Vector 的基本使用

2.1 头文件

要使用 std::vector,需要包含 <vector> 头文件:

```c++

include

```

2.2 声明和初始化

以下是几种声明和初始化 vector 的方法:

```c++
// 1. 默认构造函数,创建一个空的 vector
std::vector vec1;

// 2. 创建一个包含 n 个元素的 vector,每个元素都初始化为默认值(对于 int 是 0)
std::vector vec2(5);

// 3. 创建一个包含 n 个元素的 vector,每个元素都初始化为指定的值
std::vector vec3(5, 10); // 包含 5 个值为 10 的元素

// 4. 使用初始化列表(C++11 及以后)
std::vector vec4 = {1, 2, 3, 4, 5};
std::vector vec5{1, 2, 3, 4, 5};

// 5. 使用另一个 vector 复制构造
std::vector vec6(vec4);

// 6. 使用另一个 vector 的范围构造
std::vector vec7(vec4.begin() + 1, vec4.end() - 1); // vec7 包含 {2, 3, 4}

// 7. 创建一个二维vector
std::vector> vec8(3, std::vector(4,0)); //3x4的二维vector,元素初始化为0
```

2.3 访问元素

vector 提供了多种访问元素的方式:

  • 下标运算符 [] 像数组一样,通过索引访问元素。注意:[] 不进行边界检查,如果索引越界,会导致未定义行为。
  • at() 方法: 通过索引访问元素,并进行边界检查。如果索引越界,会抛出 std::out_of_range 异常。
  • front() 方法: 返回 vector 中第一个元素的引用。
  • back() 方法: 返回 vector 中最后一个元素的引用。
  • 迭代器: 使用迭代器遍历 vector 中的元素(稍后详细介绍)。

```c++
std::vector vec = {1, 2, 3, 4, 5};

// 使用 [] 访问
std::cout << vec[0] << std::endl; // 输出 1

// 使用 at() 访问
std::cout << vec.at(2) << std::endl; // 输出 3

// 使用 front() 和 back()
std::cout << vec.front() << std::endl; // 输出 1
std::cout << vec.back() << std::endl; // 输出 5

// 尝试越界访问
// std::cout << vec[10] << std::endl; // 未定义行为
// std::cout << vec.at(10) << std::endl; // 抛出 std::out_of_range 异常
```

2.4 修改元素

```c++
std::vector vec = {1, 2, 3};

// 使用 [] 修改
vec[0] = 10;

// 使用 at() 修改
vec.at(1) = 20;

// vec 现在是 {10, 20, 3}
```

3. Vector 的常用方法

std::vector 提供了丰富的成员函数来操作其内容。

3.1 容量相关

  • size() 返回 vector 中当前元素的个数。
  • empty() 检查 vector 是否为空(即 size() 是否为 0)。
  • capacity() 返回 vector 当前分配的存储空间可以容纳的元素个数。capacity() 通常大于等于 size()
  • reserve(n) 预留至少能容纳 n 个元素的存储空间。这可以减少 vector 在插入元素时重新分配内存的次数,提高效率。注意:reserve() 不会改变 size()
  • shrink_to_fit() (C++11)请求 vector 减少其 capacity() 以匹配 size()。这可以释放多余的内存,但并不保证一定会缩小容量(取决于具体实现)。

```c++
std::vector vec;

std::cout << "Size: " << vec.size() << std::endl; // 输出 0
std::cout << "Capacity: " << vec.capacity() << std::endl; // 输出可能是 0 或一个较小的值

vec.reserve(10);
std::cout << "Size: " << vec.size() << std::endl; // 输出 0
std::cout << "Capacity: " << vec.capacity() << std::endl; // 输出 10 或更大

vec.push_back(1);
vec.push_back(2);

std::cout << "Size: " << vec.size() << std::endl; // 输出 2
std::cout << "Capacity: " << vec.capacity() << std::endl; // 输出 10 或更大

vec.shrink_to_fit(); // 请求缩小容量
std::cout << "Size: " << vec.size() << std::endl; // 输出 2
std::cout << "Capacity: " << vec.capacity() << std::endl; // 可能会缩小,但不一定
```

3.2 元素添加和删除

  • push_back(value)vector 的末尾添加一个值为 value 的元素。
  • pop_back() 删除 vector 的最后一个元素。
  • insert(position, value) 在指定位置 position(迭代器)之前插入一个值为 value 的元素。
  • insert(position, n, value): 在指定位置插入n个值为value的元素。
  • insert(position, first, last): 在指定位置插入范围[first, last)内的元素。
  • erase(position) 删除指定位置 position(迭代器)的元素。
  • erase(first, last) 删除范围 [first, last) 内的元素。
  • clear() 删除 vector 中的所有元素,使其大小变为 0,但不影响capacity。
  • emplace_back(args...): (C++11) 在vector尾部直接构造一个元素,避免了临时对象的创建和拷贝。
  • emplace(position, args...): (C++11) 在指定位置直接构造一个元素。

```c++
std::vector vec = {1, 2, 3};

vec.push_back(4); // vec 现在是 {1, 2, 3, 4}
vec.pop_back(); // vec 现在是 {1, 2, 3}

vec.insert(vec.begin() + 1, 5); // vec 现在是 {1, 5, 2, 3}
vec.insert(vec.begin()+2, 2, 9); // vec 现在是{1,5,9,9,2,3}
std::vector vec2 = {7, 8};
vec.insert(vec.end(), vec2.begin(), vec2.end()); //vec 现在是{1,5,9,9,2,3,7,8}

vec.erase(vec.begin() + 2); // vec 现在是 {1, 5, 9, 2, 3, 7, 8}
vec.erase(vec.begin() + 1, vec.begin() + 3); //vec 现在是{1,2,3,7,8}
vec.clear(); // vec 现在是 {}

// emplace_back 示例
struct MyObject {
int x;
double y;
MyObject(int x, double y) : x(x), y(y) {}
};

std::vector objects;
objects.emplace_back(10, 3.14); // 直接在 vector 中构造 MyObject 对象

```

3.3 其他常用方法

  • resize(n) 调整 vector 的大小为 n。如果 n 小于当前大小,则删除多余的元素;如果 n 大于当前大小,则添加默认初始化的元素。
  • resize(n, value): 调整vector大小为n, 新添加的元素初始化为value.
  • swap(other_vector) 交换两个 vector 的内容。这是一个非常高效的操作,通常是常数时间复杂度。
  • assign(n, value):vector 的内容替换为 n 个值为 value 的元素。
  • assign(first, last): 将vector的内容替换为范围[first,last)内的元素。
  • operator=: 赋值运算符, 将一个vector的内容复制到另一个vector.
  • data(): (C++11) 返回一个指向vector底层数组的指针, 可以用于与C风格的API交互, 但要注意不要越界访问或修改vector的大小.

```c++
std::vector vec = {1, 2, 3};
vec.resize(5); // vec 现在是 {1, 2, 3, 0, 0}
vec.resize(2); // vec 现在是 {1, 2}
vec.resize(4, 9); //vec 现在是 {1,2,9,9}

std::vector vec2 = {4, 5, 6};
vec.swap(vec2); // vec 现在是 {4, 5, 6}, vec2 现在是 {1, 2}

vec.assign(3, 7); //vec 现在是{7,7,7}
std::vector vec3 = {1,2,3,4};
vec.assign(vec3.begin()+1, vec3.end()-1); //vec 现在是{2,3}
```

4. 迭代器

迭代器是一种用于遍历容器中元素的对象,类似于指针。vector 提供了以下几种迭代器:

  • begin() 返回指向 vector 第一个元素的迭代器。
  • end() 返回指向 vector 最后一个元素之后位置的迭代器(这是一个“past-the-end”迭代器,不指向任何有效元素)。
  • rbegin() 返回指向 vector 最后一个元素的反向迭代器。
  • rend() 返回指向 vector 第一个元素之前位置的反向迭代器。
  • cbegin()cend()crbegin()crend() (C++11)返回对应迭代器的 const 版本,用于只读访问。

```c++
std::vector vec = {1, 2, 3, 4, 5};

// 使用正向迭代器遍历
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " "; // 输出 1 2 3 4 5
}
std::cout << std::endl;

// 使用反向迭代器遍历
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << " "; // 输出 5 4 3 2 1
}
std::cout << std::endl;

// 使用 const 迭代器遍历 (C++11)
for (auto it = vec.cbegin(); it != vec.cend(); ++it) {
std::cout << it << " "; // 输出 1 2 3 4 5
//
it = 10; // 错误:不能通过 const 迭代器修改元素
}
std::cout << std::endl;
```

迭代器失效问题:

vector进行插入或删除操作(如push_back, insert, erase等)可能会导致迭代器失效。失效的迭代器不能再被使用,否则会导致未定义行为。

一般来说,以下规则可以帮助判断迭代器是否失效:

  • 插入操作:
    • 如果插入操作导致vector重新分配内存(即capacity发生了变化),则所有迭代器、指针和引用都会失效。
    • 如果插入操作没有导致vector重新分配内存,则指向插入位置之后元素的迭代器、指针和引用会失效,但指向插入位置之前元素的迭代器仍然有效。
  • 删除操作:
    • 指向被删除元素的迭代器、指针和引用会失效。
    • 指向被删除元素之后元素的迭代器、指针和引用也会失效。

为了避免迭代器失效问题,可以采取以下措施:

  • 在插入或删除元素后,重新获取迭代器。
  • 如果需要保存迭代器,可以使用整数索引代替,因为索引不会失效。
  • 对于复杂的迭代器操作,可以考虑使用更稳定的容器,如std::liststd::deque

5. Vector 的高级应用

5.1 作为函数参数和返回值

vector 可以作为函数的参数和返回值。通常,建议使用引用传递(&)或 const 引用传递(const &)来避免不必要的复制开销。

```c++
// 通过引用传递,避免复制
void processVector(std::vector& vec) {
// ... 对 vec 进行修改 ...
}

// 通过 const 引用传递,只读访问
void printVector(const std::vector& vec) {
for (int x : vec) {
std::cout << x << " ";
}
std::cout << std::endl;
}

// 返回 vector
std::vector createVector(int n) {
std::vector vec(n);
// ... 初始化 vec ...
return vec;
}
```

5.2 Vector 与算法

vector 可以与 STL 中的各种算法(如 std::sortstd::findstd::transform 等)结合使用,实现强大的功能。

```c++

include

std::vector vec = {5, 2, 8, 1, 9, 4};

// 排序
std::sort(vec.begin(), vec.end()); // vec 现在是 {1, 2, 4, 5, 8, 9}

// 查找
auto it = std::find(vec.begin(), vec.end(), 8);
if (it != vec.end()) {
std::cout << "Found 8 at index: " << std::distance(vec.begin(), it) << std::endl;
}

// 使用 lambda 表达式进行转换
std::transform(vec.begin(), vec.end(), vec.begin(), { return x * 2; });
// vec 现在是 {2, 4, 8, 10, 16, 18}
```

5.3 Vector 存储自定义类型

vector 不仅可以存储内置类型,还可以存储自定义类型(类或结构体)。但要注意,如果自定义类型包含动态分配的资源(如指针),则需要正确实现拷贝构造函数、赋值运算符和析构函数,以避免浅拷贝和内存泄漏问题。 或者使用智能指针。

```c++
struct Person {
std::string name;
int age;

Person(std::string name, int age) : name(name), age(age) {}

};

std::vector people;
people.emplace_back("Alice", 30);
people.emplace_back("Bob", 25);
```

6. Vector 的性能考虑

  • 预分配内存: 如果事先知道 vector 将要存储的元素数量,使用 reserve() 预分配足够的内存可以避免多次内存重新分配,提高效率。
  • 避免不必要的复制: 在传递 vector 时,尽量使用引用或 const 引用。在添加元素时,如果可以,使用 emplace_back()emplace() 代替 push_back()insert() 来避免创建临时对象。
  • 选择合适的插入/删除位置:vector 的末尾插入和删除元素(push_back()pop_back())非常高效(通常是常数时间复杂度)。但在中间或开头插入和删除元素(insert()erase())会导致后续元素移动,效率较低(线性时间复杂度)。如果需要频繁在中间插入和删除元素,可以考虑使用其他容器,如 std::list
  • 使用移动语义: (C++11) 如果元素类型支持移动语义(move semantics),vector 的一些操作(如 push_back()insert()resize() 等)会自动利用移动语义来提高效率。

7. 总结

std::vector 是 C++ 中一个非常强大且常用的容器。通过掌握其基本用法、常用方法、迭代器、高级应用以及性能注意事项,您可以编写出更高效、更健壮的 C++ 代码。vector 的灵活性和高效性使其成为处理动态数组和序列数据的理想选择。

希望这篇详细的指南对您有所帮助! 如果您有任何其他问题,请随时提出。

THE END