库中如何实现vector

🎈个人主页:🎈 :✨✨✨初阶牛✨✨✨

🐻推荐专栏1: 🍔🍟🌯C语言初阶

🐻推荐专栏2: 🍔🍟🌯C语言进阶

🔑个人信条: 🌵知行合一

🍉本篇简介:>:模拟实现STL库中的vector部分接口,帮助大家更好的理解vector.

vector 底层框架

cpp 复制代码
namespace cjn//命名空间,否则可能会与库中的冲突
{
    template<class T>	//容器是可以存储很多不同类型的,所以采用模板
    class vector
    {
    public:
        //迭代器
        typedef T* iterator;
        typedef const T* const_iterator;
        
        //成员函数
        //....

    private:
        iterator _start; 			// 指向数据块的开始
        iterator _finish; 			// 指向有效数据的尾
        iterator _end_of_Storage; 	// 指向存储容量的尾
    };
}

我们模拟实现的时候,其中成员变量可以采用缺省值方式更加方便.

cpp 复制代码
	private:
	    iterator _start=nullptr; 
	    iterator _finish=nullptr;
	    iterator _end_of_Storage=nullptr; 

一、构造函数实现:

(1) 默认构造函数

cpp 复制代码
// 默认构造
   vector()
       :_start(nullptr)
       , _finish(nullptr)
       , _end_of_Storage(nullptr)
   {
   }

由于我们已经给出了缺省值,所以可以不必要用初始化列表.

cpp 复制代码
   vector() { }

(2) 初始化为n个值

本应该开空间,然后再将数据插入进容器vector,此处我们复用resize函数的一种.就不需要自己再手撕一遍了.

cpp 复制代码
  //初始化为n个值
   vector(int n, const T& value = T())
   {
       resize(n, value);
   }

(3) 迭代器区间初始化

因为迭代器可能是原生指针,比如:数组的地址区间

也可能是别得复杂的迭代器,例如: vector<string>,这里采用再套一层模板,这样无论是什么形式的迭代器,总支持迭代++和 迭代区间[begin,end)

需要理解迭代器的特性.

cpp 复制代码
  //迭代器区间初始化
  template<class InputIterator> //类模板中的模板
  vector(InputIterator begin, InputIterator end)
  {
      while (begin!= end)//利用传过来的迭代器区间进行一个个尾插
      {
          push_back(*begin);
          ++begin;
      }
  }

(2) 拷贝构造 (重点,重点,重点)

拷贝构造是一种使用很频繁的构造方式,这里还存在一个复杂的问题:

外部深拷贝,内部浅拷贝的复杂问题.

方案1

cpp 复制代码
 //拷贝构造
   vector(const vector<T>& v)
   {
       _start = new T[v.capacity()];		//开辟空间
       memcpy(_start, v._start, sizeof(T)*v.size());//拷贝数据
       
       //更新_finish和_end_of_Storage 
       _finish = _start + v.size();
       _end_of_Storage = _start + v.capacity();
   }

普通类型使用此方案的拷贝构造没有问题.

cpp 复制代码
	int a3[10] = { 1,3,4,5,6,7,8,98,100,11 };
	cjn::vector<int> v3(a3, a3 + 10);//注意这里给的是+10,因为迭代器的end是指向最后一个有效元素的下一个位置,左闭右开
	//拷贝构造
	cjn::vector<int> v4(v3);
	cout << "v4=";
	for (auto it : v4)
	{
		cout << it << " ";
	}
	cout << endl;

运行结果:

v3=1 3 4 5 6 7 8 98 100 11

v4=1 3 4 5 6 7 8 98 100 11

但是对于稍微复杂的类型,成员本身也有动态开辟空间,例如:string类时

cpp 复制代码
void test4()
{
	string s1("hello world");
	string s2("hello csdn");
	string s3("hello cjn");
	string s4("hello 1234");
	cjn::vector<string> v1;
	v1.push_back(s1);
	v1.push_back(s2);
	v1.push_back(s3);
	v1.push_back(s4);

	for (auto it : v1)
	{
		cout << it << endl;
	}

	cout << endl;
	//拷贝构造
	cjn::vector<string> v2(v1);
	cout << "v4=";
	for (auto it : v2)
	{
		cout << it << endl;
	}
	cout << endl;
}

为什么运行不起来,会报错呢?

因为, _start = new T[v.capacity()];

使得外部的确完成了深拷贝,使得v1v2_start 指向了不同的空间.

但是像string类的成员自己也有动态申请空间,而内部采用
memcpy(_start, v._start, sizeof(T)*v.size());

则是按值进行的浅拷贝.

方案2:

cpp 复制代码
 //拷贝构造
 vector(const vector<T>& v)
 {
     _start = new T[v.capacity()];
     for (size_t i = 0; i < v.size(); i++)//vector<string> 等里面的数据存在动态内存开辟时可以利用本身的赋值赋值运算符重载进行深拷贝
     {
         _start[i] = v._start[i];
     }

     _finish = _start + v.size();
     _end_of_Storage = _start + v.capacity();
 }

_start[i] = v._start[i];(重点)

这可不是简单的赋值,而是调用了自己的(这里是string类)的赋值运算符重载,这就完成了内部的深层拷贝.

运行结果:

cpp 复制代码
hello world
hello csdn
hello cjn
hello 1234

v4=hello world
hello csdn
hello cjn
hello 1234

二、容量操作

这两个函数比较简单,可以根据文章开头的vector底层框架结构图分析,计算方式是根据这样设计的.

(1) size()与capacity()

cpp 复制代码
   size_t size() const
   {
      return _finish - _start;
   }

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

