STL讲解(五)——vector类的模拟实现(超详细)


前言

我们前面对于STL中的vector类有了一个详细的讲解,接下来呢,我将从0~1去模拟实现一个vector类,以便更好地巩固之前学习过的缺省参数、封装、类中的6大默认成员函数等

一、前情提要

1.1 源码引入

在这里插入图片描述
SGI STL 源码展示

下面是 SGI 版本的 STL 源码

template <class T, class Alloc = alloc>
class vector {
public:
    // 类型定义
    typedef T value_type;                    // 元素类型
    typedef value_type* pointer;             // 指针类型
    typedef const value_type* const_pointer; // 常量指针
    typedef value_type* iterator;            // 迭代器类型 ⭐
    typedef const value_type* const_iterator;// 常量迭代器
    typedef value_type& reference;           // 引用类型
    typedef const value_type& const_reference;// 常量引用
    typedef size_t size_type;                // 大小类型
    typedef ptrdiff_t difference_type;       // 指针差值类型

我们调出【vector】的一些核心源码,这里我们主要关注的就是这个使用原生指针value_type*所定义出来的迭代器 iterator

然后我们又看到了保护成员:[start]、[finish]、[end_of_stroage]

template <class T, class Alloc = alloc>
class vector {
public:
    typedef T value_type;
    typedef value_type* iterator;
    
protected:
    iterator start;           // 指向已使用空间的起始位置
    iterator finish;          // 指向已使用空间的末尾位置
    iterator end_of_storage;  // 指向可用空间的末尾位置
}

使用原生指针作为迭代器,零开销,效率最高

typedef value_type* iterator;
  • iterator 被定义为 T*(指向元素类型的指针)
  • 这意味着 vector 的迭代器就是普通指针
  • 好处:效率高,不需要额外的封装类

总体来说,这样子的好处是:
①简洁高效:用三个指针就能表示所有状态信息
②迭代器即指针:访问效率最高,没有额外开销
③空间利用清晰:通过指针差值直接计算 size 和 capacity

1.2 vector各函数接口总览

在这里插入图片描述

1.3 提前准备

为了不和库中的vector类发生冲突,我们可以包上一个名称为xt_vector的命名空间,此时因为作用域的不同,就不会产生冲突了

#include<iostream>
#include<assert.h>

using namespace std;


//为了不和std库中的vector类发生冲突,创建我们自己的作用域xt_vector

namespace xt_vector
{
	template<class T>     //通过模板能够实现不同类型的数据存储

	class vector
	{
	public:

		//定义iterator
		typedef T* iterator;
		typedef const T* const_iterator;

		/*
			各类函数功能实现
		*/


	private:
		/*
			成员变量
		*/
	};
}

二、模拟实现

2.1 成员变量

在vector当中有三个成员变量,迭代器我定义的是原生指针类型,然后将[_start]、[_finish]、[_end_of_storage]也定义为了三个迭代器类型

  • _start:指向容器中的起始位置
  • _finish:指向容器中最后一个有效数据的下一个位置
  • _end_of_storage:指向容器中现有容量的位置

在这里插入图片描述

private:
		//成员变量
		iterator _start = nullptr;             //指向容器中的起始位置
		iterator _finish = nullptr;            //指向容器中最后一个有效数据的下一个位置
		iterator _end_of_storage = nullptr;    //指向容器中现有容量的位置

2.2 成员函数

2.2.1 构造函数

1、无参构造

对于vector的无参构造,我们只需要将三个成员变量置为空指针即可

		//构造函数---无参构造
		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{

		}

2、迭代器区间构造

vector支持使用一段迭代器区间进行对象的构造。因为该迭代器区间可以是其他容器的迭代器区间,也就是说该函数接收到的迭代器的类型是不确定的,所以我们这里需要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中即可

例如:

//用一个常量字符串来构造vector
const char* p = "hello";
vector<char>v(p, p + 2);

//用一个数组来构造vector
int a[5] = { 1,2,3,4,5 };
vector<int>v1(a, a + sizeof(a) / sizeof(a[0]));

//用一个string类来构造vector 
string s1("hello");
vector<char>v2(s1.begin(), s1.end());

实现迭代器区间构造函数

//构造函数---迭代器区间构造
		template <class InputIterator>  //既是一个类模板的成员函数,又可以是一个函数模板
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);//尾插
				++first;
			}
		}

测试一下:

