【C++从入门到踹门】 第八篇:vector的实现


在这里插入图片描述


vector的实现

成员变量

vector具有动态增容的功能,故我们用三个指针分别指向所申请空间的三个关键位置:

template<class T>
class myvector
{
    typedef T* iterator;//vector所申请的是连续空间,迭代器是原生指针 
private:

    iterator _start;//指向容器的头
    iterator _finish;//指向容器有效数据的结尾
    iterator _endofstorage;//指向容器的结尾
};

构造函数

🚩默认的无参构造函数

//构造函数
myvector() :_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{

}

🚩迭代器区间构造函数

迭代器的类型并不局限于当前类型,可以是其他类型的迭代器->函数模板。
传入的模板迭代器为只读迭代器Input iterator。

template<class InputIterator>
myvector(InputIterator first, InputIterator last) 
    :_start(nullptr)
    , _finish(nullptr)
    , _endofstorage(nullptr)
{
    while (first != last)
    {
        push_back(*first);//注意这里的push_back为成员函数实现尾插功能,将会在之后实现。
        first++;
    }
}

🚩支持fill多个相同元素的构造函数

将n个值为val的数据填充容器:使用reserve扩容,再尾插进容器。相比于直接使用尾插逐步扩容,率先reserve开辟出足够的空间拥有更高的效率,

vector(size_t n, const T& val)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(n); //调用reserve函数,后续会实现
	for (size_t i = 0; i < n; i++) 
	{
		push_back(val);
	}
}

拷贝构造函数

🚩方法1:①开辟空间②复制被拷贝对象的内容

myvector(const myvector& v):_start(nullptr),_finish(nullptr),_endofstorage(nullptr)
{
    //开辟空间
    _start=new myvector[v.capacity()];
    _finish=_start+v.size();
    _endofstorage=_start+v.capacity();
    //从v拷贝元素
    int sz=v.size();
    for(int i=0;i<sz;++i)
    {
        _start[i]=v._start[i];
    }
}

这里有个问题,能不能用memcpy函数将v的内容一股脑地拷贝过来呢?

答案是不行的!❌

思考以下拷贝myvector<string>的情况

面对vector的内容是一个开辟了空间并指向其空间的指针,我们单纯使用memcpy只是浅拷贝。

memcpy会把内容拷贝过去,但是string的成员变量指向的是开辟的堆空间,那么两个myvector将指向同一空间,析构时,这片将会被释放两次。

⭐正确的拷贝正如我们上面的代码演示,将内容用赋值符=一个一个拷贝过来,即使是一个自定义类,只要定义了赋值运算符重载,也可以使用其将内容深拷贝过来,这点相当关键!

🚩 方法2:复用构造函数

我们之前定义了使用迭代器区间的构造函数,于是在拷贝构造中,我们可以复用构造函数:

① 用被拷贝对象的首尾迭代器构造出对象temp

② 再交换*this和temp两者的成员变量.

myvector(const myvector& v)
    :_start(nullptr)
    ,_finish(nullptr)
    ,_endofstorage(nullptr)
{
    myvector<T> temp(v.begin(),v.end());
    swap(tmp);
}

//自己造一个swap,如果利用库中swap将会调用三次拷贝构造,代价极大
void swap(myvector<T>& x)
{
    ::swap(_start, x._start);
    ::swap(_finish, x._finish);
    ::swap(_endofstorage, x._endofstorage);
}

⚠注意:需要用nullptr初始化 _start ,否则若 _start 为随机值,然后换给temp,最后temp析构的将是野指针!!

很狡猾的做法,让temp当了工具人。

赋值运算符重载函数

🚩复用拷贝构造(传引用)

依旧是利用工具人temp

//交换指针(传引用)
vector<T>& operator=(const vector<T>& v)//传引用  
{
	if (this != &v)
	{
		vector<T> tmp(v);//复用了拷贝构造 
		swap(tmp);//由于形参在函数结束时销毁(析构),单纯的赋给this是不行的,需要交换指针
	}
	return *this;
}

🚩复用拷贝构造(传值)

这里的工具人找到了吗,藏得很巧妙哦🤭。

// 3.交换指针(传值)
vector<T>& operator=(vector<T> v)//传参复用了拷贝构造  
{
    swap(v);
    return *this;
}

析构函数

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

迭代器相关函数

iterator begin()
{
    return _start;
}

iterator end()
{
    return _finish;
}

const_iterator begin()const
{
    return _start;
}

const_iterator end()const
{
    return _finish;
}

容量相关函数

size 、capacity

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

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

reserve resize

reserve 扩容,内容需要转移到新空间上,与拷贝构造思想近似,考虑到深拷贝,不能使用memcpy。

void reserve(size_t n)
{
    size_t  old_size= size();
    if (n > capacity())
    {
        T* tmp = new T[n];
        if (_start)
        {
            //memmove(tmp, _start, old_size*sizeof(T));
            //针对string类型,memmove没法拷贝深层的堆空间
            for (size_t i = 0; i < old_size; ++i)
            {
                tmp[i] = _start[i];//这里利用了类自身的赋值运算符重载,实现了每个元素的堆空间的深拷贝
            }
            delete[] _start;//勿忘释放旧空间避免内存泄漏!
        }
        _start = tmp;
        _finish = _start + old_size;
        _endofstorage = _start + n;
    }
}


