C++ vector

1. 引言

1.1 vector的重要性及其在C++标准库中的地位

std::vector 是C++标准库中最常用和最重要的容器之一。它是一个动态数组,可以在运行时根据需要自动调整其大小。vector 提供了一种方便、高效的方法来管理连续存储的数据,使得开发者能够轻松处理动态大小的数据集合。

std::vector 的重要性

  • 动态大小std::vector 能够根据元素的增加或删除自动调整其大小。与C语言中的数组相比,vector 不需要开发者手动管理内存,也不会导致缓冲区溢出或内存泄漏等问题。

  • 连续内存布局std::vector 在内存中存储元素时采用连续的内存块。这不仅提高了缓存的效率,还支持快速的随机访问(O(1)时间复杂度),使其适合需要高效索引的场景。

  • 丰富的功能std::vector 提供了丰富的成员函数,可以轻松地对数据进行插入、删除、排序等操作。它还支持迭代器,使得vector 与标准模板库(STL)的其他组件无缝集成。

  • 异常安全性std::vector 在进行增删操作时,如果发生异常(例如内存分配失败),它会自动回滚到操作之前的状态,从而确保程序的稳定性。

  • 强大的扩展性std::vector 可以用于不同的数据类型(包括自定义类型),并且能够通过指定分配器来定制其内存管理策略。这种灵活性使vector 能够适应各种复杂的应用场景。

std::vector 在C++标准库中的地位

std::vector 是C++标准库中的核心容器之一,具有如下关键地位:

  1. 标准库的默认选择:在大多数需要动态数组的场景下,std::vector 是默认的选择。它的设计使得它可以在绝大多数情况下替代C风格的数组,同时提供了更好的安全性和功能。

  2. 广泛使用:由于std::vector 的灵活性和高效性,它在C++程序中被广泛使用。无论是小型项目还是大型系统,std::vector 都是处理动态数据的首选。

  3. 支持泛型编程std::vector 是一个模板类,可以存储任何类型的对象。这使得它在泛型编程中发挥了重要作用,可以与其他STL算法和容器互操作。

  4. 性能优化:由于std::vector 在内存管理和访问速度上的优势,它在性能敏感的应用中也非常流行。此外,标准库对vector 的实现通常进行了高度优化,确保了其在不同平台上的高效性。

总的来说,std::vector 是C++标准库中不可或缺的一部分,其功能的丰富性、性能的高效性以及易用性使其成为处理动态数据的最佳工具之一。

1.2 为什么选择使用vector而不是传统的数组

选择使用std::vector而不是传统的C风格数组主要基于以下几个原因:

1. 动态大小管理

传统的C数组大小在声明时必须固定,无法在运行时调整。如果需要动态大小的数组,必须手动管理内存,这容易导致内存泄漏或缓冲区溢出等问题。std::vector 自动管理其大小,根据需要动态分配和释放内存,消除了手动管理内存的复杂性。

2. 内存安全性

std::vector 提供内存安全性。通过RAII(资源获取即初始化)机制,std::vector 确保在作用域结束时自动释放内存,避免了传统数组中常见的内存泄漏问题。此外,std::vector 的边界检查功能(在使用at方法时)可以防止越界访问。

3. 方便的接口

std::vector 提供了丰富的成员函数,如push_backinserterase等,使得元素的增删操作更加直观和简洁。相比之下,传统数组需要手动计算索引和内存偏移来进行类似操作,容易出错且代码冗长。

4. 异常安全性

在使用std::vector 时,如果发生异常(如内存分配失败),vector 会自动回滚到操作前的状态,保证数据的一致性。而传统数组在异常情况下,可能会导致内存泄漏或未定义行为。

6. 与标准库的兼容性

std::vector 是C++标准模板库(STL)的一部分,可以无缝与STL算法(如sortfind等)和其他容器(如mapset)结合使用。传统数组在与STL算法结合时,需要手动转换为指针范围或其他容器形式。

2. vector的基本概念

2.1 vector的定义

单独定义一个vector:

vector < typename > name;

上面这个定义其实相当于是一维数组name[size],只不过其size可以根据需要进行变化,这就是“变长数组”的名字的由来。

这里的typename可以是任何基本类型,例如int、double、char、结构体等,也可以是STL标准容器,例如set、queue、vector等。

下面我们以int为例子介绍vector的几种定义方式:

v1:一个空的vector。

vector < int > v1

v2:这个vector中包含10个int,并且默认初始化为0。

vector < int > v2(10);

代码示例:

#include "iostream"
#include "vector"

using namespace std;

