【C++】vector模拟实现

这篇具有很好参考价值的文章主要介绍了【C++】vector模拟实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🚁 个人主页:不 良
🔥 系列专栏:🛸C++  🛹Linux
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹


vector中的成员变量

为了防止与标准库当中的vector产生命名冲突,模拟实现时需放在自己的命名空间当中。

SGI版本下库中的vector成员变量:

typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type* iterator;
typedef const value_type* const_iterator;

iterator start;
iterator finish;
iterator end_of_storage;

从库中的代码可以看出iterator是T类型的指针,但是VS中的不是原生指针,所以不要只认为iterator就是指针。

start指向的是容器中第一个元素;

finish指向的就是最后一个元素的下一个位置;

end_of_storage指向的是整个数组的下一个位置。

【C++】vector模拟实现,C++,c++,开发语言,后端

typeid().name()函数能看出指定对象类型。

【C++】vector模拟实现,C++,c++,开发语言,后端

所以当我们模拟实现vector时,可以将成员变量设置为模板类型的指针_start_finishend_of_storage

模拟实现vector中类的成员变量:

namespace Niu {
	template<class T>//类模板参数
    
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

默认成员函数

构造函数

无参构造函数

vector支持一个无参的构造函数,将构造对象的三个成员变量都设置为空指针。

vector()
    :_start(nullptr)
    ,_finish(nullptr)
    ,_end_of_storage(nullptr)
{
}

构造生成n个T类型的val值

需要先使用后面要实现的reserve函数提前开空间,以及push_back函数进行尾插。

//生成n个T类型的的val值
vector(size_t n, const T& val = T())
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	reserve(n); //调用reserve函数将容器容量设置为n
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);//尾插
	}
}

这里第二个参数使用了匿名对象,初始化的时候不能给0,因为T是一个模板参数,可以是自定义类型string也可以是其他类型,所以要用匿名对象。而且const T& val = T()使用引用接收,我们之前说匿名对象的生命周期只在当前行,那是因为没有去使用匿名对象,我们使用const引用可以延长匿名对象的生命周期,相当于就是给了匿名对象一个名字,可以将匿名对象的生命周期延长到对象a销毁。

因为匿名对象和临时对象都具有常性,所以必须使用const。

class A {
public:
	void Print() const
	{
		cout << " A" << endl;
	}
};

int main()
{
	//A& a = A();//error,匿名对象具有常性
	const A& a = A();
    //能够继续使用对象a
	a.Print();//输出A
}

该构造函数最好用reserve函数一次性开辟好空间,避免调用push_back函数时需要增容多次,导致效率降低。

为了避免和迭代器构造函数的使用造成冲突,这里再重载一个 int 类型参数的函数(只是将第一个参数类型改为int):

//生成n个T类型的的val值
vector(int n, const T& val = T())
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	reserve(n); //调用reserve函数将容器容量设置为n
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);//尾插
	}
}

如果不重载,使用vector<int> v(1,2)时会去调用迭代器区间构造函数定义对象,函数体内需要解引用造成程序崩溃。

使用迭代器区间进行对象的构造

迭代器区间可以是其他容器的迭代器区间,也就是该函数接收到的迭代器的类型是不确定的,所以需要一个函数模板,在函数体内将该迭代器区间的数据尾插到容器当中。

//使用迭代器区间构造
template<class InputIterator>//函数模板
vector(InputIterator first, InputIterator last)
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	//将迭代器区间在[first,last)的数据尾插到容器中
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

析构函数

对容器进行析构时先判断该容器是否为空容器,若为空容器,则无需进行析构操作,若不为空,则先释放容器存储数据的空间,然后将容器的各个成员变量设置为空指针。

~vector()
{
	if (_start)//判断空间是否为空
	{
        delete[] _start; //释放容器存储数据的空间
	_start = nullptr;//置空
	_finish = nullptr;//置空
	_end_of_storage = nullptr;//置空
	}
}

拷贝构造函数

拷贝构造函数涉及深拷贝问题:

传统写法:

