当前位置: 首页 > news >正文

【C++初阶】vector容器的模拟实现,各接口讲解

1.vector的介绍

使用STL的三个境界:能用,明理,能扩展 ,那么下面学习vector,我们也是按照这个方法去学习,以下是vector的官方文档

2.接口模拟实现总览

为了避免命名冲突,我们还是将实现的vector封装到单独的命名空间里

#pragma once  
#include<iostream>
#include<cassert>
#include<string>
namespace gjy
{template<class T>//这是一个类模板类内部所有用到的 T(如成员变量类型、函数参数 / 返回值类型)都依赖于这个模板参数,随 vector 的实例化类型而确定。class vector{public://—————————迭代器—————————/*using iterator = T*;using const_iterator = const T*;*///这里为什么这样写,而不是直接在第一个迭代器前面加const?//是因为直接在前面加const是让指针本身不能修改,不是指针指向的内容typedef T* iterator;// 指向数据不能修改,本身可以修改typedef const T* const_iterator;iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}
//——————————1、默认成员函数——————————//1.1.构造函数//无参构造函数,使用缺省值来初始化列表vector(){}//迭代器区间构造函数template<class Inputerator>//模板函数,这里的class不是类的意义,也可以用typenamevector(Inputerator first, Inputerator last)//使用缺省值初始化列表{while (first != last){push_back(*first);first++;}}//有参构造(n个值为val的容器)vector(size_t n,const T&val)//这里的 T 就是最外层类模板的参数 T,代表当前 vector 存储的元素类型。{reserve(n);//调用reserve函数将容器容量设置为nfor (size_t i = 0;i < n;i++){push_back(val);//利用尾插函数插入数据}}//下面两个构造是跟微信那个问题有关vector(long n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);}}vector(int n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);}}//拷贝构造//传统写法//vector(const vector<T>& v)//后面不显式写初始化列表,编译器会用缺省值写//{//	_start = new T[v.capacity()];//开辟一块和容器V一样大小的空间//		for (size_t i = 0;i < v.size();i++)//左闭右开就是数据个数//		{//			_start[i] = v[i];//		}//	_finish = _start + v.size();//更新有效数据的尾//	_end_of_storage = _start + v.capacity();//}//现代写法:拷贝并交换(Copy-and-Swap)vector(const vector<T>& v){// 1. 利用迭代器区间构造函数,创建临时对象tmp(拷贝v的所有元素)vector<T> tmp(v.begin(), v.end());  // 复用已有构造函数,无需重复写拷贝逻辑// 2. 交换当前对象和临时对象的资源swap(tmp);  // 需实现swap成员函数}/*vector(const vector<T>& v){reserve(v.capacity());for (auto e : v){push_back(e);}}*/~vector(){delete[] _start;_start = _finish = _end_of_storage = nullptr;}//赋值运算符重载//传统写法//vector<T>& operator=(const vector<T>& v)//{//	if (this != &v)//防止自赋值//	{//		delete[]_start;//释放原空间//		_start = _finish = _end_of_storage = nullptr;//		reserve(v.size());//复用reserve开和v一样的空间//		for (auto& e : v)//		{//			push_back(e);//		}//	}//	return *this;//}//现代写法vector<T>& operator=(vector<T> v){if (this !=& v){swap(v);}return *this;}//—————————2.访问操作—————————T& operator[](size_t n){assert(n < size());return *(_start + n);}const T& operator[](size_t n) const{assert(n < size());return *(_start + n);}//—————————3.容量操作—————————size_t size()const{return _finish - _start;}size_t capacity()const{return _end_of_storage - _start;}void reserve(size_t n){if (capacity() < n)//判断是否需要扩容,reserve不对内容修改{size_t Old_size = size(); //记录当前容器当中有效数据的个数T* tmp = new T[n]; //开辟一块可以容纳n个数据的空间if (_start) //判断是否为空容器{for (size_t i = 0; i < Old_size; i++) //将容器当中的数据一个个拷贝到tmp当中{tmp[i] = _start[i];}delete[] _start; //将容器本身存储数据的空间释放}_start = tmp; //将tmp所维护的数据交给_start进行维护_finish = _start + Old_size; //容器有效数据的尾_end_of_storage = _start + n; //整个容器的尾}}void resize(size_t n, const T& val = T()){if (n < size()){_finish = _start + n;//直接覆盖掉后面的数据}else{reserve(n);while (_finish != _start + n){*_finish = val;++_finish;}}}bool empty(){return(_start == _finish);//或者return!capacity()}//—————————4.修改操作—————————// 实现swap成员函数(交换两个vector的资源)void swap(vector<T>& other){// 交换三个指针即可(O(1)操作,无内存分配)std::swap(_start, other._start);std::swap(_finish, other._finish);std::swap(_end_of_storage, other._end_of_storage);}void push_back(const T& val){if (_finish == _end_of_storage){size_t newcapcity = capacity() == 0 ? 4 : 2 * capacity();//将容量扩大为二倍capacityreserve(newcapcity);//开辟新空间大小}*_finish = val;_finish++;}void pop_back(){assert(capacity());_finish--;}//insert迭代器失效问题void 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 : 2 * capacity());//reserve过后原来的start去新的地方了pos = _start + len;//这里是更新后的start}iterator it = _finish - 1;//最后一个数据位置while (it >= pos){*(it + 1) = *it;//往后挪it--;}*pos = val;++_finish;}iterator erase(iterator pos)//这里不是void而是返回值,是因为vs检查机制很严格,只要删除过后,后面的迭代器都会失效,所以返回删除位置下一个元素的迭代器(其实--过后还是pos位置){assert(pos >= _start);assert(pos < _finish);iterator it = pos + 1;//记录后一个元素while (it < _finish){*(it - 1) = *it;//往前面挪++it;}--_finish;return pos;}private:iterator _start=nullptr;iterator _finish=nullptr;iterator _end_of_storage=nullptr;};}

类内部所有用到的 T(如成员变量类型、函数参数 / 返回值类型)都依赖于这个模板参数,随 vector 的实例化类型而确定。vector<int>就是存放int类型的vector容器,甚至还可以vector<vector<int>>创建一个二维数组,并且相对于C语言的二维数据,封装了各种接口可以使用

3、vector的成员变量和默认成员函数

3.1.成员变量介绍

我们先来讲讲vector的成员变量有哪些:_start、_finish、_end_of_storage
vector容器其实就是我们数据结构中学过的顺序表结构,实现这一结构的成员变量如下图

_start指向第一个元素,_finish指向结束的下一个位置,_end_of_storage指向整个容器的结尾。

3.2.构造函数

构造函数为什么还要写一个无参数的,跟后面现代写法swap函数传形式参数那里有关,一会再说有关是因为只要有构造函数,

1.无参构造函数

//无参构造函数
//没有显式写初始化列表就用缺省值
vector()
{
}

C++ 标准规定:如果用户显式定义了任何构造函数(如拷贝构造、赋值运算符等),编译器将不再自动生成默认构造函数(无参构造函数)

如果不手动定义默认构造函数 vector() {},编译器不会生成默认构造函数。此时,当需要创建无参的 vector 对象时(如 bit::vector<int> v;),会因找不到默认构造函数而编译报错。

