突破编程_C++_STL教程( vector 的基础知识)

1 std::vector 概述

std::vector 是 C++ 标准模板库(STL)中的一个动态数组,它能够随着元素的添加而自动增长,并且随着元素的移除而自动收缩。std::vector 提供了一种高效、灵活的方式来存储和操作连续的元素序列。std::vector 是一个序列容器,这意味着其元素在内存中按顺序连续存储,从而允许通过偏移量快速访问任何元素。

std::vector 的主要特点包括:

(1)动态大小: std::vector 的大小是动态可调的。可以随时向 std::vector 中添加或删除元素,它会自动调整其大小以容纳新的元素。

(2)连续存储: std::vector 中的元素在内存中连续存储,这意味着它们可以像普通数组一样通过指针和偏移量快速访问。连续存储还允许 std::vector 提供高效的迭代器,支持随机访问。

(3)随机访问迭代器: std::vector 提供了随机访问迭代器,允许你以常数时间复杂度访问任何位置的元素。这样可以使用下标运算符[]或at()函数来访问元素。

(4)自动内存管理: std::vector 负责自动分配和释放内存。当向 std::vector 中添加元素时,它会根据需要分配更多的内存。同样,当从std::vector中删除元素时,它会释放不再需要的内存。

(5)高效的尾部插入和删除: 在 std::vector 的尾部插入和删除元素通常是非常高效的,因为这些操作通常不需要移动其他元素。然而,在 std::vector 的中间插入或删除元素可能会导致需要移动大量元素,因此这些操作可能相对较慢。

(6)类型安全: 由于 std::vector 是一个模板类,因此它是类型安全的。可以为 std::vector 指定元素的类型,并确保只能存储该类型的元素。

(7)兼容性: std::vector 与 C 风格的数组兼容,这意味着可以将 std::vector 传递给期望 C 风格数组的函数,并可以使用指向 std::vector 元素的指针。

(8)算法兼容性: std::vector 与 STL 算法兼容,可以使用诸如 std::sort、std::find 等算法对 std::vector 中的元素进行操作。

1.1 std::vector 与数组的区别

std::vector 和数组在 C++ 中都是用于存储数据的结构,但它们之间存在一些重要的区别:

(1)动态大小与静态大小:
std::vector 是一个动态数组,它的大小可以在运行时动态地增长或缩小。这意味着你可以在任何时候添加或删除元素,而不需要预先指定数组的大小。
数组的大小是静态的,它在创建时就被确定,并且不能改变。必须事先知道要存储多少元素,并为数组分配足够的空间。

(2)内存管理:
std::vector 在堆上分配内存,这意味着它会自动管理内存,包括分配和释放内存。当向std::vector中添加元素时,它会根据需要自动重新分配内存。同样,当元素被删除时,内存也会被自动释放。
数组可以在栈上或静态存储区分配内存。在栈上分配的数组,其大小在编译时确定,并且当数组超出其作用域时,内存会自动被释放。但是,如果你动态地在堆上分配数组(例如使用new),则需要手动管理内存。

(4)容量与大小:
std::vector 有两个相关的概念:容量和大小。容量是 std::vector 实际分配的内存空间大小,而大小是 std::vector 当前存储的元素数量。可以使用 capacity() 和 size() 成员函数来获取这些信息。
数组没有容量和大小的区分;它只有大小,并且这个大小在创建时就确定了。

(5)添加和删除元素:
对于数组,添加或删除元素通常需要手动操作,这可能涉及到内存的重新分配和元素的移动。这可能会很复杂,特别是当数组很大时。
std::vector 提供了成员函数(如 push_back、pop_back、insert 和 erase)来方便地添加和删除元素。这些操作会自动处理内存的分配和释放。

(6)复制与赋值:
数组之间不能直接进行整体之间的复制。如果想要复制一个数组到另一个数组,则需要手动逐个元素地复制。
std::vector 容器可以进行整体之间的赋值操作。可以使用赋值运算符(=)或 std::move 来复制或移动一个 std::vector 的内容到另一个 std::vector。