先开辟一块与该容器大小相同的空间,然后将要拷贝容器当中的数据拷贝到新容器中,最后更新_finish_end_of_storage的值。

vector(const vector<T>& v)
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{
    _start = new T[v.capacity()];//开辟要拷贝数组的容量大小的空间
    for (size_t i = 0; i < v.size(); i++)
    {
        _start[i] = v[i];//将容器v中的数据赋值给新容器中
    }
    _finish = _start + v.size();//指向容器中有效数据的下一个位置
    _end_of_storage = _start + v.capacity();//指向结尾位置的下一个位置
}

注意:将容器中的数据拷贝过来的时候不能使用memcpy,因为memcpy是按照字节进行拷贝的,当vector容器中存储的是内置类型的时候使用memcpy函数进行复制不会产生错误,但是当vector容器中存储的是string这种自定义类型时,如果使用memcpy函数进行拷贝将会导致程序出现错误。

【C++】vector模拟实现,C++,c++,开发语言,后端

因为memcpy将已有的vector容器中的string类对象中的指针拷贝到新的vector容器的string中,导致两个vector容器中string指向的是同一个字符串。

for (size_t i = 0; i < v.size(); i++)
{
    _start[i] = v[i];//将容器v中的数据赋值给新容器中
}

而上面的拷贝构造代码中使用=将已有vector容器中的值赋值到新的容器中,即便是当遇到自定义类型时,也可以通过调用自定义类型对应的赋值运算符重载函数做到深拷贝。

【C++】vector模拟实现,C++,c++,开发语言,后端

如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

现代写法:

通过遍历将已有容器中的数据尾插到新的容器中。在使用范围for对已有容器进行遍历的时候,e是容器中数据的拷贝,如果是自定义类型会自动调用拷贝构造完成深拷贝,然后将e插入到新容器中。

vector(const vector<T>& v)
    :_start(nullptr)
    , _finish(nullptr)
    , _end_of_storage(nullptr)
{
    reserve(v.capacity());//开空间
    for (auto e : v)//范围for遍历
    {
        push_back(e);//尾插到新容器
    }
}

我们也可以使用迭代器区间构造函数:

vector(const vector<T>& v)
{
    vector<T> tmp(v.begin(),v.end());
    swap(tmp);//交换this指针
}

小提示:类里面写可以不加模板参数,但是还是建议加上,虽然库里面没加。

vector(const vector& v)//拷贝构造
vector& operator=(const vector& v)//赋值运算符重载

赋值运算符重载函数

赋值运算符重载也涉及到深浅拷贝。

传统写法:

先检查是否是自己给自己赋值,然后根据右操作数的容量开空间,然后再通过for循环进行赋值,注意这里也不能使用memcpy函数进行拷贝,最后更新_finish_end_of_storage,支持连续赋值,最后返回*this。引用返回减少拷贝。

vector<T>& operator=(const vector<T>& v)
{
    if (this != &v)//检查是否自己给自己赋值
    {
        delete[] _start;//释放之前的空间
        _start = new T[v.capacity()];
        for (size_t i = 0; i < v.size(); i++)
        {
            _start[i] = v[i];//赋值
        }
        //更新_finish和_end_of_storage
        _finish = _start + v.size();
        _end_of_storage = _start + v.capacity();
    }
    return *this;//支持连续赋值
}

现代写法:

现代写法中函数传参没有使用引用传参,而是使用传值传参,调用拷贝构造函数,然后将拷贝构造形成的对象和要赋值的对象通过swap函数进行交换,此时就已经完成了赋值操作。拷贝构造完成了深拷贝工作,所以这里我们不需要管。

vector<T>& operator=(vector<T> v)//编译器自动调用其拷贝构造函数
{
    swap(v);//交换这两个对象
    return *this;//支持连续赋值
}

迭代器函数

begin和end函数

begin函数返回指向容器中首个元素的指针,end函数返回指向容器中有效数据个数下一个位置的指针。

const函数和非const函数构成重载,以满足不同对象的需求。

