详解C++ STL模拟实现vector

目录
  • vector 概述
  • 接口总览
  • 默认成员函数
    • 构造函数
    • 析构函数
    • 拷贝构造函数
    • 复制赋值函数
  • vector 的迭代器
  • 元素访问
    • operator[]
    • back
  • 容量相关函数
    • size
    • capacity
    • empty
    • resize
    • reserve
  • 修改函数
    • insert
    • push_back
    • erase
    • pop_back
    • swap

vector 概述

vector 的数据结构安排及操作方式,与原生数组十分相似,两者唯一的差别在于空间运用的灵活性。原生数组是静态空间,一旦配置了就不能改变大小;vector 是动态空间,可以在插入过程中动态调整空间大小。vector 的实现技术,关键在于它对大小的控制及重新配置时的数据移动效率。

在文中,将会挑选 vector 的一些常用接口来模拟实现,但并不和标准库中实现方式相同。标准库中使用了大量内存操作函数来提高效率,代码晦涩难懂,不利用初学者学习。本文实现方式相对简单,但也需要读者有一定的 STL 使用经验。下面就让我们开始吧。

接口总览

namespace qgw {
	template <class T>
	class vector {
		typedef T* iterator;
        typedef const T* const_iterator;
		typedef T& reference;
	public:
		// 默认成员函数
		vector();										// 默认构造函数
		vector(size_t n, const T& val = T());			// 构造 n 个 T 类型对象
		vector(InputIterator first, InputIterator last);// 用一段区间构造
		vector(const vector<T>& v);						// 拷贝构造函数
		vector<T>& operator=(const vector<T>& v);		// 复制赋值函数
		~vector();

		// 迭代器函数
		iterator begin();
        const_iterator begin() const;
		iterator end();
        const_iterator end() const;

		// 元素访问
		reference operator[](size_t pos);
		const reference operator[](size_t pos) const;
		reference back();
		const reference back() const;

		// 容量
		bool empty() const;
		size_t size() const;
		size_t capacity() const;
		void resize(size_t cnt, T val = T());
		void reserve(size_t cap);

		// 修改
		iterator insert(iterator pos, const T& val);
		void push_back(const T& val);
		iterator erase(iterator pos);
		iterator erase(iterator first, iterator last);
		void pop_back();
		void swap(vector<T>& v);

	private:
		iterator _start;			// 表示目前使用空间的头
		iterator _finish;			// 表示目前使用空间的尾
		iterator _end_of_storage;	// 表示已分配空间的尾
	};
}

成员变量介绍

vector 中有三个成员变量,_start 指向使用空间的头,_finish 指向使用空间的尾,_end_of_storage 指向已分配空间的尾。

由上图也可以清晰的看出,_finish - _start 就是 size 的大小,_end_of_storage - _start 就是 capacity 的大小。

默认成员函数

构造函数

默认构造函数

vector 支持一个无参的构造函数,在这个构造函数中我们直接将上文中三个成员变量初始化为空即可。

/// @brief 默认构造函数,将指针初始化为空
vector() {
    _start = nullptr;
    _finish = nullptr;
    _end_of_storage = nullptr;
}

构造 n 个 T 类型对象

vector 支持构造 n 个 值为 val 的对象。可以先用 reserve开辟容量,在调用 push_back 插入即可。

注意:reserve 改变的是 capacity 的大小,不改变 size 的大小,先开辟容量为防止需多次扩容降低效率。

/// @brief 构造 n 个值为 val 的对象
/// @param n 容器的大小
/// @param val 用来初始化容器元素的值
vector(size_t n, const T& val = T()) {
    _start = nullptr;
    _finish = nullptr;
    _end_of_storage = nullptr;

    reserve(n);
    for (int i = 0; i < n; ++i) {
        push_back(val);
    }
}

一段区间构造

vector 支持使用一段迭代器区间构造,区间范围是 [first, last),这里的迭代器不一定要是 vector 的迭代器,只有是具有输入功能的迭代器都可以。