(7)迭代器与指针:
std::vector 提供了迭代器,可以把它视为一种特殊的指针,可以用来访问和修改容器中的元素。迭代器与指针类似,但提供了类型安全和范围检查。
数组名也可以看作是指针,它指向数组的第一个元素。可以使用指针算术来访问数组中的元素,但这可能会导致越界访问。

(8)类型安全性:
std::vector 是一个模板类,它允许指定元素的类型,从而提供了类型安全性。开发人员只能向 std::vector 中添加指定类型的元素。
数组也提供类型安全性,但不如 std::vector 那样明显。数组只能存储一种类型的元素,但类型是在运行时确定的,而不是在编译时。

综上所述,std::vector 提供了更高的灵活性和易用性,特别是当你需要动态调整数组大小或处理大量数据时。然而,在某些特定场景下,数组可能仍然是一个更合适的选择,尤其是当已经确定数组的大小是固定的,并且对性能有严格要求时。

1.2 std::vector 的内存管理

在内存管理方面,std::vector 有几个关键的特点和行为:

(1)自动分配和释放内存: 当创建一个 std::vector 对象时,它会自动分配足够的内存来存储它的元素。这个内存是在堆上分配的,这意味着它不由栈帧管理,而是在堆上持久存在,直到你显式地释放它或程序结束。当 std::vector 对象超出其作用域或被删除时,它会自动释放其分配的内存。

(2)动态调整大小: 当你向 std::vector 添加元素时(例如使用 push_back),如果当前分配的内存不足以容纳新的元素,std::vector 会自动重新分配内存。这个过程通常涉及分配一块更大的内存块,将现有元素复制到新的内存块,然后释放旧的内存块。这个过程是自动的,对用户是透明的。

(3)容量和大小: std::vector 有两个与内存管理相关的属性:size() 和 capacity()。size() 返回向量中当前元素的数量,而 capacity() 返回向量分配的内存足以容纳的元素数量。reserve 方法可以用来预分配内存,以减少因添加元素而导致的内存重新分配次数。

(4)连续内存: std::vector 保证其元素在内存中连续存储。这意味着可以像使用普通数组一样使用指针算术来访问和遍历元素。连续存储对于某些算法和数据结构来说是非常有利的,因为它允许高效的内存访问模式。

(5)内存碎片: 虽然 std::vector 在大多数情况下能够高效地管理内存,但在某些情况下,特别是当 std::vector 频繁地进行内存重新分配时,可能会导致内存碎片。这是因为每次重新分配都可能涉及不同大小的内存块,而这些内存块可能不连续。虽然现代操作系统和内存管理器通常能够有效地处理内存碎片,但在某些嵌入式系统或资源受限的环境中,这可能是一个需要考虑的问题。

(6)异常安全性: std::vector 的内存管理操作通常是异常安全的。这意味着即使在内存分配失败的情况下(例如,由于内存不足),std::vector 也不会导致数据损坏或泄漏。如果内存分配失败,std::vector 的操作通常会抛出异常(如 std::bad_alloc),并且保持其现有状态不变。

2 std::vector 的基本使用

2.1 std::vector 的声明与初始化

std::vector是一个模板类,它需要一个类型参数来指定向量中存储的元素的类型。如下是声明和初始化 std::vector 的一些样例:

声明
首先,需要包含<vector>头文件以使用 std::vector:

#include <vector>  

// 声明一个整数类型的向量  
std::vector<int> vals;

// 声明一个字符串类型的向量  
std::vector<std::string> strs;

// 声明一个自定义类型的向量  
struct MyStruct 
{
	int id;
	std::string name;
};
std::vector<MyStruct> myStructs;

初始化
可以使用多种方法来初始化std::vector。

(1)默认初始化:

std::vector<int> vals; // 空的向量

(2)使用构造函数初始化:

// 使用指定大小的构造函数  
std::vector<int> vals1(10); // 包含10个元素的向量,每个元素初始化为0  

