c++模拟实现vector

/*
1.引入函数的模板
2.const修饰匿名函数的生命周期会延长
3.在初始化时,调用要初始化对象的构造函数
4.memcpy进行的“假”浅拷贝
5.等号两边为同类型时,会调用其复制重载。

6.迭代器失效,以及如何修正迭代器

7.类中的函数模板

*/

vector作为一个容器,不仅仅可以接收char还可以接收int、double甚至自定义类型,所以它的设计就不能和string的设计一样。

框架如下:

class vector {
public:
typedef T* iterator;
//code...

private:
	iterator _start=nullptr;//指向容器的开头
	iterator _end=nullptr;//指向容器已存在内容的结尾
	iterator _end_of_storage=nullptr;//指向容器的结尾
};

这里使用了默认参数

1.构造函数:

vector() :
	_start(nullptr),
	_end(nullptr),
	_end_of_storage(nullptr)
{}

vector(size_t n, const T& v = T()) {  //queston1
	_start = new T[n];
	_end = _start;
	_end_of_storage = _start + n;
	while (_end != _end_of_storage) {
		*_end = v;       //queston2
		_end++;
	}
}
//拷贝构造
vector(const vector<T>&v) {
	_start = new T[v.capacity()];
	_end = _start;
	_end_of_storage = _start + v.capacity();
	for (size_t i = 0; i < v.size(); i++)
	{
		_start[i] = v._start[i];
	}
}

思考: 

一、

vector(size_t n, const T& v = T()) 

为什么这里要使用const T &v=T(),把这个换成const T&v=0行不行?

答:

不可以

在vector中,其存储的类型不仅仅为int,还要其他类型比如string类甚至包含自定义类型

如果换为const T &v=0,则在传递string类或其他类型时,产生错误。

那么T()是什么?

匿名函数,这里是T类型的构造函数,设计时,我们不可能知道使用者会用vector初始化何种类型,所以只能调用使用者给出的类型的默认构造参数作为v的值,这样不论使用的人传递哪种类型,这里都可以对它进行初始化。

const在此处的另一种含义:已知匿名对象的声明周期只有这一行,这点,我们在前面就已知,那么我们使用const修饰匿名对象,const会延长匿名对象的生命周期,直到对引用对象域结束。这里的const必不可少。

二、此处的等号的含义:

c++中,等号两边都为同种类型时,会调用其赋值重载

比如string s1  string s2;   s1=s2,会调用string 类的赋值重载(=号重载)

那么在这里_end为T* ,*_end为T类型,而参数v也为T类型,所以这里满足

T = T,会调用其赋值重载。

另外

等看我后文写的提示后,再来看此文章

vector中模拟实现让我困惑好久的东西-优快云博客

此文章对这里的=号进行了对于本次代码设计的进一步介绍。

2.计算size与capacity()

size_t size()const {
	return _end - _start;
}
size_t capacity() const{
	//cout << _end_of_storage - _start << endl;
	return _end_of_storage - _start;
}

3.扩容:

void reserve(size_t n) {
	//应该这样写:
	if (n > capacity()) {   //question1
		
		size_t oldSize = size();
		T* tmp = new T[n];
		for (int i = 0; i <oldSize; i++) {    //quesiton2
			tmp[i] = _start[i];
		}
		delete[]_start;
		_start = tmp;
		_end = _start + oldSize;
		_end_of_storage = _start + n;
	}
}

一、重点:

****  警惕:memcpy造成的"浅拷贝"   ***

如果写为

if (n>capacity()) {
        size_t oldCapacity = capacity();
        T* tmp = new T[n];
        memcpy(tmp,_start,sizeof(T)*oldCapacity);
        delete[]_start;
        _start = tmp;

}