void test3()
{
	//无参构造
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

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

	//迭代器区间构造函数
	xt_vector::vector<int> v2(v.begin(), v.end());
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	std::string s("abcdef");
	xt_vector::vector<char> v3(s.begin(), s.end());
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;

	int a[] = { 1,2,3,4 };
	xt_vector::vector<int> v4(a, a + 4);
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;

}

在这里插入图片描述
3、n个值构造

此外,vector还支持构造这样一种容器,该容器当中含有n个值为val的数据

对于该构造函数,我们可以先使用resize()函数进行复用将容器容量先设置为n,并进行初始化

//构造函数---n个值构造,有参构造

//作用:创建一个包含 n 个元素的 vector,每个元素的值都是 val
		vector(size_t n, const T& val = T())
		{
			resize(n, val);
		}

为什么要用 const T& val = T()

问题:T 是模板参数,在编译前不知道是什么类型

template<class T>
class vector {
public:
    vector(size_t n, const T& val = ???)
    //                             ↑
    //                      这里应该写什么默认值?
};

//如果 T 是 int,默认值可以写 0
//如果 T 是 double,默认值可以写 0.0
//如果 T 是 string,默认值可以写 ""
//如果 T 是自定义类型呢?完全不知道该写什么
  • 解决方案:使用匿名对象 T()
  • T() 的含义:调用 T 类型的默认构造函数,创建一个临时对象(匿名对象),这个对象作为默认值
  • 匿名对象 生命周期只有一行,但在被 const 修饰后,其生命周期会延长
  • 内置类型 也能创建匿名对象,比如 int()、char() 是合法可用的
  • 如果大家对匿名对象还不太了解,可以去看看这篇文章:类和对象(三)

为什么要用 const T&

vector(size_t n, const T& val = T())
//               ↑        ↑      ↑
//               │        │      └── 匿名对象(默认值)
//               │        └────────── 引用(避免拷贝)
//               └─────────────────── const(保护原值)

接着,我们来测试一下:

void test4()
{
	//n个值构造函数
	xt_vector::vector<int> v5(5, 0);
}

我们看到出现了报错,说的是“非法的间接寻址”
在这里插入图片描述
这里对迭代器first去进行解引用目的就是为了获取这个位置上的数据, 在之前的指针有所提到 只有指针和迭代器可以解引用,基本数据类型不能解引用

但是为什么这里不会去匹配有参构造,而是去匹配的迭代器区间构造呢?

当我们调用 vector v(5, 0) 时,编译器面临两个选择:一是调用 vector(size_t n, const T& val) 需要把 int 转换成 size_t;二是调用模板构造函数

在讲 C++模板 的时候,我们有说到过模版参数会去进行自动类型推导,从而匹配最合适函数模版。

因为我们在这里所传入的【10】和【1】都是int类型,但是呢有参构造的第一个形参类型为size_t,并不是最匹配的

而迭代器区间初始化其参数类型都是模版参数,vector(InputIterator first, InputIterator last),编译器可以把模板参数 InputIterator 推导为 int 类型从而完全匹配不需要转换

由于完全匹配优先级更高,编译器可能错误地选择迭代器构造,然后在函数内部执行 *5 这样的解引用操作导致编译错误或崩溃。

那我们要如何预防呢?

很简单,我们可以利用在 C++函数重载 中所学习的参数类型不同去另写一个有参构造的重载形式,添加 vector(int n, const T& val) 这个 int 版本的重载,让编译器在遇到 int 参数时有一个完全匹配的非模板函数可选,优先级比模板函数更高,从而避免错误匹配到迭代器构造函数

//构造函数---n个值构造,有参构造
		//作用:创建一个包含 n 个元素的 vector,每个元素的值都是 val
		vector(size_t n, const T& val = T())
		{
			resize(n, val);
		}

		vector(int n, const T& val = T())
		{
			resize(n, val);
		}

在这里插入图片描述
我们可以看出这里就没有歧义了

2.2.2 拷贝构造函数

首先,我们要知道为什么要写拷贝构造函数,测试一下:
在这里插入图片描述
我们通过调试可以看到:这里只是做了一个浅拷贝,而不是去做了深拷贝

所以我们要自己去实现一个深拷贝

1、传统写法

//拷贝构造函数
		vector(const vector<T>& v)
		{
			_start = new T[v.capacity()];//开辟一块和容器v大小相同的空间
			//memcpy(tmp, v._start, sizeof(T) * v.size());//将容器v当中的数据一个个拷贝过来
			for (size_t i = 0; i < v.size(); i++)
			{
				_start[i] = v[i];
			}
			_finish = _start + v.size();//容器有效数据的尾
			_end_of_storage = _start + v.capacity();//整个容器的尾
		}