// 使用迭代器范围初始化  
std::array<int, 3> arr = { 1, 2, 3 };
std::vector<int> vals2(arr.begin(), arr.end()); // 从数组初始化  

// 使用初始化列表  
std::vector<int> vals3 = { 1, 2, 3, 4, 5 };

// 使用另一个向量的拷贝构造函数  
std::vector<int> vals4(vals3); // 拷贝 vals3

(3)使用 assign 成员函数初始化:

std::vector<int> vals;
vals.assign(5, 2); // 创建一个包含 5 个元素的向量,每个元素值为 2

2.2 std::vector 的大小与容量

std::vector 中与大小与容量相关的方法有如下几种:

  • size(): 返回向量中元素的数量。
  • max_size(): 返回向量可以容纳的最大元素数量。
  • capacity(): 返回向量当前的容量。
  • resize(size_type n, value_type val = value_type()): 改变向量的大小,新元素用 val 初始化。
  • reserve(size_type n): 预留至少 n 个元素的空间。
  • shrink_to_fit(): 用于减少 vector 的容量,使其尽可能接近其当前的大小(size)。这可以在某些情况下提高内存使用效率,特别是当 vector 经历了大量的元素添加和删除操作后,其容量可能变得远大于其实际所需的大小时。注意:调用 shrink_to_fit 并不保证 vector 的容量会立即减少。

如下为样例代码:

#include <iostream>  
#include <vector>  

int main() 
{
	// 创建一个空的 vector  
	std::vector<int> numbers;

	// 查看初始大小、容量和最大可能大小  
	std::cout << "Initial size: " << numbers.size() << std::endl;
	std::cout << "Initial capacity: " << numbers.capacity() << std::endl;
	std::cout << "Max size: " << numbers.max_size() << std::endl;

	// 向 vector 中添加元素  
	for (int i = 0; i < 10; i++)
	{
		numbers.push_back(i);
	}

	// 再次查看大小、容量  
	std::cout << "Size after pushing 10 elements: " << numbers.size() << std::endl;
	std::cout << "Capacity after pushing 10 elements: " << numbers.capacity() << std::endl;

	// 预留空间  
	numbers.reserve(20);
	std::cout << "Capacity after reserving 20 elements: " << numbers.capacity() << std::endl;

	// 改变 vector 的大小  
	numbers.resize(5);
	std::cout << "Size after resizing to 5 elements: " << numbers.size() << std::endl;
	std::cout << "Capacity after resizing to 5 elements: " << numbers.capacity() << std::endl;

	// 再次改变 vector 的大小,并用新值初始化新元素  
	numbers.resize(15, -1);
	std::cout << "Size after resizing to 15 elements with value -1: " << numbers.size() << std::endl;
	std::cout << "Capacity after resizing to 15 elements: " << numbers.capacity() << std::endl;

	// 尝试减少容量以匹配当前大小  
	numbers.shrink_to_fit();
	std::cout << "Capacity after shrink_to_fit: " << numbers.capacity() << std::endl;

	return 0;
}

上面代码的输出为:

Initial size: 0
Initial capacity: 0
Max size: 4611686018427387903
Size after pushing 10 elements: 10
Capacity after pushing 10 elements: 13
Capacity after reserving 20 elements: 20
Size after resizing to 5 elements: 5
Capacity after resizing to 5 elements: 20
Size after resizing to 15 elements with value -1: 15
Capacity after resizing to 15 elements: 20
Capacity after shrink_to_fit: 15

在上面代码中,首先创建了一个空的 std::vector<int>,并打印了其初始的大小、容量和最大可能大小。然后通过 push_back 向 vector 中添加了10个元素,并在每次添加后打印其大小和容量。接着使用 reserve 预留了20个元素的空间,并再次打印容量。

之后,通过 resize 方法将 vector 的大小更改为 5,并用默认值(对于 int 类型是 0)初始化新元素。之后再次使用 resize 指定新的大小为 15,并用值 -1 初始化新元素。

