【C++】STL - Vector类的模拟实现

vector当中的成员变量

在vector当中有三个成员变量_start、_finish、_endofstorage。

_start指向容器的头,_finish指向容器当中有效数据的尾,_endofstorage指向整个容器的尾。

默认成员函数

构造函数

vector支持一个无参的构造函数,直接将构造对象的三个成员变量都设置为空指针即可。

cpp 复制代码
vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{}

vector还支持使用一段迭代器区间进行对象的构造。这里要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中。

cpp 复制代码
template <class InputIterator>
vector(InputIterator begin, InputIterator last) 
    :_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	while (begin != last) {
		push_back(*begin);
		begin++;
	}
}

vector还支持构造容器,该容器当中含有n个值为val的数据。对于该构造函数,我们可以先使用reserve函数将容器容量先设置为n,然后使用push_back函数尾插n个值为val的数据到容器当中即可。

cpp 复制代码
vector(size_t n, const T val = T()) 
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(n);
	for (int i = 0; i < n; i++) {
		push_back(val);
	}
}

**注意:**该构造函数还需要实现一个重载函数,否则我们调用以下代码时会匹配错误,因而进行解引用后报错

cpp 复制代码
vector<int> v2(10, 0);
cpp 复制代码
vector(int n, const int val = T()) 
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(n);
	for (int i = 0; i < n; i++) {
		push_back(val);
	}
}

拷贝构造函数

vector的构造函数涉及深拷贝问题,这里提供两种深拷贝的写法:

写法一:传统写法

先开辟一块与该容器大小相同的空间,然后将该容器当中的数据一个个拷贝过来即可,最后更新_finish和_endofstorage的值即可。

cpp 复制代码
vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	_start = new T[v.capacity()]; 
	for (size_t i = 0; i < v.size(); i++)
	{
		_start[i] = v[i];
	}
	_finish = _start + v.size(); 
	_endofstorage = _start + v.capacity();
}

**注意:**我们这里不能使用memcpy,当vector存储的数据的类型是需要深拷贝的自定义类型时,使用memcpy会浅拷贝从而导致析构报错。

写法二:现代写法

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

cpp 复制代码
vector(const vector<T>& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	reserve(v.capacity()); 
	for (auto e : v) 
	{
		push_back(e);
	}
}

这里范围for在进行遍历时,变量e用的是每一个数据的拷贝,e会自动调用自定义类型的拷贝构造

赋值运算符重载函数

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

写法一:传统写法

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

cpp 复制代码
vector<T>& operator=(const vector<T>& v)
{
	if (*this != v)
	{
		delete[] _start; 
		_start = new T[v.capacity()]; 
		for (size_t i = 0; i < v.size(); i++) 
			_start[i] = v[i];
		}
		_finish = _start + v.size();
		_endofstorage = _start + v.capacity(); 
	return *this; 
}

写法二:现代写法

先在右值传参时并没有使用引用传参,因为这样可以间接调用vector的拷贝构造函数,然后将这个拷贝构造出来的容器v与左值进行交换,此时就相当于完成了赋值操作,而容器v会在该函数调用结束时自动析构。

cpp 复制代码
vector<T>& operator=(vector<T> v) {
	swap(v);
	return *this;
}

析构函数

对容器进行析构时,先释放容器存储数据的空间,然后将容器的各个成员变量设置为空指针即可。

cpp 复制代码
~vector() {
	delete[] _start;
	_start = _endofstorage = _finish = nullptr;
}

迭代器相关函数

vector当中的迭代器实际上就是容器当中所存储数据类型的指针。

cpp 复制代码
typedef T* iterator;
typedef const T* const_iterator;

begin和end

vector当中的begin函数返回容器的首地址,end函数返回容器当中有效数据的下一个数据的地址。

cpp 复制代码
iterator begin() {
	return _start;
}
const_iterator begin() const{
	return _start;
}
iterator end() {
	return _finish;
}
const_iterator end() const{
	return _finish;
}

现在我们实现了迭代器,实际上也就可以使用范围for遍历容器了,因为编译器在编译时会自动将范围for替换为迭代器的形式。