这样写可以吗?不可以,虽然我们开辟了空间、也进行了memcpy拷贝,但仍然时浅拷贝。
    理由:
    假如我们初始化了vector<string>,
    vector的成员变量为:_start,_end_end_of_storage;
    string的成员变量为:_str,_size,_capacity;
    然后_start指向的是string这个容器,而不是_string的_str
    这样在进行memcpy时,将_start的东西拷贝给tmp,实则是将string这个容器的地址拷贝了过去
    所以是进行的浅拷贝

为了直观清楚的表达出问题所在,现在使用string来作为自定义类型,扩容

二、还请注意size_t 与Int的比较,会有类型的隐式提升

思考:
    现在假设定义vector<vector>上面这个程序(不是思考中的程序)还正确吗?
    很明显不正确,因为我们调用的是vector的等号赋值重载,而
    vector的复制重载还没有实现出来,所以是错误的。
    下面我们应该把vector的复制重载写出来。

void swap(vector<T>& v) {
	std::swap(_start,v._start);
	std::swap(_end,v._end);
	std::swap(_end_of_storage,v._end_of_storage);
}
vector<T>& operator=(vector<T> v) {//这里会调用拷贝构造,并且要写上模板参数
	swap(v);
	return *this;
}

这里的交换函数思路如下:

赋值重载的参数为非引用,会创建传进来的参数的拷贝,然后将这个拷贝放进swap中,与_start _end _end_of_storage互换。

至此,看到这里便可返回上文,查看另一篇文章的链接了,我再把链接贴一遍:

vector中模拟实现让我困惑好久的东西-优快云博客

void resize(size_t n,const T&v=T()) {
	if (n>size()) {
		if (n>capacity()) {
			reserve(n);
			
		}
		_end_of_storage = _start + n;
		while (_end!=_end_of_storage) {
			*_end = v;
			_end++;
		}
	}
}

4.迭代器

typedef T* iterator;
typedef const T* const_iterator;
iterator begin() {
	return _start;
}
iterator end() {
	return _end;
}
const_iterator begin()const {
	return _start;
}
const_iterator end()const {
	return _end;
}

5.从任意位置插入

iterator insert(iterator it,const T& v) {
	if (size()==capacity()) {
		size_t len = it - _start;
		reserve(capacity() == 0 ? 10 : capacity() * 2); //question1
		it = _start + len;//扩容后迭代器失效了,需要修正
	}
	iterator end = _end;
	while (end!=it) {
		*(end) = *(end - 1);
		end--;
	}
	*it = v;
	_end++;

	return it;
}

思考:为什么返回值要使用iterator 为什么函数参数要使用iterator it ,使用pos it不可以吗?

以及迭代器失效问题。



先来看迭代器失效的问题:迭代器失效,实际就是迭代器底层对应指针所指向的 空间被销毁了,而使用一块已经被释放的空间。更广义的迭代器失效,不仅仅指指向的一片被释放的空间

举个例子:

1.所指向的内容被释放掉了

在c++中的扩容都是异地扩容,而不是原地扩容,很容易想象到:

假设

string s1("abcd");

auto it =s1.begin();  //it指向a

s1.resize(100);//对s1进行扩容为100 c++为了处理扩容,就需要开辟新的空间,将旧空间里面的数拷贝过去,然后释放掉旧的空间。

此时我们在对it进行访问,则会处问题因为it指向的是一个被释放掉的空间,成为了野指针。

2.迭代器不失效,但会引起的歧义:

void text_list() {
	list<int>ret;
	ret.push_back(0);
	ret.push_back(1);
	ret.push_back(2);
	ret.push_back(3);
	auto it = ret.begin();
	ret.insert(it, 9);
	for (auto val : ret) {
		cout << val << " ";
	}
	cout << endl;
	cout << *it << endl;
}
void text_vector() {
	vector<int>ret;
	ret.reserve(10);
	ret.push_back(0);
	ret.push_back(1);
	ret.push_back(2);
	ret.push_back(3);
	auto it = ret.begin();
	ret.insert(it, 9);
	for (auto val : ret) {
		cout << val << " ";
	}
	cout << endl;
	cout << *it << endl;
}
int main() {
	cout << "list------------" << endl;
	text_list();
	cout << "vector----------------" << endl;
	text_vector();

	return 0;
}