(2) reserve() (重点)

resize:改变容量大小

cpp 复制代码
void reserve(size_t n)
{
    if (n > capacity())//先判断是否需要扩容
    {
        //开辟新空间
        T* tmp = new T[n];
        //拷贝旧数据
        //memcpy(tmp, _start, sizeof(T) * sz);	//这样内部就浅拷贝了
        
        //实现外部和内部都是深拷贝
        for (size_t i = 0; i < sz; i++)
        {
            tmp[i] = _start[i];
        }
        delete[] _start;
        _start = tmp;
        _finish = _start + size();
        _end_of_Storage = _start + n;
    }
}

此时,我们发现_finish 初始是nullptr 经过_finish = _start + size()之后,_finish 依旧是nullptr,为什么?

解释:

因为此时_start已经指向新空间了,而size=()函数计算方法是 return _finish - _start;

此时size=_finish - _start 等价于 nullptr - _start= -_start

执行_finish=_start+ (-_start)= nullptr

解决方案:

将size()保存一份,即当执行 _finish = _start + size(); 时,size是用sz也就是_start没有指向新空间时已经计算好的长度.

cpp 复制代码
void reserve(size_t n)
{
    if (n > capacity())//先判断是否需要扩容
    {
        size_t sz = size();//将size保存
        //开辟新空间
        T* tmp = new T[n];
        //拷贝旧数据
        //memcpy(tmp, _start, sizeof(T) * sz);	//这样内部就浅拷贝了
        
        //实现外部和内部都是深拷贝
        for (size_t i = 0; i < sz; i++)
        {
            tmp[i] = _start[i];
        }
        delete[] _start;
        _start = tmp;
        _finish = _start + sz;
        _end_of_Storage = _start + n;
    }
}

(3) resize()函数

此处画图理解为佳.

if (n > size()):

先判断是否需要扩容,

再从_finish 位置开始,往后填充数据即可

cpp 复制代码
void resize(size_t n, const T& value = T())
 { 
     if (n <= size())
     {
         //*(_start + n) = '\0';//不用设置为'\0',因为vector的有效数据个数是_finish指向结尾来决定
         _finish = _start + n;
     }
     else
     {
         reserve(n);
         while (_finish != _start + n)
         {
             *_finish = val;
             ++_finish;
         }
         _finish = _start + n;
     }
 }

三、修改与访问操作

(1) 迭代器:

cpp 复制代码
 public:
     //迭代器
     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;
     }

(2) 元素访问[]

注意判断pos位置是否合法.不需要考虑pos小于0,因为类型是size_t .

cpp 复制代码
T& operator[](size_t pos)
 {
     assert(pos < size());

     return *(_start + pos);
 }

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

(3) push_back与pop_back

在进行插入操作之前,先考虑是否需要扩容.

扩容逻辑需要注意,很可能初始状态capacity=0,所以要先判断一下是否是0再进行1.5倍或者二倍扩.

其次,_finish 指向的正是有效元素的最后一个位置,即是尾插时待插入元素的位置.

cpp 复制代码
void push_back(const T& x)
{
    // if (size() + 1 > capacity())
    if (_finish == _end_of_Storage)
    {
        reserve(capacity() == 0 ? 4 : capacity() * 1.5);//如果是初次扩容,容量开始是0,所以这里要判断是否是0在考虑1.5倍或者二倍扩.
    }
    *(_finish) = x;
    ++_finish;
}
void pop_back()
{
    if (size() > 0)
    {
        --_finish;
    }
}

(4) swap

借助算法库中的swap()函数,交换三个指针即可.

cpp 复制代码
void swap(vector<T>& v)
{    
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_end_of_Storage, v._end_of_Storage);
}

(5) insert 与erase

需要注意的是判断pos位置是否合法.

试着画图分析如何移动数据.

cpp 复制代码
iterator insert(iterator pos, const T& x)
{
    assert(pos >= _start && pos <= _finish);
    //if (size() + 1 > capacity())
    if (_finish == _end_of_Storage)
    {
        reserve(capacity() == 0 ? 4 : capacity() * 1.5);//如果是初次扩容,容量开始是0,所以这里要判断是否是0在考虑1.5倍或者二倍扩.
    }
    //移动数据
    iterator end = _finish++;
    while (end != pos)
    {
        *(end) = *(end - 1);
       end--;
    }
    *end = x;
    return pos;
}
iterator erase(iterator pos)
{
    assert(pos >= _start && pos < _finish);
    iterator tmp = pos;
    while (tmp != _finish)
    {
        *tmp = *(tmp + 1);
        tmp++;
    }
    --_finish;
    return pos;
}

本篇只是简单的模仿库中完成部分接口的实现,目的是帮助大家更好的理解vector.

今天就分享到这里了,下次见.

相关推荐
陌小呆^O^8 分钟前
Cmakelist.txt之win-c-udp-server
c语言·开发语言·udp
C++忠实粉丝9 分钟前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
时光の尘30 分钟前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c
我们的五年34 分钟前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
-一杯为品-39 分钟前
【51单片机】程序实验5&6.独立按键-矩阵按键
c语言·笔记·学习·51单片机·硬件工程
程序猿阿伟1 小时前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链
爱摸鱼的孔乙己1 小时前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn
Dola_Pan1 小时前
C语言:数组转换指针的时机
c语言·开发语言·算法
烦躁的大鼻嘎2 小时前
模拟算法实例讲解:从理论到实践的编程之旅
数据结构·c++·算法·leetcode
IU宝2 小时前
C/C++内存管理
java·c语言·c++