【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅

2024-10-09 20:03:10 浏览数 (5)

C vector 容器详解:从入门到精通

前言

C 标准模板库(STL)是现代 C 编程的基石,其中的容器、算法和迭代器为开发者提供了高效、灵活的数据处理工具。vector 作为 STL 中最常用的顺序容器,不仅支持动态数组的功能,还通过自动内存管理和丰富的操作接口,极大简化了数据操作的复杂性。无论是在日常开发还是算法竞赛中,vector 的高效性和灵活性都使其成为开发者的首选。

本篇文章将带你深入理解 C vector 的内部工作原理与高级用法,从基本的构造与遍历,到迭代器失效问题的深入剖析,再到在不同场景下的优化策略。通过全面、详细的讲解,我们将一步步揭开 vector 的技术细节,让你在掌握 vector 基本使用的基础上,更好地应对复杂的开发需求。

第一章:C vector 容器简介
1.1 C STL 容器概述

C 提供了丰富的标准模板库 (STL),包括 顺序容器(如 vector)、关联容器(如 mapset)等。vector 是最常用的 STL 顺序容器之一,它的特点是支持 动态数组,可以在运行时自动扩展容量,提供高效的随机访问。

1.2 为什么使用 vector

与传统的 C 风格数组(T array[N])相比,vector 具有以下优势:

  • 动态调整大小,无需手动管理内存;
  • 提供了丰富的接口,支持插入、删除、查找等操作;
  • 内置内存管理机制,防止越界访问。

例如,使用 C 风格数组的代码:

代码语言:javascript复制
int arr[5] = {1, 2, 3, 4, 5};

与之相比,使用 vector 的方式更加灵活:

代码语言:javascript复制
#include <vector>
using namespace std;

vector<int> v = {1, 2, 3, 4, 5};  // 自动管理内存和大小
1.3 vector 的优缺点
  • 优点:动态扩展、支持随机访问、效率高。
  • 缺点:尾部操作快,但头部插入和删除较慢(涉及到元素移动)。

第二章:vector 的构造方法
2.1 常见构造函数

C vector 提供了多种构造方法,可以创建不同初始状态的 vector 对象。

构造函数

功能

vector()

构造一个空的 vector

vector(size_type n, const T& val)

构造包含 n 个元素值为 val 的 vector

vector(const vector& v)

拷贝构造 vector,与已有 vector 一致

vector(initializer_list<T>)

使用初始化列表构造 vector

2.1.1 示例:不同构造方法
代码语言:javascript复制
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v1;                    // 空 vector
    vector<int> v2(5, 100);            // 5个100的元素
    vector<int> v3(v2);                // 拷贝构造
    vector<int> v4 = {1, 2, 3, 4, 5};  // 使用初始化列表

    for (int i : v4) {
        cout << i << " ";  // 输出: 1 2 3 4 5
    }
    return 0;
}

输出

代码语言:javascript复制
1 2 3 4 5
2.1.2 相关文档
  • C Reference: vector constructor

第三章:vector 容量与大小操作
3.1 容量管理接口

C vector 提供了多种方法管理容器的容量与大小。

方法名

功能描述

size()

返回当前元素个数

capacity()

返回分配的存储空间大小

empty()

判断容器是否为空

resize(n)

将容器大小调整为 n,多出的部分用默认值填充

reserve(n)

预留存储空间,避免多次扩容

shrink_to_fit()

收缩 capacity 以匹配 size() 的大小

3.1.1 示例:容量与大小操作
代码语言:javascript复制
#include <iostream>
#include <vector>
using namespace std;

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

    cout << "Size: " << v.size() << endl;         // 当前元素个数
    cout << "Capacity: " << v.capacity() << endl; // 当前容量
    v.resize(10, 100);                            // 调整大小到10
    cout << "After resize: " << v.size() << endl;
    v.reserve(20);                                // 预留空间
    cout << "Capacity after reserve: " << v.capacity() << endl;
    v.shrink_to_fit();                            // 收缩容量
    cout << "Capacity after shrink_to_fit: " << v.capacity() << endl;

    return 0;
}

输出

代码语言:javascript复制
Size: 5
Capacity: 5//说明还没扩容
After resize: 10
Capacity after reserve: 20
Capacity after shrink_to_fit: 10
3.1.2 相关文档
  • C Reference: vector capacity
3.2 动态扩展与性能问题

vector 超过当前容量时,会自动扩展存储空间,通常是翻倍。虽然扩展是自动的,但涉及到内存重新分配,因此建议提前使用 reserve() 预留空间,减少不必要的性能开销。

补充:

capacity的代码在vs和g 下分别运行会发现,vs下capacity是按1.5倍增长的,g 是按2 倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是 根据具体的需求定义的。vs是PJ版本STL,g 是SGI版本STL。

代码语言:javascript复制
// 测试vector的默认扩容机制
void TestVectorExpand()
{
    size_t sz;
    vector<int> v;
    sz = v.capacity();
    cout << "making v grow:n";
    for (int i = 0; i < 100;   i)
    {
        v.push_back(i);
        if (sz != v.capacity())
        {
            sz = v.capacity();
            cout << "capacity changed: " << sz << 'n';
        }
    }
}
vs:运行结果:vs下使用的STL基本是按照1.5倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141

g  运行结果:linux下使用的STL基本是按照2倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 4
capacity changed: 8
capacity changed: 16
capacity changed: 32
capacity changed: 64
capacity changed: 128

reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代 价缺陷问题。 resize在开空间的同时还会进行初始化,影响size。


第四章:vector 元素访问与修改
4.1 元素访问方法

方法名

功能

operator[]

通过下标访问元素,不进行边界检查

at(n)

访问指定位置元素,进行越界检查

front()

返回第一个元素

back()

返回最后一个元素

data()

返回指向数组头部的指针

4.1.1 示例:元素访问
代码语言:javascript复制
#include <iostream>
#include <vector>
using namespace std;

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

    cout << "First element: " << v.front() << endl; // 访问第一个元素
    cout << "Last element: " << v.back() << endl;   // 访问最后一个元素

    try {
        cout << v.at(10);  // 越界访问
    } catch (out_of_range& e) {
        cout << "Exception: " << e.what() << endl;
    }//异常捕获

    return 0;
}

输出

代码语言:javascript复制
First element: 1
Last element: 5
Exception: vector::_M_range_check: __n (which is 10) >= this->size() (which is 5)
4.1.2 相关文档
  • C Reference: vector element access
4.2 修改元素

通过 operator[]at() 直接修改元素内容:

代码语言:javascript复制
v[0] = 10;
v.at(2) = 20;

第五章:vector 的迭代器与遍历
5.1 迭代器

vector 提供了多种迭代器类型,便于对元素进行遍历、修改或访问。

迭代器类型

功能

begin()

返回指向容器第一个元素的迭代器

end()

返回指向容器末尾的迭代器

rbegin()

返回指向容器最后一个元素的反向迭代器

rend()

返回指向容器第一个元素之前位置的迭代器

cbegin()

常量迭代器,无法修改元素

cend()

常量迭代器,返回指向末尾的常量迭代器

)

5.1.1 示例:使用迭代器遍历 vector
代码语言:javascript复制
#include <iostream>
#include <vector>
using namespace std;

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

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

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

    return 0;
}

输出

代码语言:javascript复制
1 2 3 4 5 
5 4 3 2 1
5.2 使用 for_each() 函数遍历

for_each() 是一种 STL 提供的便捷函数,用于对容器中的每个元素执行指定的操作。

5.2.1 示例:使用 for_each() 遍历并输出元素
代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void print(int val) {
    cout << val << " ";
}

int main() {
    vector<int> v = {1, 2, 3, 4, 5};
    for_each(v.begin(), v.end(), print);  // 使用for_each输出
    return 0;
}

输出

代码语言:javascript复制
1 2 3 4 5
5.2.2 相关链接
  • C vector 迭代器文档
  • STL for_each 文档

5.3 vector 迭代器失效问题(重点)
5.3.1 迭代器失效的原因与后果

vector 迭代器失效的根本原因在于底层内存的重新分配元素的移除,导致迭代器指向的内存不再有效。当发生迭代器失效时,继续使用该迭代器可能会引发未定义行为,如程序崩溃访问错误数据

5.3.2 常见导致迭代器失效的操作
  • 扩容相关操作:当 vector 需要扩展容量时,会分配新的内存空间并将原有元素搬移到新的位置。此时,所有的迭代器将会失效。
    • resize()
    • reserve()
    • insert()
    • push_back()
    • assign()
  • 删除操作:删除操作会使指向被删除元素及其后续元素的迭代器失效。

5.3.3 扩容操作导致迭代器失效
示例:扩容导致迭代器失效
代码语言:javascript复制
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{1, 2, 3, 4, 5, 6};
    auto it = v.begin();
    
    // 扩容相关操作导致迭代器失效
    v.resize(100, 8);  // 扩容并填充元素
    // v.reserve(100);  // 扩容但不增加元素
    // v.push_back(7);  // 末尾插入可能引发扩容
    // v.assign(100, 8);  // 重新赋值并扩容

    // 扩容后需要重新获取迭代器
    it = v.begin();

    while (it != v.end()) {
        cout << *it << " ";
          it;
    }
    cout << endl;
    return 0;
}

说明:在每次扩容操作后,vector 可能会分配新的内存空间,并释放原来的内存区域。这意味着之前的迭代器已指向失效的内存,因此在扩容操作后,必须重新获取迭代器


5.3.4 删除操作导致迭代器失效

删除 vector 中的某些元素时,指向被删除元素及其后续元素的迭代器会失效。

示例:删除导致迭代器失效
代码语言:javascript复制
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v{1, 2, 3, 4};
    
    // 查找元素3的迭代器
    auto pos = find(v.begin(), v.end(), 3);
    
    // 删除元素3
    v.erase(pos);
    
    // 迭代器失效,继续使用将导致程序崩溃或未定义行为
    cout << *pos << endl;  // 非法访问
    return 0;
}

说明:删除某个元素后,指向该元素及其后续元素的迭代器会失效。在删除操作后应重新获取有效的迭代器,以避免出现非法访问或程序崩溃。


5.3.5 删除偶数时的正确和错误写法

错误的删除写法在删除元素后没有正确更新迭代器,会导致迭代器失效,引发未定义行为。

错误示例:
代码语言:javascript复制
int main() {
    vector<int> v{1, 2, 3, 4, 4};
    auto it = v.begin();
    
    // 错误的删除写法,迭代器未更新
    while (it != v.end()) {
        if (*it % 2 == 0) {
            v.erase(it);  // 迭代器失效,  it 导致未定义行为
        }
          it;
    }
    return 0;
}

这里去分析一下会发现itend两个刚好错过了,it就“离家出走”了

0 人点赞