在这里插入图片描述
注意:将容器当中的数据一个个拷贝过来时不能使用memcpy函数,当vector存储的数据是内置类型或无需进行深拷贝的自定义类型时,使用memcpy函数是没什么问题的,但当vector存储的数据是需要进行深拷贝的自定义类型时,使用memcpy函数的弊端就体现出来了。例如,当vector存储的数据是string类的时候
在这里插入图片描述
并且vector当中存储的每一个string都指向自己所存储的字符串
在这里插入图片描述
如果此时我们使用的是memcpy函数进行拷贝构造的话,那么拷贝构造出来的vector当中存储的每个string的成员变量的值,将与被拷贝的vector当中存储的每个string的成员变量的值相同,即两个vector当中的每个对应的string成员都指向同一个字符串空间
在这里插入图片描述
这显然不是我们得到的结果,那么所给代码是如何解决这个问题的呢?
在这里插入图片描述
代码中看似是使用普通的“=”将容器当中的数据一个个拷贝过来,实际上是调用了所存元素的赋值运算符重载函数,而string类的赋值运算符重载函数就是深拷贝,所以拷贝结果是这样的:
在这里插入图片描述

总结就是:如果vector当中存储的元素类型是内置类型(int)或浅拷贝的自定义类型(Date),使用memcpy函数进行进行拷贝构造是没问题的,但如果vector当中存储的元素类型是深拷贝的自定义类型(string),则使用memcpy函数将不能达到我们想要的效果。

2、现代写法
拷贝构造函数的现代写法也比较简单,使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可

//现代写法
vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(v.capacity()); //调用reserve函数将容器容量设置为与v相同
	for (auto e : v) //将容器v当中的数据一个个尾插过来
	{
		push_back(e);
	}
}

注意: 在使用范围for对容器v进行遍历的过程中,变量e就是每一个数据的拷贝,然后将e尾插到构造出来的容器当中。就算容器v当中存储的数据是string类,在e拷贝时也会自动调用string的拷贝构造(深拷贝),所以也能够避免出现与使用memcpy时类似的问题

2.2.3 运算符赋值重载

vector的赋值运算符重载也涉及深拷贝问题,我们这里也提供两种深拷贝的写法:

1、传统写法

首先判断是否是给自己赋值,若是给自己赋值则无需进行操作。若不是给自己赋值,则先开辟一块和容器v大小相同的空间,然后将容器v当中的数据一个个拷贝过来,最后更新_finish和_end_of_storage的值即可

//运算符赋值重载
		//传统写法
		vector<T>& operator=(const vector<T>& v)
		{
			if (this != &v) //防止自己给自己赋值
			{
				delete[] _start; //释放原来的空间
				_start = new T[v.capacity()]; //开辟一块和容器v大小相同的空间
				for (size_t i = 0; i < v.size(); i++) //将容器v当中的数据一个个拷贝过来
				{
					_start[i] = v[i];
				}
				_finish = _start + v.size(); //容器有效数据的尾
				_end_of_storage = _start + v.capacity(); //整个容器的尾
			}
			return *this; //支持连续赋值
		}

注意: 这里和拷贝构造函数的传统写法类似,也不能使用memcpy函数进行拷贝

2、现代写法
赋值运算符重载的现代写法非常精辟,首先在右值传参时并没有使用引用传参,因为这样可以间接调用vector的拷贝构造函数,然后将这个拷贝构造出来的容器v与左值进行交换,此时就相当于完成了赋值操作,而容器v会在该函数调用结束时自动析构

	void swap(vector<T>& v)   //交换
		{
			//交换容器当中的各个成员变量
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}
		
		//现代写法
		// v1 = v3
		// v1是v3 的拷贝, 注意这里不能用& 这样v3就变成v1了,而我们的目的是让 v1 = v3
		vector<T>& operator=(vector<T> v) //编译器接收右值的时候自动调用其拷贝构造函数
		{
			swap(v); //交换这两个对象
			return *this; //支持连续赋值
		}

注意: 在此处调用库当中的swap需要在swap之前加上“::”(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)
在这里插入图片描述
注意:赋值运算符重载的现代写法也是进行的深拷贝,只不过是调用的vector的拷贝构造函数进行的深拷贝,在赋值运算符重载函数当中仅仅是将深拷贝出来的对象与左值进行了交换而已

2.2.4 析构函数

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