//迭代器函数begin,返回指向第一个元素的指针
iterator begin()
{
    return _start;
}
const_iterator begin() const
{
    return _start;
}
//迭代器函数end,返回指向最后一个元素下一个位置的指针
iterator end()
{
    return _finish;
}
const_iterator end() const
{
    return _finish;
}

容量大小相关函数

capacity函数

capacity函数能够得到该容器当前所能存储的最多元素个数。

_end_of_storage - _start;指针减指针得到的是两者之间的元素个数,我们可以认为区间就是左闭右开的。

使用const常函数以满足当对象为const对象时的调用。

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

size函数

size函数能够得到当前容器中有效数据个数。

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

empty函数

判断容器是否为空

bool empty()
{
    return _start == _finish;//指向同一个位置即为空
}

reserve函数

扩容到指定大小。

reserve规则:
1、当n大于当前的capacity时,将capacity扩大到n。
2、当n小于当前的capacity时,什么也不做。

先判断所给n是否大于当前capacity,如果大于先记录当前有效数据个数size(),然后开辟能够存储n个T类型的空间,将原容器当中的数据拷贝到新空间中,再将原空间释放,并将新开辟的空间赋值给该容器,并且更新更新_finish_end_of_storage

void reserve(size_t n) 
{
    if (n > capacity())
    {
        size_t sz = size();//记录有效数据个数
        T* tmp = new T[n];//开空间
        //判断是否为空
        if (_start)
        {
            //如果不是空,将数据拷贝到新空间
            for (size_t i = 0; i < sz; i++)
            {
                tmp[i] = _start[i];
            }
            //拷贝完成之后销毁之前的空间
            delete[] _start;
        }
        //将tmp 赋值给 _start;
        _start = tmp;
        //更新_finish和_end_of_storage
        _finish = _start + sz;
        _end_of_storage = _start + n;
    }
}

特别注意的是在开空间赋值之前需要提前记录当前容器中有效数据的个数,并且赋值时也是深拷贝。

因为size的大小是根据_finish_start计算出来的,当我们开空间赋值之后 _start = tmp就已经改变了_start,那当我们要去更新_finish时,size的大小已经不是原来的数值了。

同样对于vector当中如果是自定义类型string,如果是浅拷贝那么delete[] _start;将会调用string的析构函数,将string指向的字符串销毁,所以在for循环中赋值运算符重载完成深拷贝。

resize函数

扩容并且初始化

resize规则:
1、当n大于当前的size时,将size扩大到n,初始化为val,若val未给出,使用缺省值(默认构造函数所构造出来的值)。
2、当n小于当前的size时,将size缩小到n。

先判断n是否小于容器当前的size,如果小于,可以通过改变_finish,将容器的size缩小到n;如果n大于size,先判断该容器是否需要扩容,然后再将扩大的空间初始化为val。

在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T()即可。

void resize(size_t n,const T& val = T())
{
    if (n < size())//当n小于size时
    {
        _finish = _start + n;//将size缩小到n
    }
    else
    {
        if (n > capacity())
        {
            reserve(n);//扩容
        }
        while (_finish < _start + n)//循环并且初始化
        {
            *_finish = val;//赋值
            _finish++;
        }
    }
}

这里要注意迭代器失效的问题,最经典的迭代器失效就是扩容造成的野指针问题导致的。

【C++】vector模拟实现,C++,c++,开发语言,后端

当扩容之后, _start _finish_end_of_storage都指向了新的位置,而指定的pos位置却没有更新,将会导致pos变为野指针。

修改操作相关函数

push_back函数

尾插

注意尾插时需要检查是否需要扩容。

void push_back(const T& val)
{
    //判断容器是否还有剩余空间
    if (_finish == _end_of_storage)
    {
        size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
        reserve(newcapacity); 
    }
    *_finish = val;//插入数据元素
    _finish++;	//向后移动一个位置
}

pop_back函数

尾删

void pop_back()
{
    //判断是否为空
    assert(!empty());
    _finish--;
}

swap函数

swap函数用于交换两个容器中的数据,实现时可以调用库当中的swap函数进行交换。