 确保成员变量的缺省值正常生效

为成员变量设置了缺省值:

private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;

这些缺省值需要在对象初始化时生效。对于默认构造函数:

  • 如果显式定义了 vector() {},编译器会在该构造函数的初始化阶段,使用成员变量的缺省值对 _start_finish_end_of_storage 进行初始化(本质是编译器为默认构造函数隐式补充了初始化列表)。
  • 如果没有显式定义默认构造函数,且编译器也没有生成(因存在其他构造函数),则无法通过无参方式创建对象,成员变量的缺省值也就无从谈起。

2.有参构造函数(利用迭代器)

/迭代器区间构造函数template<class Inputerator>//模板函数,这里的class不是类的意义,也可以用typenamevector(Inputerator first, Inputerator last)//使用缺省值初始化列表{while (first != last){push_back(*first);first++;}}

这里的迭代器使用了模板参数,各种容器的迭代器都可以传参,适用于不同类型的迭代器

3.有参构造函数(n个val值的)

vector(size_t n,const T&val)//这里的 T 就是最外层类模板的参数 T,代表当前 vector 存储的元素类型。
{reserve(n);//调用reserve函数将容器容量设置为nfor (size_t i = 0;i < n;i++){push_back(val);//利用尾插函数插入数据}
}

该构造函数用于创建一个包含 n 个相同元素 val 的 vector。例如:vector<int> (5, 10):创建一个存储 int 的 vector,包含 5 个值为 10 的元素;vector<string> (3, "hello"):创建一个存储 string 的 vector,包含 3 个值为 "hello" 的元素。

注意:
  • 该构造函数明确知道需要存储n个数据,因此建议使用reserve函数一次性分配足够的空间。这样可以避免后续调用push_back时多次扩容,从而提高效率。
  • 该构造函数还需要实现两个重载版本,如下:
    vector(long n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
    {reserve(n); //调用reserve函数将容器容量设置为nfor (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);}
    }
    vector(int n, const T& val):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
    {reserve(n); //调用reserve函数将容器容量设置为nfor (int i = 0; i < n; i++) //尾插n个值为val的数据到容器当中{push_back(val);}
    }
    

    这两个重载函数的区别在于参数n类型不同,如果不这样写的话,当我们传参如下

    vector<int>(10,1)//本意是调用第三个构造函数的

    会发生如下图一样的问题,我们的编译器会默认选择更合适的函数,因为两个int,就会调用我们使用迭代器的那个构造函数,而int类型不能解引用,因此会报错

  • C++重载决议优先选择"最匹配"的函数,不需要类型转换的版本优先于需要类型转换的版本

  • 模板函数能精确匹配时,优先于需要隐式转换的非模板函数

解决方法

        1.提供合适的重载类型就是再写一个int类型的,以及long类型的构造函数

        2.使用强制类型转换传参,调用时可以使用vector<int>{10u,7},强制转换第一个参数为unsigned。    

3.3.拷贝构造

动态开辟了资源因此也需要深拷贝,关于vector的深拷贝构造函数,提供两种实现方式:

1.传统写法:

        先开辟与原容器一样大的空间,再逐个拷贝原容器中的数据,最后更新_finish和_end_of_storage指针即可

//传统写法
vector(const vector<T>& v)//后面不显式写初始化列表,编译器会用缺省值写
{_start = new T[v.capacity()];//开辟一块和容器V一样大小的空间for (size_t i = 0;i < v.size();i++)//左闭右开就是数据个数{_start[i] = v[i];}_finish = _start + v.size();//更新有效数据的尾_end_of_storage = _start + v.capacity();
}

注意事项:

        当在逐个拷贝容器时候应该尽量避免使用memcpy函数,当vector存储的是内置类型或者无需深拷贝的自定义类型时候,还能使用,当需要重新分配资源时就不行,这个在后面reserve那里迭代器失效也有关系。如果拷贝的容器里面存放的是string类型

每一个string都指向自己所存储的字符串

        若使用memcpy函数进行拷贝构造,新构造的vector中每个string对象的成员变量将和vector中的string对象完全相同,这意味着两个vector中对应的string成员会指向相同的字符串存储空间。

那我们的代码是如何解决的呢?


for (size_t i = 0; i < v.size(); i++) 
{start[i] = v[i];
}

这里的赋值实际上调用的是赋值运算符重载,这里面会重新分配资源

实际流程是当拷贝 vector<string> 时:

  1. 先为 vector 开辟新的内存空间(new T[v.capacity()],这里 T 是 string)。
  2. 对每个 string 元素,通过 string 的拷贝赋值运算符完成深拷贝。
  3. 最终得到的新 vector<string> 与原 vector 存储的 string 元素完全独立,修改其中一个的元素不会影响另一个。

简言之,拷贝 vector<string> 时,会间接调用 string 类的拷贝赋值运算符重载,保证字符串内容的深拷贝。

2.现代写法:

// 现代写法:拷贝并交换(Copy-and-Swap)
vector(const vector<T>& v){// 1. 利用迭代器区间构造函数,创建临时对象tmp(拷贝v的所有元素)vector<T> tmp(v.begin(), v.end());  // 复用已有构造函数,无需重复写拷贝逻辑// 2. 交换当前对象和临时对象的资源swap(tmp);  // 需实现swap成员函数
}
	// 实现swap成员函数(交换两个vector的资源)void swap(vector<T>& other){// 交换三个指针即可(O(1)操作,无内存分配)std::swap(_start, other._start);std::swap(_finish, other._finish);std::swap(_end_of_storage, other._end_of_storage);}

        利用构造函数构造一个临时对象,这个临时对象和要拷贝的对象拥有一样大的空间和数据,我们只需要交换this和这个临时对象的值和空间,出了作用域临时对象销毁顺带释放了this原来的空间。我们调试会发现传参那一步跳转到了拷贝构造函数,然后就出现了错误

注意:

        出现错误的原因是因为我们传值传参调用拷贝构造,拷贝构造函数中实参v3传递给形参v,将v,这里reserve是this调用的,因为要拷贝v3的那个对象没有初始化,没有构造那个对象,因此reserve,push_back这些接口使用就会报错,所以需要我们要么初始化列表中显示初始化,要么就给成员变量缺省值


vector(const vector<T>& v):_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{//...
}

3.写法三.

        使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。

vector(const vector<T>& v)
{reserve(v.capacity());for (auto e : v){push_back(e);}
}

4.析构函数

~vector()
{delete[] _start;_start = _finish = _end_of_storage = nullptr;
}

析构函数直接删除数据即可,如果vector存放的是自定义类型会调用其内部的析构函数释放资源


5.赋值运算符重载

vector的赋值运算符重载当然也涉及深拷贝问题,我们这里也提供两种深拷贝的写法:

1.传统写法

vector<T>& operator=(const vector<T>& v)
{if (this != &v)//防止自赋值{delete[]_start;//释放原空间_start = _finish = _end_of_storage = nullptr;reserve(v.size());//复用reserve开和v一样的空间for (auto& e : v){push_back(e);}}return *this;
}

复用reserve和尾插,将右值传给左值。

2.现代写法

        还是利用swap函数,传值传参调用拷贝构造生成临时对象,交换this和临时对象两个对象的值,出了作用域利用临时对象的销毁释放原空间,就不需要在手动delete[]了.

vector<T>& operator=(const vector<T> v)
{swap(v);return *this;}


4、迭代器函数和访问元素操作

4.1.begin()和end()

依然分为const迭代器和普通迭代器,const迭代器不能直接在普通迭代器前面加const,那是是迭代器不能修改,而不是指向内容不能修改,应该在后面加const代表返回值是const类型

/*using iterator = T*;
using const_iterator = const T*;*///这里为什么这样写,而不是直接在第一个迭代器前面加const?
//是因为直接在前面加const是让指针本身不能修改,不是指针指向的内容
typedef T* iterator;
// 指向数据不能修改,本身可以修改
typedef const T* const_iterator;iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin() const
{return _start;
}const_iterator end() const
{return _finish;
}

我们实现了迭代器,编译器自己就也实现范围for,可以用范围for对容器进行遍历

4.2.operator[]

 因为底层数据结构是数组,所以vector也支持我们使用“下标+[ ]”的方式对容器当中的数据进行访问,实现时直接返回对应位置的数据即可。我们需要重载两个形式,普通的和const类型的函数。

T& operator[](size_t n)
{assert(n < size());return *(_start + n);
}
const T& operator[](size_t n) const
{assert(n < size());return *(_start + n);
}

5、容量操作

5.1.size()和capacity()

我们知道两个指针相减返回的是相差的个数而不是空间大小,比如int*p1-int*p2返回的是(p1-p2)/sizeof(int*),因此可以利用指针相减的值返回size和capacity

size_t size()const
{return _finish - _start;
}
size_t capacity()const
{return _end_of_storage - _start;
}

5.2.reserve()

        void reserve(size_t n){if (capacity() < n)//判断是否需要扩容,reserve不修改内容和size{size_t Old_size = size(); //记录当前容器当中有效数据的个数T* tmp = new T[n]; //开辟一块可以容纳n个数据的空间if (_start) //判断是否为空容器{for (size_t i = 0; i < Old_size; i++) //将容器当中的数据一个个拷贝到tmp当中{tmp[i] = _start[i];}delete[] _start; //将容器本身存储数据的空间释放}_start = tmp; //将tmp所维护的数据交给_start进行维护_finish = _start + Old_size; //容器有效数据的尾,加新的start_end_of_storage = _start + n; //整个容器的尾}}

执行扩容操作前需要先保存当前容器中的有效元素数量。 因为最终需要更新_finish指针的位置,而_finish的正确位置等于_start指针加上原始元素数量。如果在_start指针改变后再通过_finish - _start计算size,得到的结果将是无效的随机值。


5.3.resize() 

resize规则:

  1. 当n大于size时,将size扩展到n,新增元素初始化为val,如果没有提供val,则使用该类型的默认构造函数生成默认值。
  2. 当n小于size时,将size减到n。

先比较n和size的大小,当n小时,直接将_finish置到n的位置,当n比较大时,复用reserve函数,开辟空间,再进行赋值操作,不用操心是否扩容的问题了

void resize(size_t n,const T& val)
{if (n < size()){_finish = _start + n;//直接覆盖掉后面的数据}else{reserve(n);while (_finish != _start + n){*_finish = val;++_finish;}}
}


5.4.empty()

就是简单的判空即可,比较start和finish的值即可,若返回位置相同即为空

bool empty()
{return(_start == _finish);
}

6、修改操作

6.1.push_back()

void push_back(const T& val)
{if (_finish == _end_of_storage){size_t newcapcity = capacity() == 0 ? 4 : 2 * capacity();//将容量扩大为二倍capacityreserve(newcapcity);//开辟新空间大小}*_finish = val;_finish++;
}

需要注意的点时检查是否已经满了,满了要扩容,这里我们用三目表达式来写

6.2.pop_back()

尾删之前需要检查是否为空vector,若空则做断言处理,若不为空只需要将_finish--即可

void pop_back()
{assert(!empty());//或者finish>start_finish--;
}

6.3.insert()有迭代器失效情况

        insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。

//insert迭代器失效问题
void 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 : 2 * capacity());//reserve过后原来的start去新的地方了pos = _start + len;//这里是更新后的start}iterator it = _finish - 1;//最后一个数据位置while (it >= pos){*(it + 1) = *it;//往后挪it--;}*pos = val;++_finish;
}

6.4.erase()有迭代器失效情况

		iterator erase(iterator pos)//这里不是void而是返回值,是因为vs检查机制很严格,只要删除过后,后面的迭代器都会失效,所以返回删除位置下一个元素的迭代器(其实--过后还是pos位置){assert(pos >= _start);assert(pos < _finish);//assert(empty());前面断言了那两种就包含了这一个了iterator it = pos + 1;//记录后一个元素while (it < _finish){*(it - 1) = *it;//往前面挪++it;}--_finish;return pos;}

  erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器是否为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。

6.5.swap()

调用标准库里面的swap函数

void swap(vector<T>& other)
{// 交换三个指针即可(O(1)操作,无内存分配)std::swap(_start, other._start);std::swap(_finish, other._finish);std::swap(_end_of_storage, other._end_of_storage);
}

7.迭代器失效问题下一篇博客见

http://www.dtcms.com/a/592563.html

相关文章:

  • QGIS 3.34+ 网络分析基础数据自动化生成:从脚本到应用
  • 第2章-类加载子系统-知识补充
  • Go Fiber 简介
  • 专业酒店设计网站建设手机什么网站可以设计楼房
  • 20251110给荣品RD-RK3588开发板跑Rockchip的原厂Android13系统时熟悉散热风扇
  • UniApp自定义Android基座原理及流程
  • Ganache-CLI以太坊私网JSON-RPC接口执行环境搭建
  • Android 系统超级实用的分析调试命令
  • 【ZeroRange WebRTC】WebRTC 加密安全总览:对称/非对称、数字签名、证书、SHA/HMAC、随机数
  • 【ZeroRange WebRTC】数字签名与 WebRTC 的应用(从原理到实践)
  • 承德网站制作公司做国外的网站有什么不用钱的
  • 破解遗留数据集成难题:基于AWS Glue的无服务器ETL实践
  • Rust 的所有权系统,是一场对“共享即混乱”的编程革命
  • 【Rust 探索之旅】Rust 库开发实战教程:从零构建高性能 HTTP 客户端库
  • API 设计哲学:构建健壮、易用且符合惯用语的 Rust 库
  • 横沥镇做网站wordpress中文说明书
  • 先做个在线电影网站该怎么做贵阳做网站软件
  • 【字符串String类大集合】构造创建_常量池情况_获取方法_截取方法_转换方法_String和基本数据类型互转方法
  • Http请求中Accept的类型详细解析以及应用场景
  • 升鲜宝 供应链SCM 一体化自动化部署体系说明
  • grafana配置redis数据源预警误报问题(database is locked)
  • 拒绝繁琐,介绍一款简洁易用的项目管理工具-Kanass
  • 测试自动化新突破:金仓KReplay助力金融核心系统迁移周期缩减三周
  • 大语言模型入门指南:从科普到实战的技术笔记(1)
  • 大模型原理之Transformer进化历程与变种
  • 2025-简单点-ultralytics之LetterBox
  • 网站开发经济可行性分析石龙做网站
  • wordpress中国优化网络优化的目的
  • 【Linux网络】Socket编程TCP-实现Echo Server(下)
  • 路由协议的基础