//析构函数
		~vector()
		{
			if (_start)   //避免对空指针的释放
			{
				delete[] _start;//释放容器存储数据的空间,会释放整个动态分配的数组
				_start = nullptr;//_start置空
				_finish = nullptr;//_finish置空
				_end_of_storage = nullptr;//_end_of_storage置空
			}
		}

2.3 元素访问

2.3.1 operator[ ]

对于元素访问的话我们最常用的就是下标 + []的形式,这里给出两种,一个是const版本和非const版本

	//operator[ ],// 返回应用的时数组的下标
		T& operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}
		const T& operator[](size_t pos)const
		{
			assert(pos < size());
			return _start[pos];
		}

2.3.2 迭代器

经过之前的学习我们可以知道,要去遍历访问一个vector对象的时候,除了【下标 + [ ]】的形式,我们还可以使用迭代器的形式去做一个遍历

对于迭代器而言我们也是要去实现两种,一个是非const的,一个则是const的

vector类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已

typedef T* iterator;                     // 迭代器某种意义上就是指针
typedef const T* const_iterator;

vector的begin直接返回容器的_start,end返回容器的_finish

//begin
		iterator begin()
		{
			return _start;//返回容器的起始位置
		}
		//end
		iterator end()
		{
			return _finish;//返回有效数据的下一个地址
		}

const版本

//const版本迭代器
		//begin
		const_iterator begin()const
		{
			return _start;//返回容器的起始位置
		}
		//end
		const_iterator end()const
		{
			return _finish;//返回有效数据的下一个地址
		}

测试:

void test8()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	cout << "------------迭代器打印-----------" << endl;
	// 1.迭代器
	xt_vector::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	cout << "------------范围for打印-----------" << endl;
	// 2.有迭代器就支持 范围for
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
	cout << "------------下标[]打印-----------" << endl;
	// 3. []
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	cout << "------------const打印-----------" << endl;
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
}

在这里插入图片描述

2.4 容量

2.4.1 size和capacity

因为指针相减的结果就是这两个指针之间对应类型的数据个数,所以获取size只需_finish-_start,获取capacity只需_end_of_stoage-_start
在这里插入图片描述

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

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

2.4.2 reserve

reserve增容:

  1. 当n>capacity时,将capacity扩大到n
  2. 当n<capacity时,不进行任何操作

reserve函数的实现思路:先判断所给n是否大于当前容器的最大容量(否则无需进行任何操作),操作时直接开辟一块可以容纳n个数据的空间,然后将原容器当中的有效数据拷贝到该空间,之后将原容器存储数据的空间释放,并将新开辟的空间交给该容器维护,最后更新容器当中各个成员变量的值即可

	//reserve
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size(); //提前算好增容前的数据个数
				//扩容
				T* tmp = new T[n];//开辟n个空间
				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * sz);
					delete[]_start;
				}
				_start = tmp;
				_finish = _start + size();
				_end_of_storage = _start + n;
			}
		}

我们这里写一个push_back的接口(后面讲),先让代码跑起来

	//尾插
		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
			}
			*_finish = x;
			_finish++;
		}

进行调试

void test1()
{
	//无参构造
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

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

}

运行之后我们发现报错了,有地方出现了空指针异常
在这里插入图片描述

进一步,我们通过调试窗口,明显看到_finish的值为【0x00000000】
在这里插入图片描述
其实真正的问题还是出在【reserve】这个扩容的逻辑中,随着我们一步一步地去看,可以看到_start和_end_of_storage这两个都没什么问题,但是_finish就是没有什么变化,所以呢我们可以锁定到下面这句话

_finish = _start + size();

此时就需要去看看这个【size】了,之前我们使用的是_finish - _start来计算的 size(),在执行这句话时_start已经发生了改变,因为我们去开出了一块新的空间,但是这时_finish的值还是一开始的【nullptr】,那么这个 size() 最后计算出来的大小即为 -_start,此时再和_start去做一个结合的话即为 0

在这里插入图片描述
所以,上述就是为什么这个_finish的值为【0x00000000】原因,那我们要如何去修改呢

我们可以在每次没开始扩容之前我们都可以去事先保存一下这个 size(),后面的更新顺序就不需要发生变动了,在加的时候加上sz即可

//reserve
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size(); //提前算好增容前的数据个数
				//扩容
				T* tmp = new T[n];//开辟n个空间
				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * sz);
					delete[]_start;
				}
				_start = tmp;
				//_finish = _start + size();
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

这样子看,好像代码没有问题了,但是当我们将参数设置为string类时,我们运行看看