/// @brief 用给定的迭代器区间初始化,STL 中的区间均为左闭右开形式,即 [first, last)
/// @tparam InputIterator 所需最低阶的迭代器类型,具有输入功能的迭代器都可以
/// @param first 迭代器起始位置
/// @param last 迭代器终止位置
template< class InputIterator>
    vector(InputIterator first, InputIterator last) {
    _start = nullptr;
    _finish = nullptr;
    _end_of_storage = nullptr;

    // 和上一个类似,先开辟空间,尾减头即为要开辟的个数
    reserve(last - first);
    while (first != last) {
        push_back(*first);
        ++first;
    }
}

析构函数

析构函数的实现很简单,首先检查容器是否为空,不为空就释放空间,再把指针置空即可。

注意:因为我们开辟了连续的空间,要使用 delete[] 来释放空间,对应的也要使用 new[] 来开辟空间。即使我们只开辟一个空间也不能使用 new,否则对自定义类型在释放时程序会崩溃。

具体原因请看:new 和 delete 为什么要匹配使用

/// @brief 释放开辟的空间
~vector() {
    if (_start != nullptr) {
        // 释放开辟的空间,从此处可以看出,开辟空间一定要用 new[]
        // 否则对于自定义类型程序将会崩溃
        delete[] _start;
        _start = nullptr;
        _finish = nullptr;
        _end_of_storage = nullptr;
    }
}

拷贝构造函数

下面给出的实现方法比较简单,直接用容器 v 初始化创建一个临时容器 tmp,再交换 tmp 和 this 指针指向就好了。

此时 this 指向的容器是由 v 初始化出来的,tmp 指向了一个全空的容器,tmp 出了作用域就销毁了。

需要注意的是此处一定要先将指针初始化为空,否则交换给 tmp 的指针要是不为空而是随机值的话,tmp 销毁时调用析构函数就会导致程序崩溃。有些 ide(vs 2022) 可能会自动赋空,dev 下就不会,对指针类型编译器本来就不会处理。

/// @brief 用给定容器初始化
/// @param v 用来初始化的容器
vector(const vector<T>& v) {
    _start = nullptr;
    _finish = nullptr;
    _end_of_storage = nullptr;

    vector<T> tmp(v.begin(), v.end());
    swap(tmp);
}

复制赋值函数

该函数与拷贝构造函数类似,不同的是这里指针不应赋空,否则指针原本指针指向的东西就无法正确释放了。

/// @brief 替换容器内容
/// @param v 用作数据源的另一容器
/// @return *this
vector<T>& operator=(const vector<T>& v) {
    vector<T> tmp(v);
    swap(tmp);
    // 返回值为对象的引用,为的是可以连续赋值
    return *this;
}

vector 的迭代器

vector 维护的是一个连续线性空间,不论元素是什么类型,普通指针都可以作为 vector 的迭代器满足所有的条件,因此元素类型的指针就是 vector 的迭代器。

typedef T* iterator;
typedef const T* const_iterator;

begin 和 end

begin 和 end 获取的是正向迭代器,begin 指向第一个元素,end 指向最后一个元素的下一个位置,begin++ 是向后即 end 方向移动。

对应的还有反向迭代器 rbegin 和 rend,rbegin 指向最后一个元素,rend 指向第一个元素的前一个位置,rbegin++ 是向前即 rend 方向移动。两者对应如下图,因为反向迭代器复杂的多,这里就不实现了。

/// @brief 返回指向 vector 首元素的迭代器
/// @return 指向首元素的迭代器
iterator begin() {
    return _start;
}

// const 版本供 const 容器使用
const_iterator begin() const {
    return _start;
}

/// @brief 返回指向 vector 最后元素后一元素的迭代器
/// @return 指向最后元素下一个位置的迭代器
iterator end() {
    return _finish;
}

const_iterator end() const {
    return _finish;
}

元素访问

operator[]

vector 也支持向数组一样的 [] 访问方式,可以随机读取每个位置,返回该位置元素的引用。

需要注意的是,该函数并不做边界检查,需程序员自行检查。