最后调用 shrink_to_fit 尝试减少 vector 的容量以匹配其当前大小,并打印出调整后的容量。

注意:shrink_to_fit 不一定会立即减少容量,具体行为取决于 vector 的实现。在某些实现中,shrink_to_fit 可能不会做任何事情,或者可能会将容量减少到接近当前大小的值。

2.3 std::vector 的构造函数与析构函数

构造函数
std::vector 有多个构造函数,允许以不同的方式创建 vector 对象。下面是一些常用的构造函数:

  • vector(): 默认构造函数,创建一个空向量。
  • vector(size_type n, const value_type& value = value_type()): 创建一个包含 n 个元素的向量,每个元素初始化为 value。
  • vector(const vector& other): 复制构造函数,创建一个与另一个向量相同的向量。
  • vector(const vector&& other): 移动构造函数(C++11 新加入)。
  • vector(const_iterator first, const_iterator last): 用迭代器范围构造向量。
  • vector(InitializerList init): 使用初始化列表构造向量(C++11 新加入)。

析构函数
std::vector 的析构函数负责在 vector 对象生命周期结束时释放其占用的资源:

  • ~std::vector();

如下为样例代码:

#include <iostream>  
#include <vector>  

int main() 
{
	// 使用默认构造函数创建一个空向量  
	std::vector<int> emptyVector;
	std::cout << "Empty vector size: " << emptyVector.size() << std::endl;

	// 使用带有大小和初始值的构造函数创建一个向量  
	std::vector<int> fiveInts(5, 10);
	std::cout << "Vector with 5 ints initialized to 10: ";
	for (int num : fiveInts) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 使用复制构造函数创建一个与 fiveInts 相同的向量  
	std::vector<int> copiedVector(fiveInts);
	std::cout << "Copied vector: ";
	for (int num : copiedVector) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 使用移动构造函数创建一个与 copiedVector 相同的向量  
	std::vector<int> movedVector(std::move(copiedVector));
	std::cout << "Moved vector: ";
	for (int num : movedVector) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 使用迭代器范围构造向量  
	std::vector<int> iterVector{ 1, 2, 3, 4, 5 };
	std::vector<int> rangeVector(iterVector.begin(), iterVector.end());
	std::cout << "Vector constructed from iterator range: ";
	for (int num : rangeVector) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 使用初始化列表构造向量  
	std::vector<int> initListVector = { 6, 7, 8, 9, 10 };
	std::cout << "Vector constructed from initializer list: ";
	for (int num : initListVector) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 销毁向量对象时,析构函数将被自动调用  
	// 这里没有显式调用析构函数,因为析构函数是自动的  

	return 0;
}

上面代码的输出为:

Empty vector size: 0
Vector with 5 ints initialized to 10: 10 10 10 10 10
Copied vector: 10 10 10 10 10
Moved vector: 10 10 10 10 10
Vector constructed from iterator range: 1 2 3 4 5
Vector constructed from initializer list: 6 7 8 9 10

上面代码展示了如何使用 std::vector 的不同构造函数来创建向量对象。每种构造函数都用于创建一个具有不同特性的向量。另外,还展示了如何使用迭代器范围和初始化列表来构造向量。

注意:std::vector 的析构函数是自动调用的,当 vector 对象离开其作用域时,它会自动释放其分配的内存。上面代码没有显式调用析构函数,因为 C++ 的内存管理会自动处理这些事项。

3 std::vector 的元素操作

3.1 std::vector 元素的访问与修改

std::vector 中与元素的访问与修改相关的方法有如下几种:

  • at(size_type pos): 访问指定位置的元素,进行边界检查。
  • operator[] (size_type pos): 访问指定位置的元素,不进行边界检查。
  • front(): 访问向量的第一个元素。
  • back(): 访问向量的最后一个元素。

如下为样例代码:

#include <iostream>  
#include <vector>  

