【C++修炼之路 第六章】模拟实现 vector 类模板

因为我们要实现一个 模板,则不建议将 声明和定义分离至两个文件(如上几期实现string那样)

否则容易引起编译链接等错误

1、vector 的核心框架实现(vector 类模板)

#include<iostream>
#include<vector>
#include<string>
#include<assert.h>
using namespace std;

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


	private:
		iterator _start;  //指向有效数据区域的开始(相当于 begin())
		iterator _finish;  //指向有效数据的最后一个有效元素的下一个位置(相当于 end())
		iterator _end_of_storage;  //指向总存储空间的最后
	};
}

关于私有成员

先看 vector 的源码

三个私有成员都是使用 迭代器实现(不像之前的 string 类中的写法)

2. vector 元素 增加删除的相关函数

  1. reserve:扩容,增加总空间
  2. size :获取当前 vector 中有效数据的空间大小
  3. capacity:获取 总空间大小
  4. push_back / pop_back:尾插 和 尾删
  5. insert / erase : 指定 pos 位置,删除该位置的元素
  6. find:给定迭代器区间,查找值为 val 的元素,返回下标迭代器

(1)size 和 capacity

size_t size() {
	return _finish - _start;
}

size_t capacity() {
	return _end_of_storage - _start;
}

(2)reserve:扩容,增加总空间

void reserve(size_t n)
{
	if (n > capacity) {
		T* tmp = new T[n];
		//strcpy(tmp, _start); // 这是字符串的拷贝函数,不能使用在这里
		memcpy(tmp, _start, sizeof(T) * n); // 使用memcpy拷贝数据
		delete[] _start;
		_start = tmp;

		_finish = _start + size();
		_end_of_storage = _start + n;  // n 就是 新的空间的 capacity
	}

}
size_t size()
{
	return _finish - _start;
}
size_t capacity()
{
	return _end_of_storage - _start;
} 

关于需要设置 oldSize(旧空间大小)

上面那段代码会程序崩溃

问题出在这

 _finish = _start + size();
size() == _finish - _start;

//代入后,相当于 _finish 原地不动
_finish = _start + _finish - _start = _finish;

我们更新 _finish 的原理是,使用新的 _start 间隔 一块已有有效数据空间的大小 size 的距离

而当前面的 _start 更新成新空间的位置时,

_start = tmp;

函数 size() 里面的 return 值也发生改变,其中的 _start 已经不是原有空间的 _start 了,变成新的了

所以我们需要的是  原有空间的 size ,而直接使用 size函数当然计算不出

因此,我们需要使用一个临时值,来存储旧 size 的大小

代码修改为

void reserve(size_t n)
{
	if (n > capacity) {
		size_t oldsize = size(); /

		T* tmp = new T[n];
		if (_start) {
			memcpy(tmp, _start, sizeof(T) * n);
			delete[] _start;
		}
		_start = tmp;

		_finish = _start + oldsize; /

		_end_of_storage = _start + n;  // n 就是 新的空间的 capacity
	}
}

关于拷贝数据的方式

memcpy 可以逐字节的拷贝数据

但是因为 memcpy 是浅拷贝,对一些自定义类型有影响(一些自定义类型需要深拷贝)

因此,这里换成循环,每次拷贝一个元素类型大小的数据

for (size_t i = 0; i < oldSize; ++i) {
	tmp[i] = _start[i];
}

最终代码

void reserve(size_t n) {
	if (n > capacity()) {
		size_t oldSize = size();

		T* tmp = new T[n];
		//memcpy(tmp, _start, sizeof(T) * oldSize);
		// 因为 memcpy 是浅拷贝,对一些自定义类型有影响,换成循环
		for (size_t i = 0; i < oldSize; ++i) {
			tmp[i] = _start[i];
		}
		delete[] _start; // 注意:这里 delete 如果会报错,好像是因为没写析构
		_start = tmp;

		_finish = _start + oldSize;
		_end_of_storage = _start + n;
	}
}

(3)push_back / pop_back:尾插 和 尾删

void push_back(const T& val) {
	//空间不够就扩容
	if (_finish == _end_of_storage) {
		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);
	}

	*_finish = val; // 尾巴赋值
	_finish++; // 尾巴后移
}

void pop_back() {
	assert(capacity() > 0);
	_finish--;
}

(4)insert / erase : 指定 pos 位置,删除该位置的元素

我们模拟库中的 vector ,使用迭代器,会比直接使用 size_t 类型的数字下标更加安全

