C++ STL vector基本原理和用法

server/2024/12/26 23:26:16/

文章目录

  • 基本原理
      • 1. 数据存储结构
      • 2. 内存管理机制
      • 3. 迭代器实现原理
      • 4. 元素访问原理
      • 5. 插入和删除元素原理
  • 常见用法
      • 1. 概述
      • 2. 包含头文件
      • 3. 定义和初始化
      • 4. 常用成员函数
      • 5. 迭代器
      • 6. 内存管理与性能特点
      • 7. 应用场景

基本原理

以下是关于 std::vector 的基本原理讲解:

1. 数据存储结构

std::vector 在内部通常是使用一段连续的内存空间来存储元素,这一点和普通的C风格数组类似。这种连续存储的方式使得它能够实现快速的随机访问,因为可以通过简单的指针运算(基于元素类型的大小)来定位到任意位置的元素,例如,对于一个存储 int 类型元素的 vector,如果知道首地址,要访问索引为 n 的元素,在内存中就是首地址加上 n * sizeof(int) 的偏移量。

2. 内存管理机制

  • 初始内存分配
    当创建一个 vector 时,它并不会立即分配很大的内存空间,通常会分配一个较小的初始容量(不同的标准库实现可能初始容量不同,有些可能是0,有些可能是一个较小的固定值,比如4或者8等)。例如,定义 std::vector<int> myVec; 时,它可能刚开始并没有分配实际可存储元素的内存块,只是进行了一些必要的内部状态初始化。

  • 扩容机制
    随着不断向 vector 中添加元素(通过 push_back 等操作),当元素个数达到当前容量上限时,vector 就需要进行扩容操作以容纳更多元素。扩容过程一般涉及以下几个步骤:

    • 新内存分配:它会按照一定的策略分配一块更大的连续内存空间,常见的扩容倍数是1.5倍或者2倍左右(具体倍数由不同的STL实现决定)。例如,当前 vector 的容量是4,已经存储了4个元素,当再调用 push_back 添加第5个元素时,如果按照2倍扩容策略,就会另外开辟一块能容纳8个元素的新内存空间。
    • 元素复制或移动:接着把原来内存空间中存储的所有元素复制或者移动(如果元素类型支持移动语义,在C++11及以后通常优先采用移动,以提高效率)到新分配的内存空间中。这一步涉及对每个元素的操作,如果元素类型的拷贝构造函数或者移动构造函数开销较大,那么扩容操作的整体开销也会相应增大。
    • 释放旧内存:在成功将元素迁移到新内存空间后,会释放原来的那块内存空间,以避免内存泄漏。
  • 缩容机制(可选)
    有些 vector 的实现提供了手动缩容或者在特定条件下自动缩容的机制,不过不是所有标准库都保证有自动缩容功能。比如,当调用 vectorclear 函数清空所有元素后,部分实现可能会自动释放多余的内存空间,将容量减小;也可以通过类似 shrink_to_fit 这样的函数(如果标准库支持)来显式要求 vector 释放多余内存,使其容量尽量接近当前元素个数,不过这个函数只是一个建议性的操作,具体是否能真正缩容以及缩容到何种程度还是依赖具体的实现。

3. 迭代器实现原理

vector 的迭代器本质上是一种类似指针的对象,它重载了一些必要的运算符(如 *(解引用)、++(前置和后置自增)、--(前置和后置自减)、!=(不等比较)等),使其能够像指针一样遍历 vector 中的元素。

  • 普通迭代器(beginend 等返回的迭代器)
    其内部通常就是直接持有指向 vector 中元素所在内存位置的指针。例如,begin 函数返回的迭代器指向 vector 的第一个元素所在内存地址,end 函数返回的迭代器指向最后一个元素后面的内存位置(形成左闭右开区间用于遍历等操作符合STL的通用约定)。当对迭代器进行自增操作时,就是按照元素类型的大小在内存中向后移动指针到下一个元素的位置,自减操作则相反,向前移动指针。

  • 反向迭代器(rbeginrend 等返回的迭代器)
    反向迭代器的实现相对复杂一些,它一般会对普通迭代器进行适配,内部通过一些机制来实现从后往前遍历的效果。实际上,它可以看作是对普通迭代器的一种“包装”,在自增、自减等操作的语义上进行了反转,使其能够从 vector 的末尾元素开始,向开头方向依次访问元素。