/// @brief 返回位于指定位置 pos 的元素的引用,不进行边界检查
/// @param pos 要返回的元素的位置
/// @return 到所需元素的引用
reference operator[](size_t pos) {
    return _start[pos];
}

// 与上面的唯一不同就是用于 const 容器
const reference operator[](size_t pos) const {
    return _start[pos];
}

back

back 可以获取最后一个元素的引用。

/// @brief 返回到容器中最后一个元素的引用
/// @return 最后元素的引用
reference back() {
    return *(end() - 1);
}

const reference back() const {
    return *(end() - 1);
}

容量相关函数

size

根据图可知 _finish - _start 即为 size。

/// @brief 返回容器中的元素数
/// @return 容器中的元素数量
size_t size() const {
    return _finish - _start;
}

capacity

由图可知 _end_of_storage - _start 即为 capacity。

/// @brief 返回容器当前已为之分配空间的元素数
/// @return 当前分配存储的容量
size_t capacity() const {
    return _end_of_storage - _start;
}

empty

检查是否为空的方法很简单,直接比较 _start 是否 _finish 相等即可,相等就为空,返回 true。

/// @brief 检查容器是否无元素
/// @return 若容器为空则为 true,否则为 false
bool empty() const {
    return _start == _finish;
}

resize

重设容器大小以容纳 cnt 个元素。

如果当前大小大于 cnt,那么减小容器到它的开头 cnt 个元素。

如果当前大小小于 cnt,那么就调用 insert 在最后插入 cnt - size() 个 val。

一定要注意,不能只改变 _finish,呢样会因没调用析构函数从而引发内存泄漏。你可能会想,我们会在最后容器销毁的时候调用它的析构函数,它的析构函数中有 delete[],这个语句会调用数据的析构函数不会引起内存泄漏。这样想有一定的道理,但有没有可能我们 vector 中一开始有 10 个元素,我们用 resize 将其大小改变为 5,再调用 5 次 insert 将其大小变为 10,最后对象销毁调用析构。

如上图,我们一开始有 10 个 int* 类型的元素,分别指向一块空间,后面 resize 为 5 后又添加了 5 个新的数据(用蓝色标识)。当我们析构的时候我们会析构下图中的元素,释放它们指向的空间,那上图中的 6、7、8、9、10 呢,没办法,因为我们已经找不到它们的指针了,也就没办法释放它们的空间了。

/// @brief 重设容器大小以容纳 cot 个元素
/// @param cnt 容器的大小
/// @param val     用以初始化新元素的值
void resize(size_t cnt, T val = T()) {
    if (cnt < size()) {
        // 新大小小于原来的,需要将多余的部分删除掉
        // 不能只改变 _finish 指向,要使用 erase 来删除,以便调用析构函数
        erase(begin() + cnt, end());
    } else {
        // 新空间更大,直接调用 insert 插入即可
        for (int i = 0; i < cnt - size(); ++i) {
            insert(end(), val);
        }
    }
}

reserve

reserve 用来预留存储空间,如果要 push_back 大量的数据,可能会引起多次空间分配,从而多次转移元素浪费大量时间。可以预先开辟足够的空间,减少空间分配的次数,来提高效率。

注意:

1.若 cap 的值大于当前的 capacity,则分配新存储,否则不做任何事,也就是说 reserve 不会缩小容量

为什么一定要分配新存储,而不是在原空间之后接一部分新空间(因为无法保证原空间之后还有足够的可用空间)

2.同时,capacity 的改变也不会影响 size 的大小

reserve 扩容的思路也比较简单,开辟一段新空间,将原数据拷贝到新空间,释放旧空间即可。

需要注意的是,一定要在开始记录之前元素的个数,因为 _finish 还指向原空间最后一个有效数据的下一个位置,需要将其更新指向新空间。