// 插入
void insert(iterator pos, const T& val) {
	assert(pos <= _finish);
	assert(pos >= _start);
	// 扩容
	if (_finish == _end_of_storage) {
		size_t len = pos - _start;

		size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newCapacity);

		pos = _start + len;
	}

	// 后面的移动
	iterator end = _finish - 1;
	while (end >= pos) {
		*(end + 1) = *end;
		end--;
	}
	++_finish;
	// 如果扩容了,pos 还指向旧空间:迭代器失效
	*pos = val;
}
// 删除
iterator erase(iterator pos) {
	assert(pos < _finish);
	assert(pos >= _start);

	// 元素向前 移
	// 因为可能会缩容,导致 pos 指向已释放的空间
	// 库中的 erase 会返回刚刚删除的元素位置的下一个位置
	iterator end = pos + 1;
	while (end < _finish) {
		*(end - 1) = *(end);
		end++;
	}
	_finish--;

	return pos + 1;
}

关于 insert 扩容处 添加了 len

这是扩容产生的 迭代器失效问题,需要将 pos 重新移动位置(即现在 pos 指向已释放的空间,造成野指针形成的 迭代器失效问题 ,需要将 pos 指向新空间)

关于 erase 返回 iterator

因为删除数据,可能会导致缩容,也会使 pos 指向已释放的空间,产生 迭代器失效 的情况

如何解决?

库中的 erase 会返回刚刚删除的元素位置的下一个位置,我们模拟库中的写法,返回一个 iterator

(5)find:给定迭代器区间,查找值为 val 的元素,返回下标迭代器

在本文中,使用 find 配合 erase 删除某个元素

看文档中,find 是一种函数模板

template<class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& val)
{
	while (first != last) {
		if (*first == val) return first;
		++first;
	}
	return last;
}

配合 erase 使用

如在 v1 中,先找到值为 3 的数据,然后删除

void Test() {
	vector<int>v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	for (auto e : v1) {
		cout << e << ' ';
	}
	cout << '\n';


	// 删除 + find 
	vector<int>::iterator pos = find(v1.begin(), v1.end(), 3);
	v1.erase(pos);
	for (auto e : v1) {
		cout << e << ' ';
	}
	cout << '\n';
}

3. 构造 + 拷贝构造 + 析构

vector() = default;
// 拷贝构造
vector(vector<T>&v) {
	reserve(v.capacity());
	for (auto e : v) {
		push_back(e);
	}
}