void test2() 
{
	xt_vector::vector<string> v;

    v.push_back("Hello");
    v.push_back("World");
    v.push_back("Test");
    v.push_back("String");
    v.push_back("Push Back");  // 测试扩容

    for (const auto& s : v) {
        cout << s << " ";
    }
    cout << endl;

    // 测试输出
    // Hello World Test String Push Back
}

在这里插入图片描述
报错了,这是因为什么呢?
memcpy 只能用于内置类型(如 int、double),不能用于类类型(如 string、vector)
原因:memcpy 是按字节逐位拷贝(浅拷贝),不会调用拷贝构造函数,会导致严重的内存问题

可能你会想,当vector当中存储的是string的时候,虽然使用memcpy函数reserve出来的容器与原容器当中每个对应的string成员都指向同一个字符串空间,但是原容器存储数据的空间不是已经被释放了,相当于现在只有一个容器维护这这些字符串空间,这还有什么影响。

但是不要忘了,当你释放原容器空间的时候,原容器当中存储的每个string在释放时会调用string的析构函数,将其指向的字符串也进行释放,所以使用memcpy函数reserve出来的容器当中的每一个string所指向的字符串实际上是一块已经被释放的空间,访问该容器时就是对内存空间进行非法访问

vector 不能使用 memcpy 的原因:

因为 vector 的模板参数 T 可能是类类型(如 string、vector 等),这些类内部通常有指针成员指向堆内存。memcpy 只进行浅拷贝(按字节复制),会直接复制指针的值(地址),导致新旧对象的指针指向同一块堆内存,而不会分配新的堆内存并复制内容。这样当一个对象析构释放了这块内存后,另一个对象的指针就变成了野指针,再次析构时会造成 double free(重复释放同一块内存),导致程序崩溃。正确做法是使用赋值运算符(tmp[i] = _start[i]),让类型自己的拷贝机制(深拷贝)来处理,为每个对象分配独立的堆内存

示例图:
在这里插入图片描述

这显然不是我们得到的结果,那我们要如何改呢
使用循环赋值

//reserve
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size(); //提前算好增容前的数据个数
				//扩容
				T* tmp = new T[n];//开辟n个空间
				if (_start)
				{
					//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();
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

在这里插入图片描述

在这里插入图片描述

代码中看似是使用普通的“=”将容器当中的数据一个个赋值过来,实际上是调用了所存元素的赋值运算符重载函数,而string类的赋值运算符重载函数就是深拷贝

在这里插入图片描述

总结: 如果vector当中存储的元素类型是内置类型(int)或浅拷贝的自定义类型(Date),使用memcpy函数进行进行复制是没问题的,但如果vector当中存储的元素类型是深拷贝的自定义类型(string),则使用memcpy函数将不能达到我们想要的效果

2.4.3 resize

像之前一样,我们还是分三类进行讨论:

  • 一个是n < _finish的情况
  • 第二个是n > _finish && n <= _end_of_storage的情况
  • 第三个是n >_end_of_storage的情况

对于后两种情况我们可以做一个合并,使用上面【reserve】去做一个容量的检查

实现思路:进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可
在这里插入图片描述

		//resize
		void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				//先使用reserve()去检查是否需要扩容
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}

测试一下:

void test5()
{
	xt_vector::vector<int> v;
	v.resize(10);

	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
}

在这里插入图片描述
注意: 在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T( )即可

2.4.4 empty

empty函数可以直接通过比较容器当中的_start和_finish指针的指向来判断容器是否为空,若所指位置相同,则该容器为空

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

2.5 修改器

2.5.1 push_back

首先第一个的话就是push_back,这个我在上面讲【reserve】的时候给出过,现在仔细地再来讲一讲

首先的话我们要考虑的就是扩容的逻辑,之前我们有讲到在VS下是呈现 1.5倍 的增长趋势,但是在g++下呈现的则是 2倍 的扩容逻辑,这里的扩容的话我们就交给【reserve】来实现

	//尾插
		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{

				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
			}
			*_finish = x;
			_finish++;
		}

2.5.2 pop_back

在进行尾删时,需要判断容器是否为空,我们这里并没有实现vector的判空操作,可以利用_finish和_start之间的关系进行判断

//尾删
		void pop_back(const T& x)
		{
			assert(_finish > _start);
			--_finish;
		}

2.5.3 insert(迭代器失效)

void insert(iterator pos, const T& x)

首先,我们要进行断言,确定pos是有效值

然后,需要一个扩容逻辑,这里我们直接用上面在push_back()接口中所写的即可

