C++ vector

目录

一.   初步了解

框架

构造、析构、赋值 

 构造

析构 

赋值

迭代器

容量

size、capacity、max_size

resize 、reserve

访问

下标访问

迭代器遍历 

增删

​编辑

分配

尾插、尾删、插入、删除、交换、清除

查找

模拟实现

框架

构造、析构、赋值

构造

析构

赋值

容量

size、capacity

reserve

resize

下标访问

增删

insert

erase

push_back、pop_back


一.   初步了解

框架

如果说string实质上是一个字符串的话,那么vector实质上就是一个数组,而与string不同的是,vector的成员变量其实是三个迭代器

iterator _start; // 指向数据块的开始

iterator _finish; // 指向有效数据的尾

iterator _endOfStorage; // 指向存储容量的尾

因此,vector的大多数接口都是围绕迭代器展开的

而string是basic_string类模板的一个实例化,但vector本身就是一个类模板,这一点也需要注意

因此在使用时,我们应该在使用单书名号包上所要实例化的类型

还有一点不同的是,字符串的末尾是有一个'\0'的,而字符类型的vector也没有'\0'

构造、析构、赋值 

 构造

首先呢,除了最后一个拷贝构造以外,我们都能看到一个东西:alloc,这其实是一个空间配置器,我们在这里可以忽略它。因此,第一种方法其实就是一个无参的构造,构造一个空的vector类。 第二种,则是使用n个val进行构造,第三种,使用的是前后两个迭代器,至于迭代器前面的Input之类的前缀,我们在模拟实现的时候再做介绍,我们只需要知道它是迭代器就行了

而在传迭代器时,传的不仅仅可以使vector的迭代器,还可以是其他的,例如string

void test1()
{
	vector<int> v1;
	vector<int> v2(5, 8);
	vector<int> v3(v2.begin() + 1, v2.end());
	vector<int> v4(v3);
}

而上面代码中所使用的begin和end的效果和string中的一样,都是取头部的迭代器和尾部后面的迭代器。

析构 

析构嘛,没什么东西

赋值

void test1()
{
	vector<int> v1;
	vector<int> v2(5, 8);
    v1=v2;
}

迭代器

熟悉的接口,熟悉的功能

void test2()
{
	vector<int> v1(5,8);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

容量

size、capacity、max_size

size大小、capacity容量,max_size有些许的不同,string中是字符串,字符大小固定为1字节,所以max_size也是固定的,而在vector中,本质是一个数组,而数组的数据类型有多种,max_size需要根据数据类型的大小来计算

resize 、reserve

void test4()
{
	vector<int> v1;
	v1.resize(5, 8);
	v1.reserve(10);
}

访问

下标访问

void test5()
{
	vector<int> v1(5, 8);
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
}

 

迭代器遍历 

void test5()
{
	vector<int> v1(5, 8);
	for (vector<int>::iterator it = v1.begin(); it != v1.end(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
}

 

增删

分配

 我们这里就只是来看一下迭代器方式,第二种就不看了,string 里讲过了

void test6()
{
	vector<int> v1(5, 8);
	vector<int> v2(3, 6);
	v2.assign(v1.begin() + 1, v1.end() - 1);
	for (int i = 0; i < v2.size(); i++)
	{
		cout << v2[i] << " ";
	}
	cout << endl;
}

尾插、尾删、插入、删除、交换、清除

就。。。很容易理解,重点还是模拟实现上,而插入时位置参数的类型是迭代器,稍微说说

void test7()
{
	vector<int> v1(4, 8);
	vector<int> v2(3, 6);
	v1.insert(v1.begin() + 2, 3);
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
	v1.insert(v1.begin() + 3, 2, 4);
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
	v1.insert(v1.begin() + 3,v2.begin(),v2.end());
	for (int i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
}

查找

我们在vector中并没有发现find这个接口,这是为什么呢?这是因为,在大多数容器中,都需要find这个接口,因此就把find函数封装到stl库中了

void test8()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	cout << *find(v1.begin(), v1.end(), 2) << endl;
}

 而为什么string有find这个接口呢?这是因为,string比较特殊,不仅需要查找字符,也需要查找字符串,而该find函数只能做到查找字符。


模拟实现

框架

依旧是需要一个命名空间放置冲突,不同的是由于是一个类模板,需要在头部加上tmplate...

而类的内部,我们说过,成员变量是三个迭代器,而在vector和string中,迭代器实质上就是指针,所以我们可以将指针重命名为迭代器的类型

namespace bit
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

构造、析构、赋值

构造

最简单的肯定就是无参的构造

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

之后,还有拷贝构造的传统写法,我们会使用到size()和capacity()的接口,先用着,后面实现。而在拷贝时,由于vector迭代器本质上是个指针,所以可以使用memcpy

vector(vector<T>& v)
{
	_start = new T[v.capacity()];
	_finish = _start + v.size();
	_endofstorage = _start + v.capacity();

	memcpy(_start, v._start, v.size()*sizeof(T));
}

然后,就是传迭代器,在前面,我们说了,不只是需要传vector的迭代器,同时,由于一个类模板的成员函数又可以是一个函数模板,因此,我们可以将传迭代器的构造函数写作一个函数模板

template<class InputIterator>
vector(InputIterator first, InputIterator last)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

而我们为什么会采用这样的名字来定义模板参数关键字呢?

这是因为,其实迭代器分为5种,input_iterator、output_iterator、forward_iterator、bidirectional_iterator以及randomaccess_iterator,根据,名字我们就可以猜出它们所能实现的功能。而从继承来看(后面学),后面的都是前面的子类(input...、output...是并列的),因此,在传参时,例如我们需要一个forward_iterator,那么我们只能传它或它的子类,也就是forward_iterator、bidirectional_iterator以及randomaccess_iterator。在讲清楚迭代器的分类后,我们上边的命名也就很好理解了,是作为一个暗示来提醒这里需要一个input_iterator。

实现了迭代器的构造之后,我们就可以实现一下拷贝构造的现代写法,依旧是构造+交换,而这里我们还需要begin()和end()的接口,由于类成员变量本身就是迭代器,所以很容易实现,我们就在这里顺便实现了

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

vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	vector<T> tmp(v.begin(), v.end());
	swap(tmp);
}

析构

~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = _finish = _endofstorage = nullptr;
	}
}