void swap(vector<T>& v)
{
    //调用库中的函数进行交换
    std::swap(v._start, _start);
    std::swap(v._finish, _finish);
    std::swap(v._end_of_storage, _end_of_storage);
}

如果在swap函数前加上::(作用域限定符),就是告诉编译器优先在全局范围查找swap函数,如果没有加编译器会认为调用的就是正在实现的swap函数(就近原则)。

void swap(vector<T>& v)
{
    //调用库中的函数进行交换
    ::swap(v._start, _start);
    ::swap(v._finish, _finish);
    ::swap(v._end_of_storage, _end_of_storage);
}

insert函数

insert函数是在指定的迭代器位置插入数据。需要先判断位置是否合法,然后移动数据,最后再插入。

iterator insert(iterator pos, const T& val)
{
    //检查合法性
    assert(pos <= _finish);
    assert(pos >= _start);
    //判断是否需要扩容
    if (_finish == _end_of_storage)
    {
         size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
        reserve(newcapacity);

    }
    //挪动数据
    iterator end = _finish;
    while (end > pos)
    {
        *end = *(end - 1);
        end--;
    }
    *pos = val;//指定位置插入数据
    _finish++;//更新_finish位置
    return pos;
}

但是上面的代码存在问题,因为当需要扩容的时候存在迭代器失效问题,pos迭代器没有更新,而其他的指针都更新了,所以在扩容之前我们需要记录一下pos的位置,完善之后代码如下:

iterator insert(iterator pos, const T& val)
{
    assert(pos <= _finish);
    assert(pos >= _start);
    //判断是否需要扩容
    if (_finish == _end_of_storage)
    {
        size_t len = pos - _start;//记录pos的相对位置
        size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
        reserve(newcapacity);
        pos = _start + len;//更新pos位置
    }
    iterator end = _finish;
    while (end > pos)
    {
        *end = *(end - 1);
        end--;
    }
    *pos = val;
    _finish++;
    return pos;
}

我们在函数体内把pos更新了,但是因为是传值传参所以在函数外面并没有改变pos指针,而且传递pos时不能使用引用传参,当insert第一个参数是begin()时,因为 begin() 是传值返回,是使用临时变量来返回的,而临时变量具有常性,不可被改变,因此如果在 insert 函数中的参数 pos 使用传引用传参,就会造成权限放大的问题,我们可以使用返回值的方法解决这个问题,返回的是迭代器,指向新插入元素的位置。

insert 函数设置返回值,这样做是为了在调用完 insert 函数后,能够重新对迭代器进行赋值,避免迭代器失效。

当迭代器每次使用完之后要重新赋值才能使用,不然很容易出现迭代器失效问题。

erase函数

删除指定迭代器位置的数据。

iterator erase(iterator pos)
{
    //判断合法性
    assert(pos >= _start);
    assert(pos < _finish);
    iterator begin = pos + 1;
    //挪动数据
    while (begin != _finish)
    {
        *(begin - 1) = *begin;
        begin++;
    }
    --_finish;//更新_finish
    return pos;//返回pos
}

erase函数会存在迭代器失效问题,所以我们认为erase之后,pos失效,行为结果未定义(跟具体编译器实现有关)。

erase删除 pos 位置元素后, pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不会失效,但是要考虑到一种特殊情况:如果 pos指向的是最后一个元素,删完之后 pos 指向的位置刚好是_end_of_storage指向的位置,而该位置是没有元素的,那此时 pos 就失效了。所以在库中认为使用erase后,迭代器就失效了。

erase 函数设置返回值,是为了在调用完erase 函数后,能够重新对迭代器进行赋值,避免迭代器失效。

元素访问函数

[]运算符重载

重载[]运算符能够让该容器通过[]+下标的方式访问元素。

需要重载一个const函数,const对象只能够访问对象而不能修改,所以返回值也要改为const T&,使用引用是为了减少拷贝。文章来源地址https://www.toymoban.com/news/detail-534678.html