int main() 
{
	// 创建一个包含一些整数的向量  
	std::vector<int> vec = { 1, 2, 3, 4, 5 };

	// 使用 at() 方法访问指定位置的元素,进行边界检查  
	try {
		int thirdElement = vec.at(2);
		std::cout << "The element at position 2 is: " << thirdElement << std::endl;

		// 尝试访问超出范围的元素,这将抛出 std::out_of_range 异常  
		int outOfRangeElement = vec.at(10);
	}
	catch (const std::out_of_range& e) {
		std::cout << "Error: " << e.what() << std::endl;
	}

	// 使用 operator[] 访问指定位置的元素,不进行边界检查  
	// 注意:使用 operator[] 访问越界元素可能导致未定义行为  
	int fourthElement = vec[3];
	std::cout << "The element at position 3 is: " << fourthElement << std::endl;

	// 使用 front() 访问向量的第一个元素  
	int firstElement = vec.front();
	std::cout << "The first element is: " << firstElement << std::endl;

	// 使用 back() 访问向量的最后一个元素  
	int lastElement = vec.back();
	std::cout << "The last element is: " << lastElement << std::endl;

	return 0;
}

上面代码的输出为:

The element at position 2 is: 3
Error: invalid vector<T> subscript
The element at position 3 is: 4
The first element is: 1
The last element is: 5

在上面代码中,首先创建了一个包含 5 个整数的 std::vector。然后使用 at() 方法来安全地访问向量中的元素,这个方法在索引越界时会抛出 std::out_of_range 异常。随后又用了 operator[] 来访问元素,但这种方法不会进行边界检查,因此使用它时需要格外小心。

接下来使用了 front() 方法来获取向量的第一个元素,以及 back() 方法来获取向量的最后一个元素。这两个方法都是常量时间复杂度操作,它们提供了一种方便的方式来访问向量的首尾元素。

运行这段代码,可以看到控制台上输出了向量中指定位置的元素以及首尾元素的值。

3.2 std::vector 元素的插入与删除

std::vector 中与元素的插入与删除相关的方法有如下几种:

  • push_back(const value_type& val): 在向量末尾添加一个新元素。
  • emplace_back(const value_type& val): 在向量末尾添加一个新元素,比 push_back 更高效(C++11 新加入)。
  • pop_back(): 删除向量末尾的元素。
  • insert(const_iterator position, const value_type& val): 在指定位置插入一个新元素。
  • insert(const_iterator position, size_type n, const value_type& val): 在指定位置插入n个值为val的元素。
  • insert(const_iterator position, const_iterator first, const_iterator last): 在指定位置插入来自另一个范围的元素。
  • erase(const_iterator position): 删除指定位置的元素。
  • erase(const_iterator first, const_iterator last): 删除指定范围内的元素。
  • clear(): 清空向量中的所有元素。

如下为样例代码:

#include <iostream>  
#include <vector>  