int main(){
    vector <int> v2(10);
  
    cout << "v2: ";
    for(int i = 0; i < v2.size(); i++){
        cout  << v2[i]<<" ";
    }

    return 0;
}

运行结果:

v2: 0 0 0 0 0 0 0 0 0 0 
进程已结束,退出代码为 0

v3:这个vector中包含10个int,并且手动初始化为1。

vector < int > v3(10,1);

代码示例:

#include "iostream"
#include "vector"

using namespace std;

int main(){
    vector <int> v3(10,1);

    cout << "v3: ";
    for(int i = 0; i < v3.size(); i++){
        cout  << v3[i]<<" ";
    }


    return 0;
}

运行结果:

v3: 1 1 1 1 1 1 1 1 1 1 
进程已结束,退出代码为 0

v4:这个vector中采用v2迭代器区间进行开空间和初始化,并且空间大小和初始化内容与v2相同,即v4包含10个int,初始化为0。

vector < int > v4(v2.begin(),v2,end())

代码示例:

#include "iostream"
#include "vector"

using namespace std;

int main(){
    vector <int> v2(10);

    vector <int> v4(v2.begin(), v2.end());

    cout << "v4: ";
    for(int i = 0; i < v4.size(); i++){
        cout  << v4[i]<<" ";
    }


    return 0;
}

运行结果:

v4: 0 0 0 0 0 0 0 0 0 0 
进程已结束,退出代码为 0

v5:这个vector中采用拷贝构造初始化,并且空间大小和初始化内容与v3相同,即v5包含10个int,初始化为1。

vector < int > v5(v3)

代码示例:

#include "iostream"
#include "vector"

using namespace std;

int main(){
    vector <int> v3(10,1);

    vector <int> v5(v3);

    cout << "v5: ";
    for(int i = 0; i < v5.size(); i++){
        cout  << v5[i]<<" ";
    }


    return 0;
}

运行结果:

v5: 1 1 1 1 1 1 1 1 1 1 

进程已结束,退出代码为 0

v6:这个vector中采用了初始化列表初始化,{}中有几个元素就代表开了几个空间,{}中内容表示初始化的值。这是c++11中的语法。

vector < int >v6{1,2,3,4};

代码示例:

#include "iostream"
#include "vector"

using namespace std;

int main(){

    vector <int> v6{1,2,3,4};

    cout << "v6: ";
    for (int num:v6){
        cout  << num <<" ";
    }
    cout << endl;
    
    return 0;
}

运行结果:

v6: 1 2 3 4 

进程已结束,退出代码为 0

2.2 vector中元素的访问

(1)使用at进行访问,at访问不到元素是抛异常。

cout << v6.at(1) << endl;

代码示例:

#include "iostream"
#include "vector"

using namespace std;

int main(){

    vector <int> v6{1,2,3,4};


    cout << "v6: ";
    for(int i = 0; i < v6.size(); i++){
        cout  << v6.at(i)<<" ";
    }
    cout << endl;

    return 0;
}

结果展示:

v6: 1 2 3 4 

进程已结束,退出代码为 0

(2)像数组一样进行访问。

cout << v6[0] << endl;

代码示例:

#include "iostream"
#include "vector"

using namespace std;

int main(){

    vector <int> v6{1,2,3,4};


    cout << "v6: ";
    for(int i = 0; i < v6.size(); i++){
        cout  << v6[i]<<" ";}
    cout << endl;

    return 0;
}

运行结果:

v6: 1 2 3 4 

进程已结束,退出代码为 0

(3)vecctor::begin() 指向第一个元素

std::vector<int>::iterator it = numbers.begin();

代码示例:

#include <iostream>
#include <vector>

