C++STL详解(四)—— vector模拟实现

vector内置成员变量

_start: 容器的初始位置。

_finish: 容器最后一个数据的下一个位置。

_endofstorage: 指向容器的尾部,可比作容量。

这三个内置成员都代表有关vector位置的地址。
在这里插入图片描述

默认成员函数

初始化列表构造

vector()
			:_start(nullptr)  //初始值为nullptr
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
		}

迭代器区间构造函数

1: 在类模板里面又创建一个函数模板(为了能传递迭代器区间),这个模板最好使用除iterator之外的迭代器名字,因为可以传入多种容器数据类型,而不是只能像iterator 一样只能用vector容器数据类型。

2: 在迭代器区间构造本质是将其他容器的数据给定区间范围去构造一个新的vector,所以将该容器数据存放在vector之前一定要
将vector进行初始化,以防在push_back扩容时delete _start为
野指针。

template <class InputIterator>
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

赋个数赋值构造函数

C++对于内置类型也具有构造函数,它的默认构造函数的缺省值为0,val = T()就像相当于让内置类型的匿名对象去作缺省值,
在这里插入图片描述

vector(size_t n, const T& val = T())
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				push_back(val);
			}

赋值构造的相关问题

1:如果我们对vector 传入10时,同过int()作为缺省值可推断
出插入了10个0;
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
2:如果我们用10个1去构造vector v时,却发现程序报错。

3:如果我们用10个char类型时,却发现程序运行成功!

在这里插入图片描述
原因分析
情况1和情况3编译器调用构造匹配的时半缺省构造函数,只用传一个参数进行构造。

在这里插入图片描述
原因分析
情况2是因为我们传的实参为int,int类型,但是函数形参为unsign int ,int类型,编译器此时不会匹配半缺省值的构造
函数,而是会选择迭代器区间构造(实参类型相同),在传入的过程中模板参数
Inputlerator就会实例化为int,int 类型,最后在push_back中对
int类型解引用造成编译错误。
在这里插入图片描述
解决办法
我们可以增加一个与实参类型相匹配的函数重载,那么在调用时编译器就会调用相匹配的函数。

vector(int n,const T& val = T())
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)]
{
        reserve(n);
        for( size_t i = 0; i < n; ++i)
        {
        push_back(val);
        } 
}

拷贝构造函数

传统写法
复用push_back()并选择范围for将v中的数据循环放入到新构造的vector中。