最后,要在某一个位置插入数据的话就需要先去挪动部分的数据,这里我们从后往前挪,防止造成覆盖的情况,当数据挪动完毕后,再在pos这个位置插入指定的数据即可
在这里插入图片描述
实现代码:

	//insert
		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);
			//1.扩容
			if (_finish == _end_of_storage)
			{
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
			}
			//2.挪动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
		}

迭代器失效—扩容导致野指针

我们进行测试

void test9()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.insert(v.begin(), 0);
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
}

程序崩溃了
在这里插入图片描述

我们进行调试:

  • 此时我们已经往【v】中插入了4个数据,马上使用insert(v.begin(),0)去做一个头插,那么一进到函数中我们就可以知道这个当前对象的_start和pos所处的迭代器位置是相同的,也就是同一段空间的地址
    在这里插入图片描述

  • 那此时我们知道容器中的空间已经满了,所以会去走一个扩容的逻辑,此时可以看到当前对象this的_start已经发生了改变

在这里插入图片描述
可以看到,在扩完容之后,当前对象的_start和待插入位置的pos已经发生了变化,那么在此时我们再去挪动数据进行插入的时候就会出现问题了

在这里插入图片描述
我们可以通过下面的图示来看看到底这个扩完容之后是怎样的

可以看到_start确实发生了一个变化,但是呢pos还是指向原来的那个地方。那读者可以自己去想象一下子在遍历挪动数据的时候究竟何时才是个头呢
在这里插入图片描述
以上所出现的这个问题就被称作是 【迭代器失效的问题】---- 扩容导致野指针

我们要如何解决呢?

  • 首先我们要明白的一个点是出错的根本原因在于:_start的位置改变了但是pos的位置没有发生改变,参数 pos 变成野指针,函数内部使用 pos 会导致未定义行为
  • 所以我们所要做的一个点就是:让pos的位置随着_start的变动而一起变动,这样就不会出现问题了。以下我们需要改进的代码部分,在进行扩容之前,我们可以先去计算一下从【_start】到【pos】的位置有多远;
  • 然后呢我们在执行完扩容的逻辑之后,就要去更新一下这个【pos】迭代器的位置所在,就使用刚才计算出来的这段距离即可
//insert
		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);
			//1.扩容
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start; //记录pos与_start之间的间隔
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
				pos = _start + len; //通过len找到pos在增容后的容器当中的位置
			}
			//2.挪动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
		}

在这里插入图片描述


迭代器指向的位置意义改变
但是呢就上面这样还不够,我们只解决了内部迭代器失效的问题,而外部迭代器失效的问题并没有很好地解决

外部迭代器,那是什么东西? 我们来看下这段代码

void test10()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	xt_vector::vector<int>::iterator it = v.begin();
	v.insert(v.begin(), 0);
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
	cout << endl;

	cout << *it << endl;
}

在这里插入图片描述

出错的根本原因在于:扩容改变了底层数组地址,但调用者的迭代器变量没更新,调用者继续使用旧迭代器会访问已释放内存

所以,对于迭代器这一块我们在使用的时候一定要慎重,在使用完之后不要去轻易地修改它

以上所出现的这个问题就被称作是 【迭代器失效的问题】---- 位置意义改变

如果执意要进行修改的话也不是没有办法,我们只需要在【insert】之后去接受一下当前所操作的这个迭代器的位置即可,记住这个位置,下次在访问的时候也就不会出问题

iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);
			//1.扩容
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start; //记录pos与_start之间的间隔
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
				pos = _start + len; //通过len找到pos在增容后的容器当中的位置
			}
			//2.挪动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
			return pos;
		}
void test10()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	xt_vector::vector<int>::iterator it = v.begin();
	//v.insert(v.begin(), 0);
	it= v.insert(v.begin(), 0);
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
	cout << endl;

	cout << *it << endl;
}

在这里插入图片描述

注意:

  • vector的insert实现不会出现string那样的问题,核心原因是vector使用指针作为迭代器类型,而string某些实现使用size_t作为位置参数。string的问题在于size_t是无符号整数,当pos等于0时,end从0递减会下溢变成最大无符号数比如4294967295,导致循环条件end大于等于pos永远为真造成死循环。
  • 而vector中的pos是指针类型,它存储的是内存地址比如0x1000,即使pos指向第一个元素也就是等于_start,这个地址也是一个有效的内存地址而不是数值0。
  • 所谓有效地址是指操作系统分配给程序可以访问的内存地址,而地址0也就是nullptr是被操作系统特意保护起来的无效地址,任何程序都不能访问它,这样设计是为了帮助检测空指针错误。
  • 在vector的循环中,当end指针递减到pos之前的位置时,指针比较会正常判断出end小于pos,循环正常终止,因为指针的递减和比较遵循指针运算规则,不会发生无符号整数那种下溢问题。所以vector的实现本质上是利用了指针类型的特性避免了无符号整数下溢的陷阱。