int main() {
    // 创建一个存储整数的 vector
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用 vector::begin() 获取第一个元素的迭代器
    std::vector<int>::iterator it = numbers.begin();
    //std::vector<int>::iterator 是一个类型定义,用于表示 std::vector<int> 的迭代器类型。

    // 输出第一个元素
    std::cout << "The first element is: " << *it << std::endl;

    // 使用迭代器遍历整个 vector
    std::cout << "All elements in the vector: ";
    for (it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

运行结果:

The first element is: 1
All elements in the vector: 1 2 3 4 5 

进程已结束,退出代码为 0

vecctor::rbegin() 反向迭代器,指向元素最后一个元素

代码示例:

#include <iostream>
#include <vector>

int main() {
    // 创建一个存储整数的 vector
    std::vector<int> numbers = {10, 20, 30, 40, 50};

    // 使用 vector::end() 获取尾后迭代器
    std::vector<int>::reverse_iterator it = numbers.rbegin();

    std::cout << "numbers.rbegin()获取的是:" << *it << std::endl;

    return 0;
}

运行结果是:

numbers.rbegin()获取的是:50

进程已结束,退出代码为 0

vecctor::rend() 反向迭代器,指向容器第一个元素的前一个位置。

作用1:获取第一个元素;

代码示例:

#include <iostream>
#include <vector>

int main() {
    // 创建一个存储整数的 vector
    std::vector<int> numbers = {10, 20, 30, 40, 50};

    // 使用 vector::end() 获取尾后迭代器
    
    std::vector<int>::reverse_iterator it2 = numbers.rend() - 1;

    std::cout << "numbers.rend()获取的是:" << *it2 << std::endl;

    return 0;
}

结果展示:

numbers.rend()获取的是:10

进程已结束,退出代码为 0

作用2:和rbegin()一起使用反向遍历 vector

代码示例:

#include <iostream>
#include <vector>

int main() {
    // 创建一个存储整数的 vector
    std::vector<int> numbers = {10, 20, 30, 40, 50};

    // 使用反向迭代器遍历整个 vector
    std::cout << "Elements in the vector in reverse order: ";
    for (std::vector<int>::reverse_iterator rit = numbers.rbegin(); rit != numbers.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    return 0;
}

结果展示:

Elements in the vector in reverse order: 50 40 30 20 10 

进程已结束,退出代码为 0

2.3 vector中空间增长

在 C++ 中,std::vector 是一种动态数组,它可以在需要时自动调整其大小以容纳更多的元素。向量的这种自动增长机制涉及到空间的管理,尤其是容量的调整,这也是 std::vector 高效运作的关键之一。

空间增长机制

1. 容量与大小的区别
  • 大小(size):当前向量中实际存储的元素数量,可以通过 vector::size() 函数获得。
  • 容量(capacity):向量当前能够存储的最大元素数量,而无需进行重新分配,可以通过 vector::capacity() 函数获得。

当向量的大小超出其当前容量时,向量会自动增加容量以容纳更多的元素。这种容量的增加通常会比所需的元素数量大,从而减少频繁的内存分配开销。

2. 增长策略

std::vector 的增长策略一般是按倍数(如 1.5 倍或 2 倍)来增加容量。具体的增长因实现而异,但常见的增长方式是:

  • 当当前容量为 0 时,分配一个初始的容量。
  • 当向量的大小超过容量时,容量通常会增加到当前容量的 1.5 倍或 2 倍。

这种按倍数增长的策略旨在平衡内存分配的开销和向量扩展时的效率。尽管会导致一些未使用的空间,但它大大减少了重新分配和复制的频率。

3. 重新分配(Reallocation)

当向量的容量不足时,会触发重新分配。重新分配过程包括以下步骤:

  • 分配一个更大的内存块(通常是当前容量的 1.5 倍或 2 倍)。
  • 将现有元素复制到新内存块中。
  • 释放旧的内存块。

这个过程可能会导致所有现有的迭代器、引用和指针失效,因为底层存储的位置已经改变。

代码示例

以下代码演示了向量的空间增长:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;
    std::cout << "Initial capacity: " << vec.capacity() << std::endl;

    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
        std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
    }

    return 0;
}

输出示例

Initial capacity: 0
Size: 1, Capacity: 1
Size: 2, Capacity: 2
Size: 3, Capacity: 4
Size: 4, Capacity: 4
Size: 5, Capacity: 8
Size: 6, Capacity: 8
Size: 7, Capacity: 8
Size: 8, Capacity: 8
Size: 9, Capacity: 16
Size: 10, Capacity: 16

从输出中可以看出,当 size 达到 capacity 时,向量的容量会自动翻倍(或按其他倍数增长)。例如,从容量 4 增加到 8,再从 8 增加到 16。

手动控制容量

C++ 标准库提供了一些方法来手动控制向量的容量:

  • reserve():预先分配足够的内存,以减少后续的重新分配。例如,vec.reserve(100) 会确保 vec 能够存储至少 100 个元素,而不进行中途的重新分配。

  • shrink_to_fit():请求缩小向量的容量,使其与当前大小相匹配。这是一个非强制性操作,取决于实现。

性能考虑

空间增长的设计旨在权衡性能与内存使用:

  • 性能:通过倍增策略,减少了内存分配的频率,提升了 push_back() 等操作的平均性能。
  • 内存效率:由于按倍数增长,向量可能会预留一些未使用的内存。这在内存敏感的环境中需要注意。

总结

std::vector 的空间增长机制是其高效动态行为的基础。理解这个机制对于优化向量的使用、避免不必要的重新分配开销、以及在需要时手动调整容量是非常重要的。

2.4 vector中增删查改

2.4.1. 增加元素(Insert)

std::vector 提供了几种方法来向向量中添加元素:

push_back()

将元素添加到向量的末尾。这个操作会自动调整向量的大小,如果容量不足,可能会触发重新分配

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec;

    vec.push_back(10); // 添加元素10到末尾
    vec.push_back(20);
    
    for (int i : vec) {
        std::cout << i << " "; // 输出: 10 20
    }

    return 0;
}
emplace_back()