vector(const vector <T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			reserve(v.size());
			for (auto e : v)
			{
				push_back(e);
			}

现代写法
新开辟一块空间,将数据依次赋值到新拷贝的vector容器中(深拷贝),但是我们不能用memcpy进行拷贝(浅拷贝),因为对于内置类型,深拷贝和浅 拷贝没有什么问题。但是如果vector存的类型为自定义类型(例如vector)的时候,此时选择浅拷贝进而将该自定义类型依次拷贝到vector容器中,此时vector容器中的自定义类型中的_start以及被拷贝对象的_start都指向同一块空间,当程序运行完毕的时候,编译器分别调用两次析构函数,此时,同一块空间被销毁两次进而造成程序错误。所以,对于内置类型编译器调用值拷贝,对于内置类型赋值(为深拷贝)

//传统写法:
		vector(const vector<T>& v)
		{
			_start = new T[v.size()];
			for (int i = 0; i < v.size(); ++i)
        {
			_start[i] = v[i];//将vv中的vector<int>依次赋值给ret中,赋值为深拷贝。
			}
			_finish = _start + v.size();
			_end_of_storage = _start + v.size();
		
		}

赋值运算符重载函数

传统写法
首先要判断能不能给自己赋值,如果是给自己赋值则不需要操作,如果不是给自己赋值,则需要将原来的空间删除,重新开辟与vector 容器相同的空间,再将容器vector v 的数据一个个拷贝出来(深拷贝),最后更新vector中的内置成员即可。

vector<T>& operator=( const vector<T>& v)
{
        if( this != &v )
        {
           delete[] _start;
           _str = new T[v.capacity()];  //开辟一个和v同样大小的空间。
           for( size_t i = 0; i < v.size();++i)
           {
           //如果是内置类型就为值拷贝,如果是自定义类型就为深拷贝。
                    _start[i] = v[i];
           }
           _finish = _start + v.size(); //最后一个数据的下一个的下一个位置。
           _endofstorage = _start + v.capacity(); //容器的末尾。
        }=
           return *this;            //返回被赋值的类,并且能够支持连续赋值。
}

现代写法一
传值传参
现代写法在传参中没有引用传参,而是将实参传递的形参嗲用拷贝构造作为 ”中间人“ ,在函数调用结束结束时编译器会调用这个”中间人“的析构函数进行析构。

vector<T>& operator=( vector<int> v ) //传参时编译器调用拷贝构造函数,此时v为实参的拷贝。
{
          swap(v);
          return *thsi //支持连续赋值。
}

现代写法二
引用传参
现代写法中引用传参,我们还是需要这个"中间人" ,所以我们可以采用迭代器区间构造先构造一个与v相同的容器tmp,
然后再将tmp与自己交换就行了。

vector ( const vector<T>& v)
{
     //先迭代器区间构造。
     vector<T> tmp( v.begin(),v.end());
     swap(tmp);
}

析构函数

对容器进行析构时,我们首先要判断该容器是否为空,如果为空的话,我们就不需要删除,如果不为空,则需要先释放该容器空间的,再将该容器的内置成员设为nullptr即可。

~vector()
{
   //判断_start是否为空,防止对空指针进行释放。
   if( _start )
    {
         delete[] _start;
         _start = nullptr;
         _finish = nullptr;
         _endofstorage = nullptr;
    }
}

迭代器及迭代器相关函数

迭代器实际就是容器中指向数据类型的指针,在vector中分为普通迭代器和const 迭代器。(原因下面内容详解)

typedef T* iterator;
typedef const T* const_iterator;

begin和end

begin()和end()函数分别获得vector的首地址和最后一个数据的下一个位置的地址。

iterator begin()
{
     return _start;
}
iterator end()
{
     return _finish;
}

另外,因为普通迭代器iterator返回值既能读又能写,而vector容器中还规定了返回值只能读不能写的迭代器,为了让普通迭代器调用普通begin()和end(),让const迭代器调用const修饰的begin()和end().

const_iterator begin() const   //返回值不能被修改。
{
        return _start;
}
const_iterator end() const    //返回值不能被修改。
{ 
        return _finish;
}

范围for

当vector支持迭代器时,我们就可以使用范围for遍历容器了,
因为此时编译器会主动将范围for形式编译成迭代器形式。

//迭代器形式
vector<int> v( 6,6);
auto it = v.begin();
whil( it != v.end() )
{
     cout << *it <<" ";
}
cout <<endl;
//范围for形式
vector<int> v( 6,6);
for( e : v )
{
     cout <<e <<" ";
 }
 cout<<endl;

容量与扩容相关函数

size和capacity

在这里插入图片描述
由图可得,数据个数size可有_finish - _start可得,由于返回类型为size_t 类型的,编译器会主动将_fiish— _start的地址差转换为个数size。容量capacity由_endofstorage - _start获取。

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

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

reserve

当n实参n大于该对象当前的容量capacity时,将容量扩大到n或者大于n.
当传递的实参n小于对象的capacity时,不进行扩容。
reserve函数实现思路
扩容前首先要判断n与capacity的关系,当n>capacity,并且为空容器,则开辟新的空间,将原来的空间进行释放,更新内置成员。
当n > capacity,并且不为空容器,则需要对数据进行赋值拷贝,否则不用。然后将原来的内存空间进行释放,更新vector内置成员就行。

void reserve(size_t n)
{
     if ( n > capacity() )
     {
           size_t sz = size();
           //开辟新空间。
           T* tmp = new T[n];
           if( _start )                 //如果为空容器,则不用将旧数据转移到新空间中。
           {
               for ( int i = 0; i < size(); ++i )
               {
               //将就空间数据一个个赋值给新空间。
                    tmp[i] = _start[i];
               }
               //将就空间删除。
               delete[] _start;
           }
           //更新内置成员
           _start = tmp;
           _finish = _start + sz;
           _endofstorage = _start + n;
           
     }
}

注意
空指针问题
(1)在扩容之前应该提前保存当前容器数据的个数:
如果不提前保存,当vector容器为空调用push_back第一次扩容时,因为_start = tmp,
_finish = tmp + size(); 而size = _finish - _start;
经过计算的,此时的_finish = _finish; 而空容器的_finish = nullptr. 在之后的push_back解引用时会造成解用nullptr造成程序崩溃。
所以,我们必须保存原来的size(),不能让计算过程中size发生改变。

vector存储自定义而类型浅拷贝问题
在reserve过程中,拷贝旧数据也有可能发生自定义类型的浅拷贝,所以我们不能用memcpy函数进行拷贝,而应该采用赋值拷贝,此为深拷贝。

erserve后[]问题
使用reserve之后最好不能直接使用[]访问,因为resize开辟空间不会改变size,如果直接调用的话,很有可能会因为assert断言造成错误。

resize

规则
1: 如果n > capacity , 需要扩容 + 初始化。

2: 如果n > size 且 n <= capacity ,只需要在最后一个数据的下一个数据开始初始化。

3:如果 n < size, 删除数据。

void resize( size_t n, const T& val = T())
{
        if ( n > capacity() )
        {
              reserve(n);
        }
        //只需要初始化
        if( n > size())
        { 
             //从_finish开始,_start + n结束
             while( _finish < _start + n )
             {
                     *finish = val;
                     ++finish
             }
        }
//以上两个if包含了resize两种情况。
        else
        //只需要调整_finish就可以了,循环遍历的时候就访问不到了。
        {
            _finish = _start + n;
        }
}

empty

如果vector容器中_start 等于 _finish就说明容器为空。

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

vector中的增删查改:

push_back

尾插首先要判断容器是否已满,如果没满则只需要将数据尾插,如果满了得先扩容再将数据进行尾插。

void push_back( const T& x )
{
       if( _finish == _endofstorage )
       {
            //如果容量为null,则容量直接为4,如果不为空,则直接扩容两倍。
             size_t newcapacity = capacity() == 0? 4:2*capacity();
             reserve(newcapacity);
       }
       *_finish = x;
       ++_finish;
}

pop_back

尾删数据之前必须得判断容器是否为空,如果为空则断言,如果不为空则_finish–让这个数据无法访问即可。

void pop_back() 
{
//容器的数据的个数不能为0;
     assert( !empty());
     --_finish;
{

insert

inser函数可以指定位置插入数据,在插入之前一定要考虑插入的范围插入并且一定要判断是否需要扩容,pos位置后面的数据(包括pos)统一向后移动一位,最后将数据插入到pos位置就可以了。

 iterator insert( itderator pos, const T& x)
 {
         //范围一定要在_start和_finish之间,左闭右闭区间。
         assert(pos >=_start);
         assert(pos <= _finish);
         if( _finish == _end_of_storage )
         {
                size_t len = pos - _start;
                reserve( capacity() ==0? 4:2*capacity()*2);
                pos = _start + len;
         }
         iterator end = _finish;
         while( end >= pos )
         {
               *end = *(end-1);
               end--;
         }
         *pos = x;
         ++_finish;
         return pos;
 }

erase

erase函数可以删除pos位置的数据,在删除时判断vector容器必须有数据并且删除位置必须合法。然后将数据从pos+1位置开始,将前一个数据前移删除的,最后更新以下_finish就行。

        iterator erase( iterator pos )
        {
              assert( pos >= _start );
              assert( pos < _finish );
              assert( !empty());
              iterator begin =  pos +1;
              while( begin < _finish )
              {
                   *(begin-1) = *begin;
                    ++begin;
              } 
              --_finish;
              return pos;
        }

swap

为了vector中拷贝构造现代写法中的交换自定义类型,我们们可以复用std中的vector写出合适的交换函数,对于自定义类型交换,最后不要直接用std库里面的。

void swap( vector <T> v)
{
      std::swap( _start,v._start);
      std::swap( _finish,v._finish);
      std::swap(_endofstorage,v._endofstorage);
}

访问容器函数

operator[]

vector容器支持用户使用下标进行访问,返回值是对访问数据的引用,如果是const修饰的容器,则调用返回值只可读不可以写的操作符,如果是普通容器,则说明它的数据既可以读也可以写。


```cpp
T& operator[](size_t i)
{
	assert(i < size());              //访问下标应该小于size下标。 

	return _start[i]; 
}
const T& operator[](size_t i)const
{
	assert(i < size());             //访问下标应该小于size下标。 
	return _start[i]; 
}


front和back

在C++11中,vector容器还支持获取迭vector容器中的头元素和尾元素支持可读可写,前提是必须要有数据,当然如果返回的首尾数据可读而不可写秩序要加上const就行。

        T& front()
		{
			assert(size() > 0);
			return *_start;
		}
		T& back()
		{
			assert(size() > 0);
			return *(_finish - 1);
		}
//const v容器调用
      const T& front() const
		{
			assert(size() > 0);
			return *_start;
		}
		const T& back()  const
		{
			assert(size() > 0);
			return *(_finish - 1);
		}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

暂停更新

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

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

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

打赏作者

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

抵扣说明:

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

余额充值