2.5.4 erase(迭代器失效)

erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时我们也是需要先去挪动数据的,但是在这里呢我们需要从前往后挪,也是防止造成覆盖的情况

在这里插入图片描述

//erase:删除pos位置的数据
		//erase:删除pos位置的数据
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);//容器为空则断言
			//将pos位置之后的数据统一向前挪动一位,以覆盖pos位置的数据
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				it++;
			}
			_finish--;//数据个数减少一个,_finish前移
			return pos;
		}

在这里插入图片描述

三、vector 类的模拟实现整体代码

3.1 vector.h

#define _CRT_SECURE_NO_WARNINGS 1


#include<assert.h>

//为了不和std库中的vector类发生冲突,创建我们自己的作用域xt_vector

namespace xt_vector
{
	template<class T>     //通过模板能够实现不同类型的数据存储

	class vector
	{
	public:

		//定义iterator,迭代器某种意义上就是指针
		typedef T* iterator;
		typedef const T* const_iterator;

		//成员函数,各类函数功能实现

		//begin
		iterator begin()
		{
			return _start;//返回容器的起始位置
		}
		//end
		iterator end()
		{
			return _finish;//返回有效数据的下一个地址
		}

		//const版本迭代器
		//begin
		const_iterator begin()const
		{
			return _start;//返回容器的起始位置
		}
		//end
		const_iterator end()const
		{
			return _finish;//返回有效数据的下一个地址
		}

		//构造函数---无参构造
		vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{

		}

		//构造函数---迭代器区间构造
		template <class InputIterator>  //既是一个类模板的成员函数,又可以是一个函数模板
		vector(InputIterator first, InputIterator last)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			while (first != last)
			{
				push_back(*first);//尾插
				++first;
			}
		}

		//构造函数---n个值构造,有参构造
		//作用:创建一个包含 n 个元素的 vector,每个元素的值都是 val
		vector(size_t n, const T& val = T())
		{
			resize(n, val);
		}

		vector(int n, const T& val = T())
		{
			resize(n, val);
		}

		//拷贝构造函数
		vector(const vector<T>& v)
		{
			_start = new T[v.capacity()];//开辟一块和容器v大小相同的空间
			//memcpy(tmp, v._start, sizeof(T) * v.size());//将容器v当中的数据一个个拷贝过来
			for (size_t i = 0; i < v.size(); i++)
			{
				_start[i] = v[i];
			}
			_finish = _start + v.size();//容器有效数据的尾
			_end_of_storage = _start + v.capacity();//整个容器的尾
		}

		//运算符赋值重载
		//传统写法
		vector<T>& operator=(const vector<T>& v)
		{
			if (this != &v) //防止自己给自己赋值
			{
				delete[] _start; //释放原来的空间
				_start = new T[v.capacity()]; //开辟一块和容器v大小相同的空间
				for (size_t i = 0; i < v.size(); i++) //将容器v当中的数据一个个拷贝过来
				{
					_start[i] = v[i];
				}
				_finish = _start + v.size(); //容器有效数据的尾
				_end_of_storage = _start + v.capacity(); //整个容器的尾
			}
			return *this; //支持连续赋值
		}

		//现代写法
		// v1 = v3
		// v1是v3 的拷贝, 注意这里不能用& 这样v3就变成v1了,而我们的目的是让 v1 = v3
		vector<T>& operator=(vector<T> v) //编译器接收右值的时候自动调用其拷贝构造函数
		{
			swap(v); //交换这两个对象
			return *this; //支持连续赋值
		}

		//析构函数
		~vector()
		{
			if (_start)   //避免对空指针的释放
			{
				delete[] _start;//释放容器存储数据的空间,会释放整个动态分配的数组
				_start = nullptr;//_start置空
				_finish = nullptr;//_finish置空
				_end_of_storage = nullptr;//_end_of_storage置空
			}
		}

