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

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

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

💬 欢迎讨论:学习过程中有问题吗?随时在评论区与我交流。你们的互动是我创作的动力!

👍 支持我:如果你觉得这篇文章对你有帮助,请点赞、收藏并分享给更多朋友吧!

🚀 一起成长:欢迎分享给更多对 C++ 感兴趣的小伙伴,让我们共同进步!

前言

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 风格数组的代码:

int arr[5] = {1, 2, 3, 4, 5};

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

#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 个元素值为 valvector
vector(const vector& v) 拷贝构造 vector,与已有 vector 一致
vector(initializer_list<T>) 使用初始化列表构造 vector
2.1.1 示例:不同构造方法
#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;
}

输出

1 2 3 4 5
2.1.2 相关文档

第三章:vector 容量与大小操作

3.1 容量管理接口

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

方法名 功能描述
size() 返回当前元素个数
capacity() 返回分配的存储空间大小
empty() 判断容器是否为空
resize(n) 将容器大小调整为 n,多出的部分用默认值填充
reserve(n) 预留存储空间,避免多次扩容
shrink_to_fit() 收缩 capacity 以匹配 size() 的大小
3.1.1 示例:容量与大小操作
#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;
}

输出

Size: 5
Capacity: 5//说明还没扩容
After resize: 10
Capacity after reserve: 20
Capacity after shrink_to_fit: 10
3.1.2 相关文档

3.2 动态扩展与性能问题

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

补充:

capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2

倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是
根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。

// 测试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 示例:元素访问
#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;
}

输出

First element: 1
Last element: 5
Exception: vector::_M_range_check: __n (which is 10) >= this->size() (which is 5)
4.1.2 相关文档
4.2 修改元素

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

v[0] = 10;
v.at(2) = 20;

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

5.1 迭代器

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

迭代器类型 功能
begin() 返回指向容器第一个元素的迭代器
end() 返回指向容器末尾的迭代器
rbegin() 返回指向容器最后一个元素的反向迭代器
rend() 返回指向容器第一个元素之前位置的迭代器
cbegin() 常量迭代器,无法修改元素
cend() 常量迭代器,返回指向末尾的常量迭代器

)

5.1.1 示例:使用迭代器遍历 vector
#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;
}

输出

1 2 3 4 5 
5 4 3 2 1

5.2 使用 for_each() 函数遍历

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

5.2.1 示例:使用 for_each() 遍历并输出元素
#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;
}

输出

1 2 3 4 5
5.2.2 相关链接

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

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

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

5.3.3 扩容操作导致迭代器失效
示例:扩容导致迭代器失效
#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 中的某些元素时,指向被删除元素及其后续元素的迭代器会失效。

示例:删除导致迭代器失效
#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 删除偶数时的正确和错误写法

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

错误示例:
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就“离家出走”了😂

正确示例:
int main() {
    vector<int> v{1, 2, 3, 4, 4};
    auto it = v.begin();
    
    // 正确的删除写法
    while (it != v.end()) {
        if (*it % 2 == 0) {
            it = v.erase(it);  // 返回新的有效迭代器,指向被删除元素的下一个元素
        } else {
            ++it;
        }
    }

    for (int num : v) {
        cout << num << " ";
    }

    return 0;
}

输出

1 3
5.3.6 编译器对迭代器失效的处理差异

不同编译器(如 GCC 和 MSVC)对迭代器失效的处理方式不同。GCC 在某些情况下可能会宽容处理失效迭代器,程序不会立即崩溃,但输出结果不确定;MSVC 则会直接抛出错误并导致程序崩溃。

示例:GCC 下的宽松处理
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v{1, 2, 3, 4, 5};
    auto it = find(v.begin(), v.end(), 3);
    
    // 删除元素3
    v.erase(it);
    
    // 虽然迭代器失效,但在 GCC 下程序可能不会崩溃
    cout << *it << endl;  // 输出不确定
    return 0;
}
示例:MSVC 下严格处理
 #include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v{1, 2, 3, 4, 5};
    auto it = find(v.begin(), v.end(), 3);
    
    // 删除元素3
    v.erase(it);
    
    // 在 MSVC 下,使用失效迭代器会导致程序崩溃
    cout << *it << endl;  // 程序崩溃
    return 0;
}

 

5.3.7 扩容后的迭代器失效问题

即使扩容后的程序在 Linux 环境下不会立刻崩溃,但输出结果仍然是不可靠的。以下代码展示了 vectorreserve() 扩容后的迭代器失效问题。

#include <iostream>
#include <vector>
using namespace std;

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

    cout << "扩容之前,vector的容量为: " << v.capacity() << endl;
    
    // 通过 reserve 扩容
    v.reserve(100);
    
    cout << "扩容之后,vector的容量为: " << v.capacity() << endl;
    
    // 迭代器失效,输出结果错误
    while (it != v.end()) {
        cout << *it << " ";  // 输出结果可能错误
        ++it;
    }
    cout << endl;
    return 0;
}