push_back() 类似,但它直接在向量末尾构造元素,避免了临时对象的创建。

std::vector<std::pair<int, int>> vec;
vec.emplace_back(1, 2);  // 直接构造pair(1, 2)到向量中
insert()

在指定位置插入一个或多个元素,可能会导致重新分配和元素的移动。

std::vector<int> vec = {10, 20, 30};
vec.insert(vec.begin() + 1, 15);  // 在第二个位置插入15

// vec内容: 10, 15, 20, 30
emplace()

insert() 类似,但直接在指定位置构造元素,避免了临时对象的创建。

vec.emplace(vec.begin() + 1, 15); // 直接在第二个位置构造15

2.4.2. 删除元素(Erase)

pop_back()

移除向量末尾的元素,不会返回该元素。

std::vector<int> vec = {10, 20, 30};
vec.pop_back(); // 移除30

// vec内容: 10, 20
erase()

移除指定位置或范围内的元素,并调整后续元素的位置。

std::vector<int> vec = {10, 20, 30, 40};
vec.erase(vec.begin() + 1); // 移除第二个元素

// vec内容: 10, 30, 40

vec.erase(vec.begin(), vec.begin() + 2); // 移除前两个元素

// vec内容: 40
clear()

移除所有元素,将向量大小变为0,但不改变容量。

std::vector<int> vec = {10, 20, 30};
vec.clear(); // 移除所有元素

// vec内容: 空

2.4.3. 修改元素(Modify)

 通过索引修改

使用下标操作符 []at() 来访问并修改元素。

std::vector<int> vec = {10, 20, 30};
vec[1] = 25; // 修改第二个元素为25

// vec内容: 10, 25, 30

vec.at(2) = 35; // 修改第三个元素为35

// vec内容: 10, 25, 35

2.4.4. 查询元素(Access)

通过索引访问

使用下标操作符 []at() 来访问元素。at() 提供边界检查,如果索引超出范围会抛出 std::out_of_range 异常。

std::vector<int> vec = {10, 20, 30};
int value1 = vec[1]; // 获取第二个元素20
int value2 = vec.at(2); // 获取第三个元素30
front()back()
  • front() 返回第一个元素的引用。
  • back() 返回最后一个元素的引用。
std::vector<int> vec = {10, 20, 30};
int first = vec.front(); // 获取第一个元素10
int last = vec.back();   // 获取最后一个元素30
data()

返回指向向量内存数组的指针,便于与 C 风格数组或其他需要指针的 API 交互。

std::vector<int> vec = {10, 20, 30};
int* p = vec.data(); // 指向第一个元素的指针

std::cout << *p << std::endl; // 输出: 10
 遍历向量

可以使用传统的 for 循环、范围 for 循环或迭代器来遍历向量的元素。

std::vector<int> vec = {10, 20, 30};

// 使用传统for循环
for (size_t i = 0; i < vec.size(); ++i) {
    std::cout << vec[i] << " ";
}

// 使用范围for循环
for (int value : vec) {
    std::cout << value << " ";
}

// 使用迭代器
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " ";
}

总结

  • :使用 push_back()emplace_back() 追加元素;使用 insert()emplace() 在指定位置插入元素。
  • :使用 pop_back() 移除最后一个元素;使用 erase() 移除指定位置的元素;使用 clear() 清空向量。
  • :通过索引或使用 front()back() 直接访问并修改元素。
  • :使用 []at() 访问元素;使用 front()back() 访问首尾元素;使用 data() 获取底层数组指针。

这些操作使 std::vector 成为一个功能强大且灵活的容器,可以应对各种应用场景。

3. vector的常用成员函数

push_back()pop_back():添加与移除元素。

  • operator[]at():访问元素。
  • begin()end():迭代器的使用。
  • size()capacity():大小与容量的区别。
  • resize()reserve():调整大小与容量管理。
  • empty()clear():清空vector
  • insert()erase():插入与移除元素。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值