		//operator[ ],// 返回应用的时数组的下标
		T& operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}
		const T& operator[](size_t pos)const
		{
			assert(pos < size());
			return _start[pos];
		}
	
		//size
		size_t size()const
		{
			return _finish - _start;
		}

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

		//reserve
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size(); //提前算好增容前的数据个数
				//扩容
				T* tmp = new T[n];//开辟n个空间
				if (_start)
				{
					//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();
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

		//resize
		void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_finish = _start + n;
			}
			else
			{
				//先使用reserve()去检查是否需要扩容
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}
		
		//尾插
		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{

				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
			}
			*_finish = x;
			_finish++;
		}

		//尾删
		void pop_back(const T& x)
		{
			assert(_finish > _start);
			--_finish;
		}

		//insert
		//void insert(iterator pos, const T& x)
		//{
		//	assert(pos >= _start && pos <= _finish);
		//	//1.扩容
		//	if (_finish == _end_of_storage)
		//	{
		//		size_t len = pos - _start; //记录pos与_start之间的间隔
		//		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		//		reserve(newCapacity);
		//		pos = _start + len; //通过len找到pos在增容后的容器当中的位置
		//	}
		//	//2.挪动数据
		//	iterator end = _finish - 1;
		//	while (end >= pos)
		//	{
		//		*(end + 1) = *end;
		//		--end;
		//	}
		//	*pos = x;
		//	++_finish;
		//}

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);
			//1.扩容
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start; //记录pos与_start之间的间隔
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
				pos = _start + len; //通过len找到pos在增容后的容器当中的位置
			}
			//2.挪动数据
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
			return pos;
		}

		//erase:删除pos位置的数据
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);//容器为空则断言
			//将pos位置之后的数据统一向前挪动一位,以覆盖pos位置的数据
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				it++;
			}
			_finish--;//数据个数减少一个,_finish前移
			return pos;
		}

	private:
		//成员变量
		iterator _start = nullptr;             //指向容器中的起始位置
		iterator _finish = nullptr;            //指向容器中最后一个有效数据的下一个位置
		iterator _end_of_storage = nullptr;    //指向容器中现有容量的位置
	};
}

3.2 vector.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<vector>
#include<string>
#include<algorithm>

using namespace std;

#include"vector.h"



void test1()
{
	//无参构造
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

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

}

void test2() 
{
	xt_vector::vector<string> v;

    v.push_back("Hello");
    v.push_back("World");
    v.push_back("Test");
    v.push_back("String");
    v.push_back("Push Back");  // 测试扩容

    for (const auto& s : v) {
        cout << s << " ";
    }
    cout << endl;

    // 测试输出
    // Hello World Test String Push Back
}

void test3()
{
	//无参构造
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

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

	//迭代器区间构造函数
	xt_vector::vector<int> v2(v.begin(), v.end());
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	std::string s("abcdef");
	xt_vector::vector<char> v3(s.begin(), s.end());
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;

	int a[] = { 1,2,3,4 };
	xt_vector::vector<int> v4(a, a + 4);
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;

}

void test4()
{
	//n个值构造函数
	xt_vector::vector<int> v5(5, 0);

	for (auto ch : v5)
	{
		cout << ch << " ";
	}
	cout << endl;
}


void test5()
{
	xt_vector::vector<int> v;
	v.resize(10);

	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
}

void test6()
{
	//无参构造
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	xt_vector::vector<int> v1(v);
	for (auto ch : v1)
	{
		cout << ch << " ";
	}
	cout << endl;
}

void test7()
{
	//无参构造
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	xt_vector::vector<int> v1 = v;
	for (auto ch : v1)
	{
		cout << ch << " ";
	}
	cout << endl;
}

void test8()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	cout << "------------迭代器打印-----------" << endl;
	// 1.迭代器
	xt_vector::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	cout << "------------范围for打印-----------" << endl;
	// 2.有迭代器就支持 范围for
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
	cout << "------------下标[]打印-----------" << endl;
	// 3. []
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	cout << "------------const打印-----------" << endl;
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
}


void test9()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.insert(v.begin(), 0);
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
}

void test10()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	

	xt_vector::vector<int>::iterator it = v.begin();
	//v.insert(v.begin(), 0);
	it= v.insert(v.begin(), 0);
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
	cout << endl;

	cout << *it << endl;
}

void test11()
{
	xt_vector::vector<int> v;

	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(9);
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
	cout << endl;

	//删除所有的偶数
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			it++;
		}
	}
	for (auto ch : v)
	{
		cout << ch << " ";
	}
	cout << endl;
}

void test12()
{
	xt_vector::vector<int> v;
	auto it = v.begin();
	it = v.erase(it);
}

int main()
{
	test11();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值