【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指向的是整个数组的下一个位置。

image-20230629162815227

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

image-20230629163530917

所以当我们模拟实现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函数进行拷贝将会导致程序出现错误。

image-20230630162643835

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

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

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

image-20230630163345276

如果对象中涉及到资源管理时,千万不能使用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++;
        }
    }
}

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

image-20230630232721532

当扩容之后, _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&,使用引用是为了减少拷贝。

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];//返回当前下标元素
}
  • 43
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 38
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不 良

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值