void resize(size_t n,const T& val=T())//注意:C++中的匿名对象是pure RValue(纯粹的右值),引用匿名对象一定要在前用const修饰,如果生成的匿名对象在外部有对象等待被其实例化,此匿名对象的生命周期就变成了外部对象的生命周期。
{
    
    if (n < size())
    {
        _finish = _start + n;
    }
    else
    {
        if (n > capacity())
        {
            reserve(n);
        }
        while (_finish < _start + n)
        {
            *_finish = val;
            ++_finish;
        }
    }
}

empty

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

修改内容

push_back 、 pop_back

void push_back(const T& x)
{ 
    //检查空间
    if (_finish==_endofstorage)
    {
        size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
        reserve(newcapacity);
    }
    *_finish = x;
    ++_finish;
}

void pop_back()
{
    assert(!empty());
    --_finish;
}

insert 、 erase

void Insert(iterator pos, const T& x)
{
    assert(pos >= begin() && pos <= end());
    if (_finish == _endofstorage)
    {
        size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
        //这里防止换新空间后迭代器pos失效,需要实现记录下pos在数组中的相对位置
        int delta = _finish - pos;
        reserve(newcapacity);
        //更新pos,但这里只是改变了形参pos,真正的实参pos将失效。可以传入pos的引用改变实参,但这样也会导致其他问题
        pos = _finish - delta;
    }
    iterator i = _finish-1;
    while (i >= pos)
    {
        *(i + 1) = *i;
        --i;
    }
    *pos = x;
    ++_finish;

    //防止pos的意义改变,需要更新pos至原先指向的值,那么就是往后一格
    //pos = pos + 1;
}
    
void Insert(iterator pos, size_t n, const T& x)
{
    assert(pos >= begin() && pos <= end());
    if (_finish == _endofstorage || size()+n>capacity())
    {
        size_t newcapacity = capacity() == 0 ? n : n+capacity();
        
        int delta = _finish - pos;
        reserve(newcapacity);
        
        pos = _finish - delta;
    }
    memmove(pos + n, pos, sizeof(T) * (_finish-pos));
    size_t i = 0;
    while (i < n)
    {
        *(pos + i) = x;
        i++;
    }
    _finish+=n;
}

iterator erase(iterator pos)
{
    assert(pos >= begin() && pos < end());
    iterator it = pos+1;
    while (it < _finish)
    {
        *(it - 1) = *it;
        it++;
    }
    --_finish;
    return pos;
}

opeartor[]

operator需要重载一个const版本,只读不修改。

T& operator[](size_t i)
{
    assert(i < size());
    return _start[i];
}

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

迭代器的失效

在使用std迭代器的时候,如果使用insert/erase造成的元素增减,导致迭代器的指向意义不明(究竟是指向原位置还是原来的元素),从而使得迭代器失效。

设想这样的一种情况,我们需要删除一个vector 的数组中的偶数元素

测试用例:

v1: 1 2 3 4 5
v2: 1 2 4 5
v3: 1 2 3 4

我们代码设计如下:利用迭代器去遍历数组,找到偶数删除

vector<int>::iterator it=v.begin();
while(it!=v.end())
{
    if(*it%2==0)
    {
        v.erase(it);
    }
    ++it;
}

for(auto& e:v)
{
    cout<<e<<' ';
}cout<<endl;

我们在g++编译器下分别对v1、v2、v3用例进行测试并输出,得到结果:

v1结果:输出正常

v2结果:解答错误,数组中仍然残留偶数元素

v3结果:段错误,指访问的内存超过了系统所给这个程序的内存空间

显然代码是错误的,不仅v2解答错误还会产生v3的执行错误,只能说v1能通过是歪打正着。

代码错误的地方就在于,迭代器指向的意义不明确

以 v1:1 2 3 4 5 为例:

当迭代器指向2的时候,元素是顺利删除了,接下来迭代器指向的是3这个位置(vector自动将后续元素顶替了上来,然而编写这段代码的用户可没这么认为),但是呢我们之后又对迭代器+1,这就使得3这位元素侥幸逃脱了判定,迭代器直接指向了4。

这就是为何当有两个相邻的偶数元素时(v2),其中一个偶数就会逃脱判定。

更坏的情况在于,如果末尾元素是偶数元素时,按照上述所言,迭代器依旧连进两步,这会导致迭代器访问的越界。

如果这段代码放在vs中检测,会直接报错,因为vs编译器检查的更严格,它认为迭代器的指向和用户的设想会造成歧义。

那我们在else中++it呢?应该考虑到有些编译器中的erase函数会有缩容量的情况,此时不及时更改迭代器,会导致迭代器成为野指针。

所以应实时给予正确的迭代器指向,
erase函数返回值是将指向删除元素后面的那个值的迭代器,那我们就以此重新给迭代器赋值:

vector<int>::iterator it=v.begin();
while(it!=v.end())
{
    if(*it%2==0)
    {
        it=v.erase(it);//实时更新迭代器
    }
    else
    {
        ++it;
    }
}

for(auto& e:v)
{
    cout<<e<<' ';
}cout<<endl;

这段代码,无论在什么环境下都能完成遍历的任务且不造成迭代器因为缩容而失效的情况。


青山不改 绿水长流

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值