输出

1 2 3 4 5  // 正常输出
扩容之前,vector的容量为: 5
扩容之后,vector的容量为: 100
0 2 3 4 5  // 错误输出

5.3.8 总结与建议

  • 避免迭代器失效:进行可能引发迭代器失效的操作(如扩容、删除等)后,必须重新获取迭代器,以保证程序的稳定性。
  • 最佳实践:对于 erase() 操作,使用函数返回的迭代器继续遍历,以避免出现迭代器失效问题。
  • 编译器差异:不同编译器(如 GCC 和 MSVC)对迭代器失效的处理方式不同,在开发跨平台程序时应尤为注意。

第六章:vector 的插入、删除与修改

6.1 插入操作

vector 提供多种方法用于向容器中插入元素。

方法名 功能描述
push_back() 在末尾插入一个元素
insert() 在指定位置插入元素
emplace_back() 在末尾直接构造元素,避免不必要的复制开销
6.1.1 示例:使用 push_back()insert() 插入元素
#include <iostream>
#include <vector>
using namespace std;

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

    // 在末尾插入
    v.push_back(4);

    // 在第二个位置插入元素5
    v.insert(v.begin() + 1, 5);

    for (int val : v) {
        cout << val << " ";
    }
    return 0;
}

输出

1 5 2 3 4
6.1.2 emplace_back()push_back() 的区别

emplace_back() 直接在容器末尾构造元素,减少了不必要的临时对象生成。适用于复杂对象的插入场景。

6.1.3 示例:使用 emplace_back() 插入元素
#include <iostream>
#include <vector>
using namespace std;

struct Point {
    int x, y;
    Point(int a, int b) : x(a), y(b) {}
};

int main() {
    vector<Point> points;

    // 直接在末尾构造对象
    points.emplace_back(1, 2);

    cout << "Point: " << points[0].x << ", " << points[0].y << endl;

    return 0;
}

输出

Point: 1, 2
6.2 删除操作

vector 提供了多种删除元素的方式,包括删除末尾元素和删除指定位置的元素。

方法名 功能描述
pop_back() 删除末尾元素
erase() 删除指定位置的元素或一段范围内的元素
clear() 清空整个 vector
6.2.1 示例:使用 pop_back()erase() 删除元素
#include <iostream>
#include <vector>
using namespace std;

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

    // 删除末尾元素
    v.pop_back();

    // 删除第一个元素
    v.erase(v.begin());

    for (int val : v) {
        cout << val << " ";
    }
    return 0;
}

输出

2 3 4


6.2.2 使用 clear() 清空 vector
v.clear();
cout << "Vector size after clear: " << v.size() << endl;  // 输出:0
6.2.3 相关链接

6.3 修改元素

通过迭代器或下标可以直接修改 vector 中的元素。

6.3.1 示例:修改 vector 元素
#include <iostream>
#include <vector>
using namespace std;

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

    // 修改第二个元素
    v[1] = 10;

    for (int val : v) {
        cout << val << " ";
    }
    return 0;
}

输出

1 10 3 4 5

写在最后

在C++标准模板库(STL)中,vector 是最常用的顺序容器之一。本文通过详细的代码示例和深入分析,全面介绍了 vector 的构造、容量管理、元素操作、迭代器使用及失效问题等。我们探讨了如何高效处理扩容、删除、迭代等常见操作,避免潜在的迭代器失效问题。同时,结合不同编译器下的行为差异,帮助读者理解和避免 vector 使用中的常见错误。无论你是初学者还是高级开发者,这篇文章都将助你全面掌握 vector 的使用技巧和性能优化策略。


💬 欢迎讨论:如果你有任何问题或建议,请在评论区留言。


👍 支持一下:如果你觉得这篇文章对你有帮助,请点赞、收藏并分享!你们的支持是我持续创作的动力!


以上就是关于【C++篇】解密 STL 动态之魂:全面掌握 C++ vector 的高效与优雅的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️

目录
相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
8月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
201 2
|
8月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
412 73
|
9月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
9月前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
8月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
319 3
|
9月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
473 1
|
10月前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
221 21
|
9月前
|
存储 算法 C++
深入浅出 C++ STL:解锁高效编程的秘密武器
C++ 标准模板库(STL)是现代 C++ 的核心部分之一,为开发者提供了丰富的预定义数据结构和算法,极大地提升了编程效率和代码的可读性。理解和掌握 STL 对于 C++ 开发者来说至关重要。以下是对 STL 的详细介绍,涵盖其基础知识、发展历史、核心组件、重要性和学习方法。
|
11月前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
222 1