int main() 
{
	// 创建一个空的向量  
	std::vector<int> numbers;

	// 使用 push_back 添加元素  
	numbers.push_back(10);
	numbers.push_back(20);

	// 输出当前向量内容  
	std::cout << "After push_back: ";
	for (int num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 使用 emplace_back 添加元素  
	numbers.emplace_back(30);

	// 输出当前向量内容  
	std::cout << "After emplace_back: ";
	for (int num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 使用 pop_back 删除元素  
	numbers.pop_back();

	// 输出当前向量内容  
	std::cout << "After pop_back: ";
	for (int num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 在指定位置插入元素  
	numbers.insert(numbers.begin() + 1, 50);

	// 输出当前向量内容  
	std::cout << "After insert: ";
	for (int num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 在指定位置插入多个元素  
	numbers.insert(numbers.end(), 2, 60);

	// 输出当前向量内容  
	std::cout << "After inserting multiple elements: ";
	for (int num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 删除指定位置的元素  
	numbers.erase(numbers.begin() + 1);

	// 输出当前向量内容  
	std::cout << "After erase: ";
	for (int num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 清空向量  
	numbers.clear();

	// 输出当前向量内容(应为空)  
	std::cout << "After clear: ";
	for (int num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	return 0;
}

上面代码的输出为:

After push_back: 10 20
After emplace_back: 10 20 30
After pop_back: 10 20
After insert: 10 50 20
After inserting multiple elements: 10 50 20 60 60
After erase: 10 20 60 60
After clear:

在上面代码中,重点关注 push_back 与 emplace_back 的区别:
push_back 是 std::vector 的一个经典成员函数,用于在向量末尾添加一个新元素。它首先创建一个新元素,然后将其复制到向量的末尾。这意味着在内部,可能会发生一次或多次复制操作,这可能会影响性能,尤其是在元素类型很大或复制成本很高时。
emplace_back 是 C++11 引入的一个新函数,用于在向量末尾就地构造一个新元素。与 push_back 不同的是,emplace_back 直接在向量的内存空间内构造元素,因此它避免了额外的复制或移动操作。这通常使 emplace_back 比 push_back 更高效,尤其是对于大型或昂贵复制的对象。

3.3 std::vector 元素的遍历删除

在 std::vector 中遍历并删除元素需要小心处理,因为直接删除元素会导致迭代器失效。一种常见的方法是使用 std::vector 的 erase 方法结合 std::remove_if 算法来遍历并删除满足特定条件的元素。

如下为样例代码:

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

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

	// 使用 std::remove_if 算法标记要删除的元素  
	auto it = std::remove_if(v.begin(), v.end(), [](int n) {
		return n % 2 == 0; // 假设要删除所有偶数  
		});

	// 使用 erase 方法删除标记过的元素  
	v.erase(it, v.end());

	// 输出结果  
	for (const auto& elem : v) {
		std::cout << elem << ' ';
	}

	return 0;
}

上面代码的输出为:

1 3 5 7 9

在这个例子中,std::remove_if 将所有不被移除的元素移动到容器的前面,并返回一个指向第一个应该被移除的元素的迭代器。然后,erase 方法用于删除从该迭代器到 vector 实际结尾的所有元素。

如果需要在遍历过程中逐个删除元素(效率更高),那么可以使用 std::vector::erase 方法结合普通的循环,但每次删除元素后,都需要更新迭代器。以下是一个逐个删除特定元素的例子:

#include <iostream>  
#include <vector>  

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

	// 使用普通循环遍历 vector  
	auto it = v.begin();
	while (it != v.end()) {
		if (*it % 2 == 0) { // 假设要删除所有偶数  
			it = v.erase(it); // 删除元素并更新迭代器  
		}
		else {
			++it; // 移动到下一个元素  
		}
	}

	// 输出结果  
	for (const auto& elem : v) {
		std::cout << elem << ' ';
	}

	return 0;
}

上面代码的输出为:

1 3 5 7 9

在上面代码中,使用一个循环来遍历 vector,并在每次迭代中检查当前元素是否满足删除条件。如果满足条件,则使用 erase 方法删除该元素,并更新迭代器。如果不满足条件,则简单地递增迭代器以继续遍历。

注意:在删除元素后,迭代器 it 会被 erase 方法更新为指向被删除元素之后的位置,因此在下一次循环迭代中,it 仍然有效。

4 std::vector 的迭代器

4.1 std::vector 迭代器的基本使用

std::vector 中与迭代器相关的方法有如下几种:

  • begin(): 返回指向向量第一个元素的迭代器。
  • end(): 返回指向向量末尾之后位置的迭代器。
  • cbegin(), cend(): 返回常量版本的迭代器。

如下为样例代码:

#include <iostream>  
#include <vector>  

int main() 
{
	// 创建一个向量并添加一些元素  
	std::vector<int> numbers = { 1, 2, 3, 4, 5 };

	// 使用 begin() 和 end() 迭代器遍历向量  
	std::cout << "Using begin() and end(): ";
	for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); it++) {
		std::cout << *it << " ";
	}
	std::cout << std::endl;

	// 使用 cbegin() 和 cend() 常量迭代器遍历向量  
	std::cout << "Using cbegin() and cend(): ";
	for (auto it = numbers.cbegin(); it != numbers.cend(); it++) {
		std::cout << *it << " ";
	}
	std::cout << std::endl;

	// 使用基于范围的 for 循环遍历向量(隐式使用 begin() 和 end())  
	std::cout << "Using range-based for loop: ";
	for (const int& num : numbers) {
		std::cout << num << " ";
	}
	std::cout << std::endl;

	// 使用 begin() 和 end() 来获取向量的第一个和最后一个元素  
	if (!numbers.empty()) {
		std::cout << "First element: " << *numbers.begin() << std::endl;
		std::cout << "Last element: " << *(numbers.end() - 1) << std::endl;
	}

	// 注意:不应解引用 end() 迭代器,因为它指向向量末尾的下一个位置  
	// 下面的代码是错误的,会导致未定义行为  
	// std::cout << "End element: " << *numbers.end() << std::endl;  

	return 0;
}

上面代码的输出为:

Using begin() and end(): 1 2 3 4 5
Using cbegin() and cend(): 1 2 3 4 5
Using range-based for loop: 1 2 3 4 5
First element: 1
Last element: 5

在上面代码中,begin() 和end() 被用来遍历向量中的元素,而 cbegin() 和 cend() 被用来进行只读遍历。这两种方式的主要区别在于 cbegin() 和 cend() 返回的迭代器是常量迭代器,这意味着它们不能用来修改向量的元素。

基于范围的 for 循环在 C++11 及更高版本中可用,它内部使用 begin() 和 end() 来遍历容器。这种方式通常使代码更加简洁和易读。

注意:end() 迭代器指向的是向量中最后一个元素之后的位置,因此不应解引用它。解引用 end() 迭代器会导致未定义行为。

4.2 std::vector 迭代器使用的注意事项

在使用 std::vector 的迭代器时,有几点重要的注意事项:

(1)有效性: 迭代器的有效性与其所指向的容器元素紧密相关。如果容器(在这里是 std::vector)被修改(例如插入、删除元素),那么已有的迭代器可能就不再有效。具体来说,如果迭代器指向的元素被删除,那么迭代器就失效了。尝试使用失效的迭代器将导致未定义行为。

(2)不要解引用 end(): end() 迭代器指向的是容器最后一个元素之后的位置,因此不应解引用它。解引用 end() 会导致未定义行为。

(3)迭代器类型: 不同的迭代器类型(如 std::vector<int>::iterator 和 std::vector<int>::const_iterator)有不同的使用场景。非常量迭代器可以用来修改元素的值,而常量迭代器则不能。

(4)范围检查: 在使用迭代器遍历容器时,需要确保迭代器的范围是正确的。在循环中,应该检查迭代器是否不等于 end(),以避免访问越界。

(5)失效的迭代器: 以下操作会导致迭代器失效:

  • 使用 erase() 删除迭代器指向的元素。
  • 使用 insert() 在迭代器指向的位置之前插入元素。
  • 使用 resize() 改变向量的大小。
  • 使用 clear() 清空向量。
    当迭代器失效后,任何对它的操作都是未定义的,通常会导致程序崩溃。

(6)线程安全: 在多线程环境中使用迭代器时,需要注意线程安全。如果多个线程同时修改容器,那么迭代器可能会失效。在这种情况下,需要使用适当的同步机制来确保线程安全。

(7)拷贝和赋值: 迭代器的拷贝和赋值是安全的,但是拷贝的迭代器如果在使用过程中原始容器被修改,那么拷贝的迭代器也会失效。

(8)性能: 迭代器操作通常是高效的,因为它们通常只涉及到指针操作。但是,如果迭代器的使用导致了容器的频繁重新分配(例如,通过 push_back() 导致向量的扩容),那么性能可能会受到影响。

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值