/// @brief 增加 vector 的容量到大于或等于 cap 的值
/// @param cap vector 的新容量
void reserve(size_t cap) {
    size_t len = size();
    if (cap > capacity()) {
        T* tmp = new T[cap];
        if (_start != nullptr) {
            // 如果容器内之前有数据,将数据拷贝到新位置
            for (int i = 0; i < size(); ++i) {
                tmp[i] = _start[i];
            }
            delete[] _start;    // 释放掉旧的空间
        }
        _start = tmp;
    }
    // 指向新的地址空间
    _finish = _start + len;
    _end_of_storage = _start + cap;
}

修改函数

insert

insert 函数的功能很多,可以在指定位置插入一个或多个值,也可以插入一段区间。这里我们只实现插入一个值的函数,在 pos 前面插入 val。

插入之前首先要检查容量,不够就进行扩容。然后将插入位置之后的数据向后挪动一个位置,插入 val 即可。

需要注意的是,扩容的话需要提前记录 pos 之前的元素个数。因为 pos 指向的是之前空间的某个位置,要将其更新为新空间的地址。

/// @brief 将 val 插入到 pos 迭代器之前
/// @param pos 	将内容插入到它前面的迭代器
/// @param val 要插入的元素值
/// @return 指向被插入 val 的迭代器
iterator insert(iterator pos, const T& val) {
    // 检查参数是否在合法返回,assert 只在 degug 版本下有效
    assert(pos >= _start && pos <= _finish);

    if (_finish == _end_of_storage) {
        // 首先检查容量,空间不够要进行扩容
        // 先记录插入位置之前元素个数
        size_t len = pos - _start;
        // 第一次开辟空间给 10 个,后续扩容为 2 倍
        size_t newCap = capacity() == 0 ? 10 : capacity() * 2;
        reserve(newCap);
        // 更新 pos 在新空间中的位置
        pos = _start + len;
    }

    // 将插入位置之后的所有数据向后挪动一个位置
    iterator end = _finish - 1;
    while (end >= pos) {
        *(end + 1) = *end;
        --end;
    }
    *pos = val;
    ++_finish;
    return pos;
}

push_back

push_back 是向容器的最后添加一个元素,直接调用 insert 即可。

/// @brief 添加给定元素 val 到容器尾
/// @param val 要添加的元素值
void push_back(const T& val) {
    // 直接调用 insert 即可
    insert(end(), val);
}

erase

erase 从容器中删除指定的元素:

1.移除位于 pos 的元素

2.移除范围 [first, last) 中的元素

移除 pos 位置的元素方法很简单,首先调用该位置的析构函数,然后将后面的数据向前移动一个位置,最后的 --_finish 就可以了。

/// @brief 从容器擦除指定位置的元素
/// @param pos 指向要移除的元素的迭代器
/// @return 移除元素之后的迭代器
iterator erase(iterator pos) {
    assert(pos >= _start && pos < _finish);
    // 在 pos 位置调用析构函数,释放资源
    pos->~T();
    // 将 pos 位置之后的的元素都向前移动一个位置
    iterator it = pos + 1;
    while (it != _finish) {
        *(it - 1) = *it;
        ++it;
    }
    --_finish;
    // 此时的 pos 指向没删除前 pos 位置下一个元素
    return pos;
}

范围删除同样也很简单,首先要计算出要删除的个数,循环调用 erase 删除就可以了。

注:下面这种循环调用的方式效率十分低,库函数并没有使用这种方法,库函数首先对要删除的范围调用析构函数,然后将区间后面的数据移到前面。这样就只会移动一次数据,不向下面需要移动 cnt 次。

/// @brief 移除一段范围的元素
/// @param first 要移除的起始范围
/// @param last 要移除的结束返回
/// @return 移除的最后一个元素之后的迭代器
iterator erase(iterator first, iterator last) {
    int cnt = last - first;
    while (cnt--) {
        first = erase(first);
    }
    return first;
}

pop_back

pop_back 用来删除容器的最后一个元素,直接调用 erase 删除就行。

/// @brief 移除容器最后一个元素
void pop_back() {
    erase(_finish - 1);
}

swap

swap 函数可以用来交换两个容器的内容,不过不用实际交换数据,只需要改变两个容器指针的指向即可。