cpp 复制代码
void test() {
	std::string s("hello world");
	vector<int> v4(s.begin(), s.end());
	vector<int> v5(v4);

	vector<int>::iterator vit = v5.begin();
	while (vit != v5.end()) {
		cout << *vit << endl;
		vit++;
	}

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

容量和大小相关函数

size和capacity

对照着vector当中三个成员遍历各自的指向,我们可以很容易得出当前容器中的有效数据个数和最大容量。

cpp 复制代码
size_t capacity() const{
	return _endofstorage - _start;
}
size_t size() const{
	return _finish - _start;
}

reserve

当n大于对象当前的capacity时,将capacity扩大到n或大于n。

当n小于对象当前的capacity时,什么也不做。

在reserve函数的实现当中有两个地方需要注意:

1、提前记录当前容器当中有效数据的个数。

我们最后需要更新_finish指针的指向,计算出有效数据的偏移量,当_start指针的指向改变后我们再调用size函数通过_finish - _start计算出的有效数据的个数就是一个随机值了。

cpp 复制代码
void reserve(size_t n) {
	if (n > capacity()) {
		size_t sz = size();
		T* tmp = new T[n];
		if (tmp == nullptr) {
			perror("reserve");
			return;
		}
		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 = tmp + sz;
		_endofstorage = tmp + n;
	}
}

2、拷贝容器当中的数据时,不能使用memcpy函数进行拷贝。

原因同拷贝构造

resize

当n大于当前的size时,将size扩大到n,扩大的数据为val,若val未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。

当n小于当前的size时,将size缩小到n。

在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T( )即可。

cpp 复制代码
void resize(size_t n, const T& val = T()) {
	if (n > size()) {
		reserve(n);
		T* end = _start + n;
		while (_finish != end) {
			*_finish = val;
			_finish++;
		}
	}
	else {
		_finish = _start + n;
	}
}

empty

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

cpp 复制代码
bool empty()const{
	return _start == _finish;
}

修改容器内容相关函数

push_back

要尾插数据首先得判断容器是否已满,若已满则需要先进行增容,然后将数据尾插到_finish指向的位置,再将_finish++即可。

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

pop_back

尾删数据之前也得先判断容器是否为空,若为空则做断言处理,若不为空则将_finish--即可。

cpp 复制代码
void pop_back() {
	assert(size() > 0);
	_finish--;
}

insert

insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。

cpp 复制代码
void insert(iterator pos, const T& x) {
	assert(pos >= _start);
	assert(pos <= _finish);
	if (pos == _finish) {
		push_back(x);
		return;
	}
	if (_finish == _endofstorage) {
		size_t n = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + n;
	}
	iterator end = _finish;
	iterator cur = _finish - 1;
	while (end != pos) {
		*end = *cur;
		end--;
		cur--;
	}
	*pos = x;
	_finish++;
}

erase

erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。

cpp 复制代码
iterator erase(iterator pos) {
	assert(pos < _finish);
	assert(pos >= _start);
	iterator cur = pos;
	while (cur != _finish) {
		*cur = *(cur + 1);
		cur++;
	}
	_finish--;
	return pos;
}

swap

swap函数用于交换两个容器的数据,我们可以直接调用库当中的swap函数将两个容器当中的各个成员变量进行交换即可。

cpp 复制代码
void swap(vector<T>& tmp) {
	std::swap(_start, tmp._start);
	std::swap(_finish, tmp._finish);
	std::swap(_endofstorage, tmp._endofstorage);
}

访问容器相关函数

operator

vector也支持我们使用"下标+ "的方式对容器当中的数据进行访问,实现时直接返回对应位置的数据即可。

cpp 复制代码
T& operator[](size_t pos) {
	assert(pos < size());
	return *(_start + pos);
}
const T& operator[](size_t pos) const{
	assert(pos < size());
	return *(_start + pos);
}

vector迭代器失效问题

迭代器的主要作用就是让我们在使用各个容器时不用关心其底层的数据结构,而vector的迭代器在底层实际上就是一个指针。迭代器失效就是指迭代器底层对应指针所指向的空间被销毁了,而指向的是一块已经被释放的空间,如果继续使用已经失效的迭代器,程序可能会崩溃。

迭代器失效问题举例

cpp 复制代码
void test4() {
	vector<int> v;
	v.push_back(2);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
		
	for (auto& e : v) {
		cout << e << " ";
	}
	cout << endl;

	auto it = v.begin();
	while (it != v.end()) {
		if (*it % 2 == 0) {
			v.erase(it);
		}
	}

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

这里根本没有对第二个数进行判断

迭代器失效解决方法

每次使用前,对迭代器进行重新赋值。

cpp 复制代码
void test4() {
	vector<int> v;
	v.push_back(2);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
		
	for (auto& e : v) {
		cout << e << " ";
	}
	cout << endl;

	auto it = v.begin();
	while (it != v.end()) {
		if (*it % 2 == 0) {
			it = v.erase(it);
		}
		else {
			it++;
		}
	}

	for (auto& e : v) {
		cout << e << " ";
	}
	cout << endl;
}
相关推荐
wuminyu1 小时前
HotSpot的轻量锁与膨胀后的ObjectMonitor状态重建原理
java·linux·c语言·jvm·c++
一只小透明啊啊啊啊1 小时前
Socket、AIDL、SOME/IP
c++·mcu
Tim_103 小时前
【LeetCode】338、比特位计数
c++·算法·leetcode
程序员AlbertTu3 小时前
# Mantissa 使用教程 — Python 版与 C++ 版
c++·python·数值运算
Lazionr4 小时前
多态:从多种形态到运行时绑定
开发语言·c++
别动我齐刘海4 小时前
ROS2 Jazzy + C++ 实战路线——ros2_control
c++·人工智能·python·opencv·机器学习·机器人·github
All for pursuit.4 小时前
【栈-4】739.每日温度
数据结构·c++·算法·leetcode
码匠许师傅4 小时前
【C++三方组件】glog:Google 出品的 C++ 日志库
c++
Lazionr5 小时前
二叉搜索树:从树形结构到高效查找
开发语言·c++