T& operator[](size_t pos)
{
    assert(pos < size());//检测下标是否合法
    return _start[pos];//返回当前下标元素
}

//重载一个const函数
const T& operator[](size_t pos) const
{
    assert(pos < size());//检测下标是否合法
    return _start[pos];//返回当前下标元素
}

到了这里,关于【C++】vector模拟实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++ STL】vector模拟实现

    2023年05月17日
    浏览(51)
  • 【C++】vector容器的模拟实现

    目录 一,框架设计 二,构造函数 三,析构函数 四,赋值运算符 五,容器接口的实现 1,迭代器实现 2,“ [] ”运算符的实现 3,swap交换和resize重设大小 4,insert插入和erase删除 介绍:         本文,我们重点实现vector容器的用法,这里要注意的是vector容器可以接纳任意类

    2024年02月02日
    浏览(55)
  • C++——vector类及其模拟实现

    前言:前边我们进行的string类的方法及其模拟实现的讲解。这篇文章将继续进行C++的另一个常用类——vector。 vector和string一样, 隶属于C++中STL标准模板库中的一个自定义数据类型 ,实际上就是 线性表 。 两者之间有着很多相似,甚至相同的方法 。 但是它们也有着很大的不

    2024年04月13日
    浏览(41)
  • 【C++】vector模拟实现及其应用

    vector是表示可变大小数组的序列容器。 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。 本质讲,vector使用动态分

    2023年04月25日
    浏览(47)
  • C++ STL vector 模拟实现

    ✅1主页:我的代码爱吃辣 📃2知识讲解:C++之STL 🔥3创作者:我的代码爱吃辣 ☂️4开发环境:Visual Studio 2022 💬5前言:上次我们已经数字会用了vector,这次我们对其底层更深一步挖掘,其中重点是,Vector中一些深浅拷贝问题。 目录 一.Vector模拟实现的整体框架 二. Vector的构

    2024年02月13日
    浏览(35)
  • C++中的vector类模拟实现

    目录 vector模拟实现 vector类设计 vector类构造函数 vector类根据个数构造函数 vector类根据迭代器区间构造函数 vector类拷贝构造函数 vector类赋值运算符重载函数 vector类析构函数 vector类获取有效数据个数函数 vector类获取容量大小函数 vector类begin()函数 vector类end()函数 vector类reser

    2024年04月13日
    浏览(38)
  • C++:vector使用以及模拟实现

    和我们原来讲的string不同, vector并不是类,是一个类模板,加类型实例化以后才是类。 vector是表示 可变大小数组 的序列容器。 像数组一样 ,vector也采用的连续存储空间来存储元素,但是容量可以动态改变。 和其它容器相比,vector访问元素、尾插、尾删较高效,但不在尾部

    2024年02月11日
    浏览(43)
  • 【C++】vector模拟实现+迭代器失效

    铁汁们,今天给大家分享一篇vector模拟实现 + 迭代器失效,来吧,开造⛳️ 指向最后一个空间的下一个位置 💡 iterator _endofstorage 指向存储第一个有效数据空间的位置 💡 iterator _start 指向存储最后一个有效数据空间的下一个位置 💡 iterator _finish 在成员变量声明处给缺省值,

    2024年02月21日
    浏览(36)
  • C++ [STL之vector模拟实现]

    本文已收录至《C++语言》专栏! 作者:ARMCSKGT vector是STL容器容器之一,其底层实现类似于数据结构顺序表,相当于string来说得益于泛型模板的加持使得vector可以变为任何类型,且是可以动态扩容,堪称大号数组!在vector的实现中,有许多值得我们学习的细节,接下来将为大家

    2024年02月11日
    浏览(42)
  • 【C++】vector的使用及模拟实现

    vector是一个可变大小数组的容器,与数组一样,vector也是一块连续的空间,可以像数组一样对元素进行高效的遍历访问,但是普通数组的大小是不变的,vector可以改变自身大小。vector是采用动态分配数组来存储数据,即插入新元素时要改变存储空间大小,往往要分配一个新的

    2024年01月16日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包