赋值

赋值的话我们·这里就直接写现代写法了

vector<T>& operator=(vector<T> v)
{
	swap(v);

	return *this;
}

容量

size、capacity

由于vector中的迭代器本质上就是指针,因此,我们可以用指针减指针来得到size和capacity

size_t size()
{
	return _finish - _start;
}

size_t capacity()
{
	return _endofstorage - _start;
}

reserve

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		memcpy(tmp, _start, sizeof(T) * size());
		_endofstorage += n - capacity();
		delete[] _start;
		_start = tmp;
		_finish = _start + size();
		_endofstorage = _start + n;
	}
}

这样写看上去是没什么问题,但是,其实在给_finish赋值的时候,size()的计算已经出现了问题,因为_start已经被赋值了,而_finish还是原来的位置,因此我们可以将_finish的赋值放在前面,当然,我们也可以使用一个变量来提前计算size()

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		memcpy(tmp, _start, sizeof(T) * size());
		_endofstorage += n - capacity();
		delete[] _start;
   		_finish = _start + size();
		_start = tmp;
		_endofstorage = _start + n;
	}
}

除此之外,还有一个问题,memcpy是浅拷贝,例如当我们的数组类型中的空间为动态开辟时,例如string,在进行浅拷贝时就会出现例如野指针的问题,因此我们要把memcpy改为深拷贝

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			for (size_t i = 0; i < sz; ++i)
			{
				tmp[i] = _start[i];
			}
				delete[] _start;
		}
			_start = tmp;
		_finish = _start + sz;
		_endofstorage = _start + n;
			}
		}

resize

依旧是注意好n的大小判断就好

void resize(size_t n, const T& val = T())
{
	if (n < size())
		_finish = _start + n;
	else
	{
		if (n > capacity())
			reserve(n);
		while (_finish != _endofstorage)
		{
			*_finish = val;
			_finish++;
		}
	}
}

而模拟实现好函数内部后,我们回过头来看一个问题,在开头的传引用中,我们使用了一个匿名对象作为val的缺省值,但我们知道,匿名对象的周期只有它所在的这一行,这又是为什么呢?

这是由于,若是匿名对象赋值给一个const引用的变量时,会延迟匿名对象的生命周期,使其与该变量保持一致,上面的生命周期就是在整个函数

下标访问

也挺简单的

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

T& operator[](size_t i)
{
	assert(i < size());
	return _start[i];
}

增删

insert

所注意的还是那几点,只是在一开始判断pos位置是否合法时要注意pos的类型时迭代器

iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _finish);
	if (_finish == _endofstorage)
		reserve(capacity() == 0 ? 4, 2 * capacity);
	for (iterator end = _finish; end > pos; end--)
	{
		*end = *(end - 1);
	}
	*pos = x;
	_finish++;
	return pos;
}

这段代码看起来可能也没有什么问题,但是真的没有问题吗?

真的没有问题我就不会说出来了,那么问题出在哪里呢?

可以看到,我们若是进行扩增的话,创建的是一个新的空间,而且将成员变量赋给新的位置,然而,成员变量被重新赋值了,那么pos呢?pos还是指向的原来的位置啊,没有人在意pos的感受吗?这也就导致了迭代器的失效的问题,因此,我们也就需要重新赋值一下pos

iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _finish);
	if (_finish == _endofstorage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4, 2 * capacity);
		pos = _start + len;
	}
	for (iterator end = _finish; end > pos; end--)
	{
		*end = *(end - 1);
	}
	*pos = x;
	_finish++;
	return pos;
}

小小的改进一下就好了,而返回pos这个迭代器也是为了避免原pos被继续使用而引发问题

erase

删除就不用考虑什么迭代器失效了,没什么可以失效的

iterator erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos <= _finish);
	for (iterator begin=pos; begin < _finish-1; begin++)
	{
		*begin = *(begin + 1);
	}
	_finish--;
	return pos;
}

但是,真的就不会引发迭代器的失效了吗?

例如我们想实现删除数组中的偶数

vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
    if (*it % 2 == 0)
    {
        v1.erase(it);
    }
    ++it;
}

要是这么写就出问题了,在删除了一个偶数之后,it迭代器指向的位置是不变的,而原位置已经被原本的下一个位置所覆盖,因此,在删除过后,其实it就算是挪到了下一个位置,不需要再进行++,如果进行++,若是刚好在最后一个位置,还是会引发迭代器的失效,即使不在最后一个位置,也会引发一些其他的错误

vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
    if (*it % 2 == 0)
    {
        v1.erase(it);
    }
    else
    {
        ++it;
    }
}

当然,string类中也有迭代器失效,情况和vector差不多

push_back、pop_back

这俩就真没什么好说的了

void push_back(const T& x)
{
	if (_finish == _endofstorage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	*_finish = x;
	++_finish;
}

void pop_back()
{
	assert(_finish > _start);

	--_finish;
}

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

finish_speech

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

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

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

打赏作者

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

抵扣说明:

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

余额充值