/// @brief 交换 this 指向的容器和 v 的内容
/// @param v 要交换内容的容器
void swap(vector<T>& v) {
    // 直接交换所有指针即可
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_end_of_storage, v._end_of_storage);
}

到此这篇关于详解C++ STL模拟实现vector的文章就介绍到这了,更多相关C++ STL vector内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++模拟实现STL容器vector的示例代码

    目录 一.vector迭代器失效问题 二.模拟实现构造函数调用不明确 1.问题描述 2.解决调用不明确的方法 三.reserve中的深浅拷贝问题 1.reserve中浅拷贝发生原因 2.浅拷贝发生的图解 3.解决方法 四.模拟实现vector整体代码 一.vector迭代器失效问题 1.Visual Studio和g++对迭代器失效问题的表现 int main(){ std::vector<int>v{1,2,3,4}; std::vector<int>::iterator it

  • C++模拟实现vector的示例代码

    目录 一.迭代器 定义 普通迭代器 const类型迭代器 二.构造类 构造函数 拷贝构造函数 赋值运算符重载 析构函数 三.容量相关操作 size.capacity empty resize reserve 三.元素访问 [ ]重载 front back 四.修改类接口 push_back pop_back insert erase clear swap 五.成员变量 一.迭代器 定义 vector类型的迭代器就是原生态的指针,对T*进行重命名即可 typedef T* iterator; ty

  • C++模拟如何实现vector

    目录 一.迭代器 定义 普通迭代器 const类型迭代器 二.构造类 构造函数 拷贝构造函数 赋值运算符重载 析构函数 三.容量相关操作 size.capacity empty resize reserve 三.元素访问 [ ]重载 front back 四.修改类接口 push_back pop_back insert erase clear swap 五.成员变量 总结 一.迭代器 定义 vector类型的迭代器就是原生态的指针,对T*进行重命名即可 typedef T* iterator;

  • C++  STL _ Vector使用及模拟实现

    目录 1.Vector的介绍 1.1 Vector的介绍 2.Vector的使用 2.1 vector的定义 2.2 vector 迭代器的使用 2.3 vector的空间增长问题 3. vector的增删查改 3.1 push_back (重点) 3.2 pop_back (重点) 3.3 insert 3.4 erase 3.5 operator [ ] 1.Vector的介绍 1.1 Vector的介绍 ​ ​vector官方文档介绍​​ 1.vector是表示可变大小数组的序列容器. 2

  • 详解C++ STL模拟实现vector

    目录 vector 概述 接口总览 默认成员函数 构造函数 析构函数 拷贝构造函数 复制赋值函数 vector 的迭代器 元素访问 operator[] back 容量相关函数 size capacity empty resize reserve 修改函数 insert push_back erase pop_back swap vector 概述 vector 的数据结构安排及操作方式,与原生数组十分相似,两者唯一的差别在于空间运用的灵活性.原生数组是静态空间,一旦配置了就不能改变大小:vec

  • 详解C++ STL vector容器访问元素的几种方式

    学会如何创建并初始化 vector 容器之后,本节继续来学习如何获取(甚至修改)容器中存储的元素. 访问vector容器中单个元素 首先,vector 容器可以向普通数组那样访问存储的元素,甚至对指定下标处的元素进行修改,比如: #include <iostream> #include <vector> using namespace std; int main() { vector<int> values{1,2,3,4,5}; //获取容器中首个元素 cout &l

  • 详解C++ STL vector容量(capacity)和大小(size)的区别

    很多初学者分不清楚 vector 容器的容量(capacity)和大小(size)之间的区别,甚至有人认为它们表达的是一个意思.本节将对 vector 容量和大小各自的含义做一个详细的介绍. vector 容器的容量(用 capacity 表示),指的是在不分配更多内存的情况下,容器可以保存的最多元素个数:而 vector 容器的大小(用 size 表示),指的是它实际所包含的元素个数. 对于一个 vector 对象来说,通过该模板类提供的 capacity() 成员函数,可以获得当前容器的容量

  • 详解使用IDEA模拟git命令使用的常见场景

    大家好,最近白泽第一次开始参与小组合作开发,以前都是自己用git保存自己的代码,自己维护,用git的场景也比较单一,没有遇到过拉取代码合并出现冲突的问题.但是小组开发拉取远程仓库的代码时,遇到他人所提交代码与自己的本地代码出现冲突在所难免,所以白泽特意去学习了一下git的冲突处理,接下来用一个小demo复现一下我学习的过程 前期准备 新建一个远程仓库 在一个文件夹内建立两个子文件夹作为两个本地仓库的存放位置 之所以建立两个文件夹,这样做的目的是为了模拟两个用户对同一个项目进行合作开发,假设dem

  • C语言详解如何应用模拟字符串和内存函数

    目录 1.strlen 求字符串长度 使用案例: 1.计数法 2.不创建临时变量计数器-递归 3.指针-指针的方式 2.长度不受限制的字符串函数 1.strcpy 使用案例: 模拟实现: 2.strcat 使用案例: 模拟实现: 3.strcmp-比较字符串首字母的大小 使用案例: 模拟实现: 3.长度受限制的字符串函数  1.strncpy 使用案例: 2.strncat  使用案例: 3.strncmp 使用案例: 4.strstr-找子串  使用案例: 模拟实现: 5.strtok 用法:

  • 详解vue2.0模拟后台json数据

    最近在跟着做vue2.0以上版本的一个购物平台,在涉及到模拟后台数据交互的时候,视频里讲的是通过json-server这个插件和express,由于之前的配置都是在build/dev-server.js文件夹下,在vue2.0都没有了,全部整合到了build/webpack.dev.conf.js文件里,通过不断查阅资料后终于模拟成功. 1.首先 npm install vue-resource  --save安装vue-resourse,并且在页面上引用(--save 会把依赖包名称添加到 p

  • 详解C++编程中的vector类容器用法

    vector简介 vector是STL中最常见的容器,它是一种顺序容器,支持随机访问.vector是一块连续分配的内存,从数据安排的角度来讲,和数组极其相似,不同的地方就是:数组是静态分配空间,一旦分配了空间的大小,就不可再改变了:而vector是动态分配空间,随着元素的不断插入,它会按照自身的一套机制不断扩充自身的容量. vector的扩充机制:按照容器现在容量的一倍进行增长.vector容器分配的是一块连续的内存空间,每次容器的增长,并不是在原有连续的内存空间后再进行简单的叠加,而是重新申请

  • 详解Java编程中向量(Vector)的应用

    Vector(向量)是 java.util 包中的一个类,该类实现了类似动态数组的功能. 向量和数组相似,都可以保存一组数据(数据列表).但是数组的大小是固定的,一旦指定,就不能改变,而向量却提供了一种类似于"动态数组"的功能,向量与数组的重要区别之一就是向量的容量是可变的. 可以在向量的任意位置插入不同类型的对象,无需考虑对象的类型,也无需考虑向量的容量. 向量和数组分别适用于不同的场合,一般来说,下列场合更适合于使用向量: 如果需要频繁进行对象的插入和删除工作,或者因为需要处理的对

  • 详解vue-cli中模拟数据的两种方法

    在main.js中引入vue-resource模块,Vue.use(vueResource). 1.使用json-server(不能用post请求) 接下来找到build目录下的webpack.dev.conf.js文件,在const portfinder = require('portfinder')后面引入json-server. /*引入json-server*/ const jsonServer = require('json-server') /*搭建一个server*/ const

  • Windows系统下nodejs、npm、express的下载和安装教程详解

    1. node.js下载 首先进入http://nodejs.org/dist/,这里面的版本呢,几乎每个月都出几个新的,建议大家下载最新版本,看看自己的电脑是多少位的,别下错了. 下载完解压到你想放的位置就好了,解压后你会发现里面有node.exe.我解压到了D:\software_install文件夹. 接下来去命令行,即点击电脑左下角的开始-->运行-->cmd. 进入node.exe所在的目录,输入node -v,查看你的node版本.我的路径如下图所示: 如果你获得以上输出结果,说明

随机推荐