4. 元素访问原理

  • 下标访问(operator[]
    这个操作符重载的实现就是基于 vector 内部元素连续存储的特性,通过简单的指针算术运算来实现快速访问。它接收一个索引值作为参数,在内部会将这个索引值与元素类型的大小相乘(例如对于 int 类型就是乘以 sizeof(int)),然后加上首元素的地址(也就是 vector 内部管理的那块连续内存空间的起始地址),最后返回这个计算得到的内存地址对应的元素的引用,不过它不会进行边界检查,所以如果使用不当,访问越界就会导致未定义行为。

  • at 函数访问
    at 函数和 operator[] 类似也是用于访问元素,但它内部会首先进行边界检查,判断传入的索引值是否在合法的范围(即大于等于0且小于 vector 的大小)内,如果越界则会抛出 std::out_of_range 类型的异常,避免了因越界访问带来的潜在错误隐患,相对更加安全。

5. 插入和删除元素原理

  • 末尾插入(push_back
    在末尾添加元素时,如果当前 vector 还有剩余空间(即元素个数小于容量),那么直接在已有内存空间的末尾位置构造新元素即可,这个过程相对简单高效;但如果已经满容量需要扩容,就会触发上述的扩容流程后再添加元素。

  • 中间或开头插入(insert
    当要在 vector 的中间或者开头位置插入元素时,需要先将插入位置及之后的所有元素依次向后移动,为新插入的元素腾出空间,然后再在腾出的位置构造新元素。元素移动的过程同样涉及到对每个元素的拷贝或移动操作,所以在元素个数较多的 vector 中进行这类插入操作相对来说效率较低,时间复杂度是线性的 O(n),其中 nvector 的元素个数。

  • 删除元素(pop_backerase 等)
    pop_back 操作相对简单,只是销毁 vector 末尾的元素(调用其析构函数,如果有的话),然后将元素个数减1即可;而 erase 操作在删除指定位置的元素时,需要将该位置后面的所有元素依次向前移动来填补删除元素后留下的空位,同样涉及到多个元素的拷贝或移动,效率方面也会受 vector 元素个数影响,在中间或开头位置删除元素时间复杂度为 O(n)

总之,std::vector 通过巧妙的内存管理、迭代器设计以及对各种操作的底层实现,为开发者提供了一个方便、灵活且功能强大的动态数组容器,在了解其基本原理后能更好地掌握它的使用场景、性能特点以及避免一些因不当使用而可能出现的问题。 以下是对C++ STL(标准模板库)中vector的详细讲解:

常见用法

1. 概述

vector是C++ STL中非常常用的一个容器类,它提供了动态大小的数组功能,能够自动管理所存储元素的内存分配与释放,并且支持快速的随机访问。简单来说,你可以把它看作是一种可以根据需要自动扩容和缩容的数组,使用起来比普通的C风格数组更加方便、安全和灵活,广泛应用于各种需要存储和操作一组同类型元素的场景中。

2. 包含头文件

要使用vector容器,需要在代码中包含<vector>头文件,例如:

#include <vector>

3. 定义和初始化

  • 基本定义
    定义一个vector对象的语法形式通常为 std::vector<元素类型> 变量名;,这里的“元素类型”可以是C++中的各种基本数据类型(如intdoublechar等),也可以是自定义的结构体、类等类型。例如,定义一个存储整数的vector
std::vector<int> myVector;
  • 初始化方式
    • 默认初始化:像上面那样定义后,vector会被默认初始化为空,即其中不包含任何元素,其大小(通过size()成员函数获取)为0。
    • 指定初始大小和初始值:可以在定义时指定vector的初始大小以及每个元素的初始值,语法为 std::vector<元素类型> 变量名(元素个数, 初始值);。例如,创建一个包含5个初始值都为0的整数的vector
std::vector<int> anotherVector(5, 0);

此时,anotherVector的大小为5,并且每个元素的值都是0。
- 使用初始化列表初始化:利用花括号{}包裹元素来初始化vector,这种方式在C++11及以后的版本中更加常用和方便。例如:

std::vector<int> initListVector = {1, 2, 3};

这个vector包含了3个元素,分别是1、2和3。
- 从其他vector复制或移动初始化:可以通过已有的vector对象来初始化一个新的vector,实现复制或移动语义(如果适用)。例如:

std::vector<int> sourceVector = {1, 2, 3};
// 复制初始化
std::vector<int> copiedVector(sourceVector);
// 移动初始化(假设C++11及以上,利用std::move函数实现移动语义)
std::vector<int> movedVector(std::move(sourceVector));

复制初始化会创建一个新的vector,其元素与源vector完全相同;而移动初始化则是将源vector的资源(如内存空间等)转移给新的vector,源vector在移动后通常处于一种有效但已转移资源的特殊状态(比如大小变为0等)。

4. 常用成员函数

  • 添加和删除元素相关函数
    • push_back():用于在vector的末尾添加一个元素。例如:
std::vector<int> numbers;
numbers.push_back(5);  // 在末尾添加整数5
numbers.push_back(10); // 再添加整数10,此时numbers包含两个元素,分别是5和10
- **`pop_back()`**:与`push_back()`相反,它用于删除`vector`末尾的一个元素。例如:
std::vector<int> nums = {1, 2, 3};
nums.pop_back();  // 删除末尾的元素3,此时nums包含元素1和2
- **`insert()`**:可以在指定位置插入一个或多个元素。它有多种重载形式,常见的一种是 `iterator insert(iterator position, const T& value);`,其中`position`是指向插入位置的迭代器(可以通过`begin()`、`end()`等函数获取迭代器来指定位置),`value`是要插入的元素值。例如:
std::vector<int> vec = {1, 3};
auto it = vec.begin() + 1;  // 指向索引为1的位置(也就是元素3所在位置)
vec.insert(it, 2);  // 在索引为1的位置插入元素2,此时vec变为{1, 2, 3}

还可以一次插入多个相同元素,比如 iterator insert(iterator position, size_type n, const T& value); 形式可以在指定位置插入n个相同的value元素。
- erase():用于删除指定位置的一个或多个元素,同样有多种重载形式。例如,删除单个元素可以这样用:iterator erase(iterator position);,删除一段元素区间可以用 iterator erase(iterator first, iterator last);,其中firstlast分别是要删除区间的起始和结束迭代器(左闭右开区间)。例如:

std::vector<int> v = {1, 2, 3, 4};
auto it = v.begin() + 1;  // 指向元素2
v.erase(it);  // 删除元素2,此时v变为{1, 3, 4}

删除一段元素示例:

std::vector<int> ve = {1, 2, 3, 4, 5};
auto start = ve.begin() + 1;  // 指向元素2
auto end = ve.begin() + 3;  // 指向元素4后面的位置(左闭右开区间概念)
ve.erase(start, end);  // 删除元素2和3,此时ve变为{1, 4, 5}
  • 访问元素相关函数
    • at():用于安全地访问vector中的元素,它会进行边界检查,如果访问越界会抛出 std::out_of_range 异常。语法为 reference at(size_type n);,其中n是要访问元素的索引(从0开始计数)。例如:
std::vector<int> values = {10, 20, 30};
try {int element = values.at(1);  // 获取索引为1的元素,即20std::cout << element << std::endl;
} catch (const std::out_of_range& e) {std::cerr << "访问越界: " << e.what() << std::endl;
}
- **`operator[]`(下标运算符)**:也用于访问元素,它与数组的下标访问方式类似,但不会进行边界检查,如果访问越界会导致未定义行为,所以使用时要确保索引在合法范围内。例如:
std::vector<int> data = {5, 10};
int element = data[0];  // 获取第一个元素5,注意这里没有边界检查,需谨慎使用
- **`front()`**:返回`vector`的第一个元素的引用,可以用于获取或修改第一个元素的值。例如:
std::vector<int> firstVector = {1, 2};
int firstElement = firstVector.front();  // 获取第一个元素1
firstVector.front() = 10;  // 将第一个元素修改为10,此时firstVector变为{10, 2}
- **`back()`**:与`front()`相对,返回`vector`的最后一个元素的引用,同样可用于获取或修改最后一个元素的值。例如:
std::vector<int> lastVector = {3, 4};
int lastElement = lastVector.back();  // 获取最后一个元素4
lastVector.back() = 40;  // 将最后一个元素修改为40,此时lastVector变为{3, 40}
  • 获取容器信息相关函数
    • size():返回vector中当前元素的个数,返回值类型是 std::vector<元素类型>::size_type(通常是无符号整数类型)。例如:
std::vector<int> sizeVector = {1, 2, 3};
std::vector<int>::size_type elementCount = sizeVector.size();  // 获取元素个数,这里elementCount的值为3
- **`capacity()`**:返回`vector`当前已经分配的内存空间能够容纳的元素个数,也就是它的容量。一般来说,随着不断向`vector`中添加元素,当元素个数接近容量时,`vector`会自动进行扩容操作来分配更多的内存空间。例如:
std::vector<int> capVector;
std::cout << "初始容量: " << capVector.capacity() << std::endl;  // 初始容量可能为0或者一个较小的值,取决于实现
capVector.push_back(1);
capVector.push_back(2);
std::cout << "添加元素后的容量: " << capVector.capacity() << std::endl;  // 容量可能已经自动扩容了
- **`empty()`**:判断`vector`是否为空,如果为空(即元素个数为0)则返回`true`,否则返回`false`。例如:
std::vector<int> emptyVector;
if (emptyVector.empty()) {std::cout << "该vector为空" << std::endl;
}
std::vector<int> nonEmptyVector = {1};
if (!nonEmptyVector.empty()) {std::cout << "该vector不为空" << std::endl;
}
  • 其他常用函数
    • clear():用于清空vector中的所有元素,将其大小变为0,但不会释放已分配的内存空间(容量可能不会改变,除非后续有添加或删除元素等操作触发了内存重新分配)。例如:
std::vector<int> clearVector = {1, 2, 3};
clearVector.clear();
if (clearVector.empty()) {std::cout << "已清空vector,现在为空" << std::endl;
}
- **`resize()`**:可以改变`vector`的大小,如果新大小大于当前大小,会根据元素类型的默认构造函数(对于内置类型可能会进行未定义初始化,对于类类型会调用默认构造函数)或者指定的初始值来填充新增的元素;如果新大小小于当前大小,则会删除多余的元素。例如:
std::vector<int> resizeVector(3, 1);  // 初始化为{1, 1, 1}
resizeVector.resize(5, 0);  // 扩大为{1, 1, 1, 0, 0},新增元素初始化为0
resizeVector.resize(2);  // 缩小为{1, 1},删除后面的元素

5. 迭代器

vector支持迭代器来遍历容器中的元素,迭代器就像是指向容器中元素的指针,可以通过它来依次访问每个元素。常用的迭代器相关操作有:

  • begin()end()begin()返回指向vector第一个元素的迭代器,end()返回指向vector最后一个元素后面位置的迭代器(形成一个左闭右开的区间概念,用于循环遍历等操作符合STL的通用设计模式)。例如,使用迭代器遍历vector的常见方式如下:
std::vector<int> iterVector = {1, 2, 3};
for (auto it = iterVector.begin(); it!= iterVector.end(); ++it) {std::cout << *it << " ";  // 输出每个元素,*it用于解引用迭代器获取元素值
}
// 输出: 1 2 3
  • rbegin()rend():这两个函数分别返回反向迭代器,用于从后往前遍历vectorrbegin()指向最后一个元素,rend()指向第一个元素前面的位置(同样是左闭右开区间概念)。例如:
std::vector<int> reverseIterVector = {4, 5, 6};
for (auto rit = reverseIterVector.rbegin(); rit!= reverseIterVector.rend(); ++rit) {std::cout << *rit << " ";  // 从后往前输出元素,这里输出: 6 5 4
}

6. 内存管理与性能特点

  • 内存自动管理vector会自动处理内存的分配和释放,当添加元素导致当前分配的内存空间不足时,它会自动进行扩容操作,通常是按照一定的倍数(不同的STL实现可能有不同的扩容策略,常见的是1.5倍或2倍等)分配新的更大的内存空间,然后将原有元素复制或移动到新空间中,并释放原来的内存。这种自动管理机制方便了开发者,但也可能在频繁扩容时带来一定的性能开销(尤其是元素类型的拷贝或移动成本较高时)。
  • 随机访问性能好:由于vector内部元素在内存中是连续存储的,类似于普通的数组,所以它支持快速的随机访问,通过下标运算符或者迭代器解引用访问元素的时间复杂度为常数时间 O(1)
  • 插入和删除元素的性能特点:在vector末尾添加或删除元素(通过push_back()pop_back())的操作通常比较高效,时间复杂度接近常数时间(平均情况,不考虑偶尔的扩容操作带来的影响);但在中间或者开头位置插入或删除元素(通过insert()erase())可能相对较慢,因为需要移动插入或删除位置后面的所有元素,时间复杂度为线性时间 O(n),其中nvector中元素的个数。

7. 应用场景

  • 存储一组同类型的数据:比如存储游戏中的玩家分数列表、学生的成绩数组等,方便进行数据的添加、删除、修改以及遍历统计等操作。
  • 作为函数参数传递数组:在函数间传递数组时,如果数组大小不确定或者可能会变化,使用vector作为参数传递要比传递普通C风格数组更加方便、安全,避免了传递数组长度等额外参数以及数组越界等风险。例如:
void processVector(const std::vector<int>& vec) {// 在这里可以安全地遍历vec等操作,不用担心越界问题for (int element : vec) {std::cout << element << " ";}
}
int main() {std::vector<int> data = {1, 2, 3};processVector(data);return 0;
}
  • 配合其他STL算法使用vector可以很好地与STL中的各种算法(如排序算法std::sort、查找算法std::find等)配合使用,实现更复杂的数据处理功能。例如:
#include <algorithm>
std::vector<int> numbers = {5, 3, 1, 4, 2};
std::sort(numbers.begin(), numbers.end());  // 对vector中的元素进行排序

总之,vector是C++ STL中一个功能强大、使用方便的容器,掌握它的各种特性和使用方法对于高效地进行C++编程非常有帮助,能够满足很多常见的动态数组相关的编程需求。


http://www.ppmy.cn/server/153452.html

相关文章

全国硕士研究生入学考试(考研)常识详解之复试考试科目:笔试、面试与加试

全国硕士研究生入学考试&#xff08;考研&#xff09;常识详解之复试考试科目&#xff1a;笔试、面试与加试 硕士研究生入学考试的复试是对考生进行全面评估的重要环节&#xff0c;旨在考察考生的专业知识、综合素质及科研潜力。复试主要包括笔试与面试两大核心部分&#xff0…

光谱相机的工作原理

光谱相机的工作原理主要基于不同物质对不同波长光的吸收、反射和透射特性存在差异&#xff0c;以下是其具体工作过程&#xff1a; 一、光的收集 目标物体在光源照射下&#xff0c;其表面会对光产生吸收、反射和透射等相互作用。光谱相机的光学系统&#xff08;如透镜、反射镜…

Linux下的MySQL:表的增删查改

目录 插入 1.全列插入 2.指定列插入 3.多行插入 4.插入否则更新 查询 1.全列查询 2.查询指定列 3.查询表达式 4.对查询的结果去重 5.where条件 结果排序 筛选分页结果 更新 ​删除 删除整表 截断表 插入 创建一张学生表 1.全列插入 、 2.指定列插入 3.多行插…

如何用pS制圣诞C4D效果海报

工具&#xff1a;STARTAI 网址&#xff1a;StartAI画图软件官网_PS插件StartAI绘画软件生成器_Photoshop图像处理插件 咒语&#xff1a;C4D style, isometric perspective, mid view, 2.5D, Open the gift box, inside there is a miniature Christmas scene bedroom, Christ…

STM32 高级 物联网通信之CAN通讯

目录 CAN通讯介绍 物理层 协议层 CAN的帧(报文)种类 1 数据帧(发送单元->接受单元) 2 远程帧(接受单元->发送单元) 3 错误帧(发送方发送数据错误会发送的状态帧) 4 过载帧(接收方放不下会发送到的状态帧) 5 帧间隔(状态) 数据帧介绍 远程帧介绍 C…

解决 fatal: detected dubious ownership in repository at ‘XXXX‘ 问题

重装系统后&#xff0c;重新下载git之后&#xff0c;出现了这个问题&#xff1a; fatal: detected dubious ownership in repository at V:/Web/Github_commit/Interview这个错误表明 Git 检测到仓库的所有者和当前用户不一致&#xff0c;这可能被视为一个安全风险。要解决这…

(八)循环神经网络_门控循环单元GRU

一、提出背景 2014年提出&#xff0c;主要针对LSTM模型计算比较复杂容易出现梯度消失或爆炸的问题进行改进。 二、与LSTM的区别 三、网络结构 1. 整体结构 2. 重置门 3. 更新门 4. 候选隐状态 5. 隐状态 四、总结

『Linux学习笔记』FRPC 详细介绍及配置解析!

『Linux学习笔记』FRPC 详细介绍及配置解析&#xff01; 文章目录 一. FRPC 详细介绍及配置解析FRPC 的主要功能FRPC 配置文件解析全局配置代理配置第一个代理服务第二个代理服务 配置文件整体工作流程常见配置项说明FRPC 的使用步骤注意事项结论 二. 参考文献 一. FRPC 详细介…