Bootstrap

【C++】vector 模拟实现

一.vector 介绍

  • vector 表示可变大小数组的序列式容器
  • 就像数组一样,vector 也采用连续的存储空间来存储元素,这也就是意味着可以采用下标对 vector 中的元素进行访问,其效率和数组一样高效。但是它又不像数组,它的大小是可以动态改变的,甚至还有自动扩容的功能。

二. vector 的模拟实现

1. 基本框架

vector 是个类模板,它就可以存储不同数据类型的元素,其迭代器是数据类型的指针。

template<class T>
class vector
{
public:
	typedef T* iterator;            //普通迭代器
	typedef const T* const_iterator;//const迭代器 

private:
	iterator _first;         //指向第一个有效数据的指针
	iterator _finish;        //指向有效数据的尾(尾不存储有效数据)
	iterator _endofstorage;  //指向存储容量的尾(尾是未开辟的空间)
};

成员变量解释
在这里插入图片描述

2. 迭代器相关接口

2.1 begin 和 cbegin

原型:iterator begin();

作用:返回一个指向第首元素的迭代器

原型:const_iterator cbegin() const;

作用:返回一个指向首元素的 const 迭代器

template<class T>
class vector
{
public:
	typedef T* iterator;            
	typedef const T* const_iterator;
	
	// begin
	iterator begin()
	{
		return _first;
	}
	
	// cbegin
	const_iterator cbegin() const
	{
		return _first;
	}

private:
	iterator _first;         
	iterator _finish;        
	iterator _endofstorage;  
};

2.2 end 和 cend

原型:iterator end();

作用:返回一个指向最后一个元素的下一个位置的普通迭代器

原型:const_iterator cend() const;

作用:返回一个指向最后一个元素的下一个位置的 const 迭代器

template<class T>
class vector
{
public:
	typedef T* iterator;            
	typedef const T* const_iterator;

	//end
	iterator end()
	{
		return _finish;
	}
	//cend
	const_iterator cend()const
	{
		return _finish;
	}

private:
	iterator _first;         
	iterator _finish;        
	iterator _endofstorage;  
};

3. 容量操作接口

3.1 size 和 capacity

原型:size_type size() const;

作用:返回 vector 对象中有效数据的个数

原型:size_type capacity() const; 

作用:返回 vector 对象的容量大小

注意指针减指针的结果并不是单纯的数字减法的结果,编译器会根据指针类型把数字减法的结果除以其权重 sizeof(T),得到中间元素的个数。

指针相减的原理
1、计算间隔的字节数
2、除以权重 sizeof(T)。

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

//capacity
size_t capacity()const
{
	return _endofstorage - _first;
}

3.2 reserve

原型:void reserve(size_t n);
作用:调整 vector 对象的容量

规则

  • n > 对象的容量(capacity()):开新空间扩容(还要拷贝旧空间的数据、释放旧空间)
  • n <= 对象的容量(capacity()):啥事都不干
void reserve(const size_t n)
{
	if (n > capacity())
	{
		// 1、开新空间
		size_t len = size();
		T* tmp = new T[n];
		// 2、拷贝数据
		for (size_t i = 0; i < len; ++i)
			tmp[i] = _first[i];
		// 3、释放旧空间
		delete []_first;
		_first = tmp;
		tmp = nullptr;
		_finish = _first + len;
		_endofStorage = _first + n;
	}
}

问题:拷贝数据时能否用 memecpy 函数?

在这里插入图片描述
在回答这个问题之前我们先来看看 memcpy 函数的拷贝原理,其实就是按照字节的内容,逐个字节地去拷贝。

// 传入数据可能是各种类型的,所以用void*接收
void* memcpy(void* dest, const void* src, size_t num)
{
    //断言,判断指针的有效性,防止野指针
    assert(dest!=NULL);
    assert(src!=NULL);
    
	// void* 可以接收所有类型的指针
	// 不可以进行解引用和加减的操作,但可以比较大小
	void* tmp = dest;
	while (num--)
	{
		//把指针类型转化为char*在解引用和+1/-1时可以访问一个字节
		*(char*)dest = *(char*)src;
		((char*)dest)++;
		((char*)src)++;
	}
	return tmp;
}

如果我们的元素是内置类型(int、char、double 等)还好,但如果类型是像 string 类对象那样,成员变量是一个指向内存空间的指针的话,memcpy 就会造成浅拷贝。
在这里插入图片描述

3.3 resize

原型:void resize(size_t n, const T& val = T());
作用:调整 vector 有效数据的内容和大小

  • n > 有效元素个数(size()):多出来的有效空间用 val 去填补,如果不传 val 的话,就填补对应数据类型的默认值 T();如果大于 capacity() 的话还会扩容。
  • n <= 有效元素个数(size()):会截断多出来的有效数据
void resize(size_t n, const T& val = T())
{
	// 1.当n大于其有效数据长度的处理
	if (n > size())
	{
		if (n > capacity())
		{
			reserve(n);
		}
		iterator finish = _first + n;
		while (_finish != finish)
		{
			*_finish = val;
			++_finish;
		}
	}
	else // 2.当n小于等于其有效数据长度的处理
	{
		_finish = _first + n;
	}
}

补充:关于内置类型的构造函数

我们 resize 参数列表中有个形参const T& val = T(),这里的 T 是元素的类型,T() 就是这个类型的一个匿名对象。如果 T 是自定义类型的话就调用它的默认构造函数去初始化这个匿名对象,但如果 T 是内置类型的话,像 int,double,char 等,他们也有自己的默认构造函数吗?
在这里插入图片描述

