初始vector——数组的高级产物

前言:

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

本文将详细介绍vector的用法及逻辑,带领读者逐步实现对vector这一动态数组从入门到精通,感悟C++对比C语言的超脱与精妙之处。

一.容器的引入

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

其中其他容器会在后续逐个介绍,本文重点讲解vector的相关语法。

二.vector的简要介绍

1.含义

vector是表示可变大小数组的序列容器。

其存储方式与数组类似,都是一片区域空间的连续存储,但是数组的大小在初始化时就已经确定,

而vectotr支持在使用时动态改变大小。

使用对比如下:

使用C语言创建一个数组:
 

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

容器初使用:

#include <vector>
using namespace std;

vector<int> v = {1, 2, 3, 4, 5};  // 自动管理内存和大小

2.优劣对比

优点:支持[]随机访问,动态分配大小,使用灵活效率高

缺点:插入数据时只有尾插较为切换,其他插入与删除数据设计数组元素的大量移动,效率较低。 

三.vector的构造函数

1.常用示例

C++提供了多种构造函数,以应对不同情况下的参数传递。

示例如下:

#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.具体文档

vector的详细用法与介绍见以下文档。

cplusplus.com/reference/vector/vector/

四.vector容积大小相关操作

C++针对vector的动态分配功能,提供了多个函数。

 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
 

分析:

1.原先元素个数为5,容积也为5(初始情况下容积大小不一定等于元素个数大小! 不同环境下情况可能不同!)

2.使用resize后,将数组元素个数拓展到10个,由于给出了扩容后的默认值为100,因此数组会再增加5个值为100的元素。

3.使用reserve和shrink_to_fit函数改变了容积的大小。

注意:

1.无论是reserve还是shrink_to_fit,都不会导致数组容积小于元素个数这种情况发生,更不会改变数组内的元素情况

2.如果reserve和shrink_to_fit的参数n小于元素个数,编译器有两种选择,要么直接忽视,要么将容积缩小到略大于元素个数为止。并且,shrink_to_fit在遭遇这种情况时,还可能会导致vector数组元素空间的重新分配,但是并不影响数组的元素本身。

3.resize则可以改变元素个数,如果参数n小于当前元素个数,则会直接删除后续元素直到元素个数为n。

2.具体文档

capacity具体相关语法内容如下。

https://cplusplus.com/reference/vector/vector/capacity/

3.扩容说明

一般情况下,当vector的容积需要扩容时采取翻倍的方式,该倍数通常为2倍,3倍等。虽然扩展是自动的,但涉及到内存重新分配,因此建议提前使用 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

 五.vector元素的访问与修改操作

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)
 

2.具体文档

vector内元素的访问与修改接口具体内容如下:

https://cplusplus.com/reference/vector/vector/at/ 

3.修改操作

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

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

六.vector的迭代器与遍历操作

前言:

1. iterator指的就是迭代器,其本质相当于一个指针,for循环中的auto遍历操作与此原理相同。

2. 当我们使用不同容器的迭代器时,需要在前面注明不同容器的作用域以进行限定。

例如 :

vector<int> v(5,10);
vector::iterator it=v.begin();

1.常用示例

分析:

1.begin和end指的是分别返回容器首元素和尾元素

2.rbegin和rend则分别返回容器的尾元素和首元素,采取倒序遍历的操作,

注意:当倒叙遍历时,仍使用rbegin逐次++至rend的方式,且并不会使容器内的元素逆置。

3.crbegin和crend同上,只是返回的元素被const修饰。

#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
 

 2.使用for_each遍历

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

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
void print(int val)
{
	cout << val << " ";
}

int main()
{
	vector<int> v(5, 1);
	for_each(v.begin(), v.end(), print);
	cout << endl;
	return 0;

}

输出:

3.迭代器失效问题

1)失效原因与后果

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

2)常见导致失效的操作

扩容相关操作:当 vector 需要扩展容量时,会分配新的内存空间并将原有元素搬移到新的位置。此时,所有的迭代器将会失效。

示例:

#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 可能会分配新的内存空间,并释放原来的内存区域。这意味着之前的迭代器已指向失效的内存,因此在扩容操作后,必须重新获取迭代器

删除操作:删除操作会使指向被删除元素及其后续元素的迭代器失效。

示例如下:

#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;
}

分析:删除元素后原先地址失效,此时需要重新接收。如果继续使用原先的迭代器则会导致程序崩溃。

3)错误举例

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

错误示例:
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;
}

正确示例:

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. 首先需要在删除之后及时更新迭代器避免迭代器失效

2. erase若删除成功则会返回删除元素的下一个元素的地址,因此若删除成功则不需要++it,其余情况则需要++it进行遍历。

4)编译器对迭代器失效的处理差异

不同编译器(如 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)扩容后的迭代器失效问题

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

#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  // 错误输出
 

6)小结

避免迭代器失效:进行可能引发迭代器失效的操作(如扩容、删除等)后,必须重新获取迭代器,以保证程序的稳定性。


最佳实践:对于 erase() 操作,使用函数返回的迭代器继续遍历,以避免出现迭代器失效问题。


编译器差异:不同编译器(如 GCC 和 MSVC)对迭代器失效的处理方式不同,在开发跨平台程序时应尤为注意。

七.vector的插入,删除与修改

1.插入

vector提供多种方法用于插入操作。

使用 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
 

emplace_back() 与 push_back() 的区别:

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

使用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

2.删除

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

使用 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

使用 clear() 清空 vector

v.clear();
cout << "Vector size after clear: " << v.size() << endl;  // 输出:0

 具体文档:

https://cplusplus.com/reference/vector/vector/erase/

3.修改

通过迭代器或下标可以直接修改 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
 

总结:

本文主要介绍了vector的含义结构,相关接口的使用方法,以及迭代器的概念与使用,内附详细例子与具体文档参考链接,希望能对各位佬产生帮助,欢迎佬们前来斧正支持!!!

评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值