使用了两个函数,分别输出在插入后,打印迭代器所指向的值。

output:

list------------
9 0 1 2 3
0
vector----------------
9 0 1 2 3

9  //这里其实会报错,vs强制检查,通过调试可以得出这个结果为9

我们发现使用迭代器调用begin,然后再在该迭代器处插入数据,最后打印迭代器,这两个迭代器的值居然不一样。--这也是迭代器引起的第二个错误,严格意义上不能算迭代器的失效,针对不同的实现方法,迭代器的值也会产生变化,那么插入数据时,我们甚至都不会提前知道数组是否扩容,所以使用插入后的没有更新的迭代器是危险的行为!!!

注意:使用erase也会造成迭代器的失效:

#include <iostream>
using namespace std;
#include <vector>
int main()
{
 int a[] = { 1, 2, 3, 4 };
 vector<int> ret(a, a + sizeof(a) / sizeof(int));
 vector<int>::iterator pos = ret.begin();
 // 删除pos位置的数据,导致pos迭代器失效。
 ret.erase(pos);
 cout << *pos << endl; // 此处会导致非法访问
 return 0;
}

erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代 器不应该会失效,这一点和我上述所描述的相同,但是:如果pos刚好是最后一个位置的元素,删完之后pos刚好是end的位置,而end位置是 没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效 了。考虑考虑如果删除list中的,那么不论是否是最后位置的元素,都会造成迭代器失效

思考:什么操作会导致迭代器失效?

答:会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、 push_back等。

那么仅仅是vector、list会引起迭代器失效吗?不不不,string进行相关的操作也会引起迭代器的失效。

迭代器失效的解决办法:

在使用前,对迭代器重新赋值即可。



好,迭代器失效已经解释清楚,下面回答:为什么返回值要使用iterator 为什么函数参数要使用iterator it ,使用pos it不可以吗?

1.为什么返回值要使用iterator。因为迭代器会失效,所以我们在这里返回修正后的迭代器。

2.为什么函数模板要使用iterator,这是在这里使用int pos也可以,只不过到了后面的list再使用int pos就有局限性了,访问慢。

知识点--在类中定义新的类模板

template<class T>
class vector{
    //code...
    template<class InputIterator>
    vector(InputIterator first, InputIterator last) {
	    while (first != last) {
		    push_back(*first);
		    first++;
	    }
    }
    //code.

}

 用处:我们有个string类的对象,我们想用string类去初始化vector<int>,就可以使用在始终定义新的类模板来接收参数,为什么是string,这里因为string为字符,字符可以转换为数字,所以只要能相互转化的都可以这样使用。

	string s("abcd");
	vector<int> ret(s.begin(),s.end());
	for (auto val:ret) {
		cout << val << " ";
	}

来看看什么时候会匹配到这里:vector(InputIterator first, InputIterator last)

这里的参数类型都是Inputterator,也就是我们传进来的两个参数类型相同,再没有更好的匹配下,就会匹配这里?

什么是更好的?

vector(int a ,int b)  vector(size_t ,int b)

vector(10,1);这个会去匹配vector(int a, int b)因为这个没有涉及到类型转换,与我们传进去参数更加匹配。

但是我们在程序里面加上这个后,再使用vector(10,1)时,则会匹配到这里,而不会匹配到

vector(size_t n, const T& v = T())该构造函数里面,因为该构造函数里面涉及到了类型转换。

所以为了能使vector(10,1)能正常匹配,那么我们就要重载vector(size_t n, const T& v = T())为

vector(int n,const T&v=T())。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

蠢 愚

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

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

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

打赏作者

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

抵扣说明:

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

余额充值