4. 默认成员函数

4.1 构造函数

类型一:默认构造函数

原型:vector()
作用:默认构造函数

参数什么都没传,那构造出来的就是空对象。它内部没有开辟空间,其成员变量的值都为 nullptr。

vector()
	:_first(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{}

类型二:初始化构造 n 个 val

原型:vector(size_t n, const T& val = T())
作用:构造并初始化 n 个 val

vector(size_t n, const T& val = T())
	// 1.先把成员变量初始化为空
	:_first(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{
	// 直接调用 resize() 完帮助我们成初始化工作
	resize(n, val);
}

问题:既然是初始化构造 n 个 val,直接在函数体里构造就行了,在初始化列表把成员变量初始化为空还有必要吗?
在这里插入图片描述

4.2 拷贝构造

vector(const vector& v)
	:_first(nullptr)
	,_finish(nullptr)
	,_endofstorage(nullptr)
{
	// 1.开和v一样大的空间
	reserve(v.capacity());
	// 2.遍历v,利用迭代器拷贝数据
	const_iterator it = v.cbegin();
	const_iterator finish = v.cend();
	while (it != finish)
	{
		*_finish++ = *it++;
	}
}

补充:拷贝构造的几点说明

在这里插入图片描述

4.3 赋值重载

vector<T>& operator=(const vector<T> v)
{
	swap(v);
	return *this;
}

//成员函数 vector::swap
void swap(vector<T>& v)
{
    //相互交换各自指向的地址
    //下面的swap是std的swap
	std::swap(_first, v._first);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

补充:区分成员函数 vector::swap 和 std 标准库中的 std::swap
在这里插入图片描述

补充:参数和返回值说明

在这里插入图片描述

5. 修改操作接口

5.1 insert

原型:iterator insert(iterator pos, const T& val);
作用:在 pos 位置插入一个元素

iterator insert(iterator pos, const T& val)
{
	// 插入之前检查是否需要扩容
	if (_finish == _endofstorage)
	{
		size_t len = pos-_first;
		size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2;
		reserve(newcapacity);
		// 更新迭代器(下面图里有说明)
		pos = _first + len;
	}
	// end为最后一个有效数据的迭代器
	iterator end = _finish-1;
	// 挪动数据
	while (pos <= end)
	{
		*(end + 1) = *end;
		--end;
	}
	// 插入数据
	*pos = val;
	++_finish;
	return pos;
}

补充:insert 涉及到的迭代器失效问题

①增容导致外面传入的迭代器 pos 变成野指针
因为增容的过程是:开新空间、拷贝数据、释放旧空间。在增容后外面传入的迭代器 pos 原来指向的空间被释,变成了野指针。造成这个问题的原因是原空间被释放,而外面的 pos 不能及时更新。
在这里插入图片描述

当然对于 pos 这个参数我们是传值进来的,虽然我们在函数里面更新了它,但是外面的 pos 其实还是失效的,它指向一块已经被释放的空间,变成了野指针。如果是传引用的话就可以解决,不过STL库并没有这样做,而是通过返回值去返回更新后的迭代器。
在这里插入图片描述

②插入数据导致迭代器的 pos 的意义改变
这是在没有增容的情况下发生的迭代器失效问题,插入后外面的迭代器 pos 指向的值变成了新插入的 val。造成这个问题的原因是 vector 的存储空间是连续的,外面的 pos 永远指向内存空间的那块位置,而 insert 就是要挪动 pos 以及 pos 位置以后的数据来完成数据插入,最后在 pos 位置上插入新的数据就是 val。
在这里插入图片描述

总结:insert 迭代器失效问题总结
在使用 insert 函数后,原来 pos 的迭代器最好别用了,因为我们不确定是否增容导致它变成野指针,还是没增容但是它的意义改变了。要用的话就更新它,即让它接收 insert 的返回值,这个返回值一定是有效的,但是我们要清楚返回的那个迭代器,它的值是指向新插入的那个元素的。
在这里插入图片描述

问:插入一个数据,能否用 memset?
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

总结:memcpy 和 memset 拷贝数据总结

拷贝数据时慎用 memcpy 和 memset,前者如果拷贝的是涉及到内存管理的数据类型,比如 string 的话会造成浅拷贝;后者只是按 value 一个字节的内容来拷贝的。最保险的拷贝方式还是遍历一遍原空间,然后单独拿出空间中的每个一数据实现拷贝。

5.2 erase

原型:iterator erase(iterator pos);
作用:删除 pos 位置上的元素

iterator erase(iterator pos)
{
	//把 pos 之后的数据一个一个地往前挪
	iterator begin = pos + 1;
	while (begin != _finish)
	{
		*(begin - 1) = *begin;
		++begin;
	}
	--_finish;
	return pos;
}

erase 涉及到的迭代器失效问题

①缩容导致外面传入的迭代器pos变成野指针
有的编译器,如果你一直删除数据,而导致太多空间空出来不同,这时编译器会把你的容量缩小:开辟小空间、拷贝数据、释放旧的大空间。和 insert 一样,此时外面的迭代器 pos 变成了野指针。

②删除导致外面传入的迭代器 pos 意义改变
在未缩容的情况下,erase 删除一个数据后,外面 pos 的值变成了那个被删除数据的后一个位置元素的值,这时因为 vector 的存储空间是连续的,而 pos 永远指向空间的那个位置,删除一个数据就是把后面位置的数据往前挪,pos 指向的内存空间的值改变了。

关于erase写法的几点说明

在这里插入图片描述

;