// 析构
~vector() {
	if (_start) {
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

关于 vector() = default 

default 关键字:使编译器强制生成默认函数

因为在 vector 的实现中,直接使用默认生成的构造函数已经足够用,无需自己写

但是当我们显式实现 拷贝构造函数时,编译器不会默认生成构造函数

因此要使用关键字 default 强制生成

4. 迭代器 begin() / end() 

这里实现 iterator 和 const_iterator 两种

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

const_iterator begin() const {
	return _start;
}
const_iterator end() const {
	return _finish;
}

5. 迭代器区间拷贝

这是一种函数模板:可以将传递过来的迭代器区间内的 数据 拷贝

template<class inputiterator>
vector(inputiterator first, inputiterator last) {
	while (first != last) {
		push_back(*first);
		first++;
	}
}

6. 模拟库中的 拷贝构造

产生数量 n 个元素 ,初始化为 val,否则使用缺省值初始化

size_type 就是 size_t 
value_type 就是 T

// 模拟库的构造函数:vector (size_type n, const value_type& val = value_type()

vector(size_t n, const T& val = T()) {
	reserve(n); // 和拷贝构造那里一样,先使用 reserve
	for (size_t i = 0; i < n; ++i) {
		push_back(val);
	}
}

关于缺省值

这里的缺省值能不能直接给 零?

vector(size_t n, const T& val = 0);

若 T 为 int 的整型类型,给 0 确实可以

但是 T 为 string 呢?T 为 set 呢?

因此:一个 整型类型的 0 不能初始化所有种类的类型

这里修改

vector(size_t n, const T& val = T());

这个意思是:给自定义类型创建一个临时的匿名对象,初始化为这个

int 这种 内置类型可以这样写吗?如 int()

可以,C语言不支持,C++可以

就是因为出现 模板 这个语法,有了泛型编程的概念

T 可以是任意类型,初始化时会调用构造函数,T 可以是 自定义类型时如此,T 也可以是 内置类型呀

可是内置类型不是没有构造函数吗

因此为了兼容模板, C++ 对内置类型进行升级,使内置类型也有构造函数,也可以像下面这样初始化

int i = 10;
int j(10);
int  k = int();
int x = int(2);

关于函数匹配冲突问题

从上面的文章写下来的代码中

如果你这样写,会报错:非法的间接寻址

vector<int>v3(3, 5); 

这个函数会自动匹配上面讲解过的  迭代器区间拷贝函数

// 迭代器区间拷贝
template<class inputiterator>
vector(inputiterator first, inputiterator last) {
	while (first != last) {
		push_back(*first);
		first++;
	}
}

为什么不匹配   拷贝构造函数,而是上面这个?

vector(size_t n, const T& val = T()) {
	reserve(sizeof(T) * n);  // 一次开好空间,避免频繁扩容
	for (size_t i = 0; i < n; ++i) {
		push_back(val);
	}
}

因为在编译器的眼中,参数类型更加匹配的函数,会优先被选择

前者参数类型都是统一的 inputiterator

后者不一样:一个是 size_t,一个是 T(这里的 T = int)

因此优先匹配前者

但是使用一个 int 类型放到 一个关于迭代器的函数,就会出现一些语法错误:非法的间接寻址

在函数里,就是将 int 解引用使用了

如何避免冲突?

既然是类型惹的祸,可以从类型方面思考

我们可以添加一个新的重载函数(将第一个参数的 size_t 类型换成 int),这样编译器就会优先选择这个新的函数

vector(int n, const T& val = T()) {
	reserve(sizeof(T) * n);  // 一次开好空间,避免频繁扩容
	for (size_t i = 0; i < n; ++i) {
		push_back(val);
	}
}

7. 赋值重载 + 重载方括号

赋值重载

这个是一种 STL 的现代写法,不知道的可以了解一下

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

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

重载方括号

这个重载函数使 vector 可以像数组一样直接使用下标访问

一种是访问了需要修改:传引用

一种只需访问无需修改:const 修饰

// 重载方括号
T& operator[](size_t pos) {
	assert(pos < size());
	assert(pos >= 0);
	return _start[pos];
}

const T& operator[](size_t pos) const {
	assert(pos < size());
	assert(pos >= 0);
	return _start[pos];
}

8. (重点)关于花括号的隐式类型转换 与 initializer_list< T >

观察下面代码:关于对象的直接构造 / 隐式类型转换 / 花括号的隐式类型转换

class A {
public:
	A(int n)
		:_a1(n)
		, _a2(0)
	{}
	A(int x, int y)
		:_a1(x)
		, _a2(y)
	{}
private:
	int _a1; 
	int _a2;
};

void Test7()
{
	// 直接构造
	A a1(1);
	A a2(1, 2);

	// 单参数 和 多参数对象隐式类型转换
	A a3 = 1;
	A a4 = { 1, 2 };


	// C++11:提出一切皆可用花括号初始化(可以说,遇到花括号基本是 隐式类型转换)
	// 这几个是单参数的隐式类型转换(不建议单参数使用花括号操作,但看到别人代码这样写要懂)
	A a5 = { 1 };
	A a6{ 1 };  // 省略掉等号
	A a7{ 1, 2 }; // 省略掉等号
	const A& a8 = { 1 }; // 引用的是临时对象(常性)

}

我们使用本章学的 vector 试试:看看花括号能不能初始化 vector

//my::vector<int> v1 = { 1, 2, 3 };  // 报错
std::vector<int> v1 = { 1, 2, 3 }; // 用库里面的 vector 却不报错

甚至,花括号里面给多少值都正确,难道是库里面的 vector 构造函数 有好多参数(甚至无穷个)供你一一匹配吗?

std::vector<int> v1 = { 1, 2, 3 }; 
std::vector<int> v1 = { 1, 2, 3, 4}; 
std::vector<int> v1 = { 1, 2, 3, 4, 5}; 
std::vector<int> v1 = { 1, 2, 3, 4, 5, .....}; 

解释:

这种也是一种隐式类型转换,但是这里的参数匹配不是按花括号里面的几个数字一一匹配的,花括号括起来的所有值一起算作一个整体匹配到一个 参数中(而数字之间并非独立的参数个体)

这个“强大的”参数是 C++11新增的语法:initializer_list< T > 初始值设定项列表

大致基础功能,就和直接传一个 数组过去差不多(“该数组”里面存储着你自定义的几个值)


使用示范

auto in1 = { 1, 2, 3, 4 };
initializer_list<int> in2 = { 1, 2, 3, 4, 5 };

该函数模板 内部实际有两个指针,一个指向头,一个指向尾


前面为什么说 这个也算作隐式类型转换?

因为这也是一种类型,可以直接隐式类型转换

库中的vector:

vector (initializer_list<value_type> il)
std::vector<int> v1 = { 1, 2, 3 }; 
// 上面这样写,就相当于传一个参数过去,如下面
std::vector<int> v1({ 1, 2, 3 }); 

因此,仿照这个,我们自己模拟实现一个支持initializer_list 的 vector 构造函数

//  支持 initializer_list 的构造
vector(initializer_list<T> il) {
	reserve(il.size());
	for (auto e : il) {
		push_back(e);
	}
}

9. 总代码

vector.h

#pragma once
#include<iostream>
#include<vector>
#include<string>
#include<assert.h>
using namespace std;

/*
* 开发日志
* 、基础模板框架
* 、迭代器
* 、空间增长相关函数析构(push_back / pop_back  + 插入删除(同时实现 find)) + 析构
* 、拷贝构造:模拟库的构造函数(+int型的重载,为了不错误匹配)+ 原始的拷贝构造
* 、一般构造  +  支持 initializer_list 的构造
* 、赋值重载 + 重载方括号
* 、迭代器区间
*/

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


		// 析构
		~vector(){
			if (_start) {
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
			}
		}


		//  支持 initializer_list 的构造
		vector(initializer_list<T> il) {
			reserve(il.size());
			for (auto e : il) {
				push_back(e);
			}
		}

		// 1、迭代器
		iterator begin() {
			return _start;
		}
		iterator end() {
			return _finish;
		}
		const_iterator begin() const {
			return _start;
		}
		const_iterator end() const {
			return _finish;
		}


		// 2、空间增长相关函数

		size_t size(){
			return _finish - _start;
		}

		size_t capacity(){
			return _end_of_storage - _start;
		}

		void reserve(size_t n) {
			if (n > capacity()) {
				size_t oldSize = size();

				T* tmp = new T[n];
				//memcpy(tmp, _start, sizeof(T) * oldSize);
				// 因为 memcpy 是浅拷贝,对一些自定义类型有影响,换成循环
				for (size_t i = 0; i < oldSize; ++i) {
					tmp[i] = _start[i];
				}
				delete[] _start; // 注意:这里 delete 会报错,好像是因为没写析构
				_start = tmp;

				_finish = _start + oldSize;
				_end_of_storage = _start + n;
			}
		}

		void push_back(const T& val) {
			//空间不够就扩容
			if (_finish == _end_of_storage) {
				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);
			}

			*_finish = val;
			_finish++;
		}

		void pop_back() {
			assert(capacity() > 0);
			_finish--;
		}

		// 插入
		void insert(iterator pos, const T& val) {
			assert(pos <= _finish);
			assert(pos >= _start);
			// 扩容
			if (_finish == _end_of_storage) {
				size_t len = pos - _start;

				size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newCapacity);

				pos = _start + len;
			}

			// 后面的移动
			iterator end = _finish - 1;
			while (end >= pos) {
				*(end + 1) = *end;
				end--;
			}
			++_finish;
			// 如果扩容了,pos 还指向旧空间:迭代器失效
			*pos = val;
		}

		// 删除
		iterator erase(iterator pos) {
			assert(pos < _finish);
			assert(pos >= _start);

			// 元素向前 移
			// 因为可能会缩容,导致 pos 指向已释放的空间
			// 库中的 erase 会返回刚刚删除的元素位置的下一个位置
			iterator end = pos + 1;
			while (end < _finish) {
				*(end - 1) = *(end);
				end++;
			}
			_finish--;

			return pos + 1;
		}
		

		// 看文档中,find 是一种函数模板
		template<class InputIterator, class T>
		InputIterator find(InputIterator first, InputIterator last, const T& val)
		{
			while (first != last) {
				if (*first == val) return first;
				++first;
			}
			return last;
		}




		// 3、拷贝构造
		// 3.1 一般拷贝构造
		vector() = default;
		vector(vector<T>& v) {
			reserve(v.capacity());
			for (auto e : v) {
				push_back(e);
			}
		}
		void swap(vector<T>& v) {
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}
		// 2.2 模拟库的拷贝构造:给数量,初始化
		vector(size_t n, const T& val = T()) {
			reserve(sizeof(T) * n);  // 一次开好空间,避免频繁扩容
			for (size_t i = 0; i < n; ++i) {
				push_back(val);
			}
		}
		// 2.3 避免冲突
		vector(int n, const T& val = T()) {
			reserve(sizeof(T) * n);  // 一次开好空间,避免频繁扩容
			for (size_t i = 0; i < n; ++i) {
				push_back(val);
			}
		}
		


		// 4. 迭代器区间拷贝
		template<class inputiterator>
		vector(inputiterator first, inputiterator last) {
			while (first != last) {
				push_back(*first);
				first++;
			}
		}

		// 5、赋值重载 + 重载方括号
		// 赋值重载
		vector<T>& operator=(const vector<T>& v) {
			vector<T> tmp(v);
			swap(tmp);
			return *this;
		}
		// 重载方括号
		T& operator[](size_t pos) {
			assert(pos < size());
			assert(pos >= 0);
			return _start[pos];
		}
		const T& operator[](size_t pos) const {
			assert(pos < size());
			assert(pos >= 0);
			return _start[pos];
		}

	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值