从零开始的C++学习生活 7:vector的入门使用

个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

目录

前言

[1. vector简介](#1. vector简介)

[1.1 什么是vector?](#1.1 什么是vector?)

[1.2 为什么选择vector?](#1.2 为什么选择vector?)

传统数组的局限性

vector的优势

[2. vector的基本使用](#2. vector的基本使用)

[2.1 构造vector](#2.1 构造vector)

[2.2 迭代器使用](#2.2 迭代器使用)

[2.3 容量管理](#2.3 容量管理)

[2.4 元素访问](#2.4 元素访问)

[2.5 修改操作](#2.5 修改操作)

[3. 迭代器失效问题(重点)](#3. 迭代器失效问题(重点))

[3.1 导致迭代器失效的操作](#3.1 导致迭代器失效的操作)

扩容操作

插入操作

删除操作

正确示例:

[4. vector的模拟实现](#4. vector的模拟实现)

[4.1 基础框架](#4.1 基础框架)

[4.2 深拷贝问题](#4.2 深拷贝问题)

[4.3 完整实现关键函数](#4.3 完整实现关键函数)

[5. 性能优化建议](#5. 性能优化建议)


上一篇:从零开始的C++学习生活 6:string的入门使用-CSDN博客

前言

在前面我们学过了c++中的string,一个c++内置的处理字符串的类。不仅于此,c++还内置了很多像string的容器,如vector,list,stack和queue。

而今天我们要学习的是vector,即顺序表(数组),同样内置了各种我们所需的功能,无论是实战项目还是oj题都十分重要。

本篇博客将带你梳理一遍vector常用的功能。

1. vector简介

1.1 什么是vector?

vector是C++标准库中的一个序列容器,它封装了动态大小数组的功能。主要特点包括:

  • 动态扩容:根据需要自动调整容量

  • 随机访问:支持通过下标直接访问元素,时间复杂度O(1)

  • 连续存储:元素在内存中连续存放,缓存友好

  • 类型安全:模板化设计,编译时类型检查

1.2 为什么选择vector?

传统数组的局限性

cpp 复制代码
int arr[10];  // 固定大小,无法动态调整
arr[10] = 5;  // 可能越界,危险!

传统数组存在数组越界的风险,并且不够智能。无法自由在里面插入数据,删除数据并保持元素顺序连续,即无法动态插入数据。

虽然我们学习数据结构时实现了自己的顺序表,但在题目当中难道还要自己手手搓一个吗?显然不现实

vector的优势

随时随用

vector是集成在c++中的一个容器,只需要包含头文件即可使用

cpp 复制代码
#include <vector>

功能丰富

vector内置顺序表中几乎所有需要用到的功能,例如增删查改

2. vector的基本使用

2.1 构造vector

vector的构造和string基本一致

cpp 复制代码
#include <vector>
using namespace std;

// 1. 默认构造 - 空vector
vector<int> v1;

// 2. 构造包含n个val的vector
vector<int> v2(5, 10);  // {10, 10, 10, 10, 10}

// 3. 拷贝构造
vector<int> v3(v2);     // 与v2相同

// 4. 使用迭代器范围构造
int arr[] = {1, 2, 3, 4, 5};
vector<int> v4(arr, arr + 5);  // {1, 2, 3, 4, 5}

// 5. 初始化列表构造 (C++11)
vector<int> v5 = {1, 2, 3, 4, 5};

2.2 迭代器使用

vector的迭代器可以简单看作是数据类型的指针,如果是vector<int>那么迭代器是int*

cpp 复制代码
void Print()
{
    iterator it = begin();
    while (it!=_finish)
    {
        cout << *it << " ";
        it++;
    }
    cout << endl << "size:" << size() << " " << "capacity:" << capacity() << endl;
}

2.3 容量管理

容量空间 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize 改变vector的size
reserve 改变vector的capacity

对于vector的reserve(设置容量)规则是:

如果 n 大于当前容量,则该函数会导致容器重新分配其存储,将其容量增加到 n (或更大)。

在所有其他情况下(即n小于或等于当前容量),函数调用不会导致重新分配,并且容量不受影响。

对于vector的resize(设置有效数据个数)规则是:

如果 n 小于当前容器大小,则内容将减少到其前 n 个 元素,删除超出的元素(并销毁它们)。

如果 n 大于当前容器大小,则通过在末尾插入所需数量的元素来扩展内容,以达到 n 的大小。如果指定了 val ,则新元素将初始化为 val 的副本,否则,它们将被值初始化。

如果 n 也大于当前容器容量,则会自动重新分配已分配的存储空间。

cpp 复制代码
vector<int> v;

cout << v.size();        // 元素个数: 0
cout << v.capacity();    // 总容量: 0
cout << v.empty();       // 是否为空: true

v.reserve(100);          // 预留100个元素空间
cout << v.capacity();    // 容量变为100

v.resize(10, 5);         // 调整大小为10,新增元素初始化为5
cout << v.size();        // 元素个数: 10

2.4 元素访问

元素访问 接口说明
operator[] 数组下标访问
front 数组第一个元素
back 数组最后一个元素
data 数组地址
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};

// 下标访问
cout << v[0];           // 1 (不检查边界)

// 首尾元素访问
cout << v.front();      // 1
cout << v.back();       // 5

// 数据指针
int* data = v.data();   // 获取底层数组指针

2.5 修改操作

vector增删查改 接口说明
push_back(重点) 尾插
pop_back(重点) 尾删
find 查找。(注意这个是算法模块实现,不是vector的成员接口)
insert 在position之前插入val
erase 删除position位置的数据
swap 交换两个vector的数据空间
cpp 复制代码
vector<int> v = {1, 2, 3};

// 尾部操作
v.push_back(4);         // {1, 2, 3, 4}
v.pop_back();           // {1, 2, 3}

// 插入操作
v.insert(v.begin() + 1, 10);  // {1, 10, 2, 3}

// 删除操作
v.erase(v.begin() + 1);       // {1, 2, 3}
v.erase(v.begin(), v.begin() + 2); // {3}


// 交换
vector<int> v2 = {4, 5, 6};
v.swap(v2);             // 交换两个vector的内容

find(v.begin(),v.end(),2)//algorithm库中的函数,查找对应值的数据

3. 迭代器失效问题(重点)

迭代器失效是使用vector时最容易出错的地方,必须特别重视。

3.1 导致迭代器失效的操作

扩容操作

cpp 复制代码
vector<int> v = {1, 2, 3};
auto it = v.begin();

v.reserve(100);  // 扩容,迭代器it失效!
// cout << *it;  // 错误!可能崩溃或输出错误数据

扩容之后,相当于是重新开辟了一块空间,清除掉了之前的空间,而迭代器仍指向之前空间的一个地址,就成了野指针

插入操作

cpp 复制代码
vector<int> v = {1, 2, 3};
auto it = v.begin();

v.insert(v.begin(), 0);  // 插入可能导致扩容,it失效!

插入元素期间,可能会引起扩容,而导致原空间被释放

删除操作

cpp 复制代码
vector<int> v = {1, 2, 3, 4};
auto it = v.begin() + 1;  // 指向2

v.erase(v.begin());       // 删除1,后面的元素前移
// cout << *it;          // it指向的位置现在是什么?不确定!

删除1时,会使后面的数据前移,即{2,3,4}

之前的it指向第二个位置即2,但删除操作之后数据前移,第二个位置就成了3,it由原来的2变成了3,导致it指向的数据混乱,甚至数组越界

正确示例:

无论是扩容,插入还是删除,都必须保证迭代器有效

cpp 复制代码
vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
while (it != v.end()) {
    if (*it % 2 == 0) {
        it = v.erase(it);  // erase返回下一个有效迭代器
    } else {
        ++it;
    }
}

4. vector的模拟实现

4.1 基础框架

在vector库中,vector的成员变量有_start(数组首地址),_finish(数组末尾元素的下一个地址),_end_of_storage(存储空间的末尾)组成

cpp 复制代码
template<typename T>
class vector {
private:
    T* _start;          // 指向首元素
    T* _finish;         // 指向最后一个元素的下一个位置
    T* _end_of_storage; // 指向存储空间的末尾

public:
    // 构造函数
    vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
    
    vector(size_t n, const T& val = T()) {
        _start = new T[n];
        _finish = _start + n;
        _end_of_storage = _finish;
        for (size_t i = 0; i < n; ++i) {
            _start[i] = val;
        }
    }
    
    // 析构函数
    ~vector() {
        if (_start) {
            delete[] _start;
            _start = _finish = _end_of_storage = nullptr;
        }
    }
    
    // 容量相关
    size_t size() const { return _finish - _start; }
    size_t capacity() const { return _end_of_storage - _start; }
    bool empty() const { return _start == _finish; }
    
    // 元素访问
    T& operator[](size_t pos) { return _start[pos]; }
    const T& operator[](size_t pos) const { return _start[pos]; }
};

4.2 深拷贝问题

错误方式(使用memcp):

cpp 复制代码
void reserve(size_t n) {
    if (n > capacity()) {
        T* tmp = new T[n];
        memcpy(tmp, _start, size() * sizeof(T));  // 浅拷贝!
        delete[] _start;
        _start = tmp;
        _finish = _start + size();
        _end_of_storage = _start + n;
    }
}

问题 :如果T是管理资源的类(如string),memcpy会导致多个对象共享同一资源,析构时重复释放。

正确方式:

cpp 复制代码
void reserve(size_t n) {
    if (n > capacity()) {
        T* tmp = new T[n];
        // 使用placement new或循环赋值,调用拷贝构造
        for (size_t i = 0; i < size(); ++i) {
            tmp[i] = _start[i];  // 调用赋值运算符,深拷贝
        }
        delete[] _start;
        _start = tmp;
        _finish = _start + size();
        _end_of_storage = _start + n;
    }
}

4.3 完整实现关键函数

cpp 复制代码
// 拷贝构造(深拷贝)
vector(const vector<T>& v) 
    : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {
    reserve(v.capacity());
    for (size_t i = 0; i < v.size(); ++i) {
        _start[i] = v[i];
    }
    _finish = _start + v.size();
}

// 赋值运算符(现代写法)
vector<T>& operator=(vector<T> v) {  // 传值,利用拷贝构造
    swap(v);
    return *this;
}

void swap(vector<T>& v) {
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_end_of_storage, v._end_of_storage);
}

// push_back
void push_back(const T& val) {
    if (_finish == _end_of_storage) {
        reserve(capacity() == 0 ? 4 : capacity() * 2);
    }
    *_finish = val;
    ++_finish;
}

// insert
iterator insert(iterator pos, const T& val) {
    assert(pos >= _start && pos <= _finish);
    
    if (_finish == _end_of_storage) {
        size_t len = pos - _start;
        reserve(capacity() == 0 ? 4 : capacity() * 2);
        pos = _start + len;  // 扩容后迭代器失效,需要重新计算
    }
    
    // 后移元素
    for (auto it = _finish; it > pos; --it) {
        *it = *(it - 1);
    }
    *pos = val;
    ++_finish;
    
    return pos;
}

// erase
iterator erase(iterator pos) {
    assert(pos >= _start && pos < _finish);
    
    for (auto it = pos; it < _finish - 1; ++it) {
        *it = *(it + 1);
    }
    --_finish;
    
    return pos;
}

5. 性能优化建议

  1. 预分配空间 :如果知道大概的元素数量,使用reserve()避免多次扩容

  2. 使用emplace_back:C++11引入,避免不必要的拷贝/移动

  3. 避免在循环中判断容量:在循环前确保容量足够

  4. 使用swap释放内存vector<T>().swap(v)可以真正释放内存

cpp 复制代码
vector<int> v;
v.reserve(1000);  // 预分配空间,避免push_back时多次扩容

for (int i = 0; i < 1000; ++i) {
    v.push_back(i);  // 不会触发扩容
}

// 释放内存
vector<int>().swap(v);  // v现在为空,且capacity为0
相关推荐
i学长的猫3 小时前
Ruby小白学习路线
开发语言·学习·ruby
送秋三十五3 小时前
Docker 构建教程:学习上下文、架构和性能优化技术
学习·docker·架构
张口口4 小时前
1032 挖掘机技术哪家强
c语言
奋斗羊羊4 小时前
【C++】使用MSBuild命令行编译ACE、TAO、DDS
开发语言·c++·windows
思成不止于此4 小时前
软考中级软件设计师备考指南(四):I/O 技术、安全与可靠性 —— 综合应用篇
网络·笔记·学习·信息安全·总线系统·i/o 技术·可靠性计算
史迪奇_xxx5 小时前
9、C/C++ 内存管理详解:从基础到面试题
java·c语言·c++
WTCLLB5 小时前
Acer软件下载
学习·软件保护卡
晨非辰5 小时前
《超越单链表的局限:双链表“哨兵位”设计模式,如何让边界处理代码既优雅又健壮?》
c语言·开发语言·数据结构·c++·算法·面试
胖咕噜的稞达鸭5 小时前
算法入门:专题攻克一---双指针4(三数之和,四数之和)强推好题,极其锻炼算法思维
开发语言·c++·算法