C++ --- vector的简单实现

发布于:2025-06-26 ⋅ 阅读:(14) ⋅ 点赞:(0)

引言

本次vector的成员属性是三个指针,分别是_start,_finish,_end_of_storage,如下图所示:

在这里插入图片描述
这是一个vector对象,_start指向顺序表表头,_finish指向容器实际空间大小,_end_of_storage指向容器的容量大小。
对比string类的的成员属性,_str,_size,_capacity,在vector中_start就是_str,_size就是_finish 减去 _start,_capacity就是_end_of_storage 减去 _start,本质上没有什么区别。

并且此容器使用模板类(模板不能声明定义分离),以满足任意类型。

一、默认成员函数

1.构造函数

1.1 默认构造函数

由于本次实现vector所定义的成员属性都是指针,所以直接赋值为空指针即可。

// 构造函数
vector()
	:_start(nullptr)
	,_finish(nullptr)
	,_end_of_storage(nullptr)
{}

对于上述代码还能简化,在属性声明处直接给定缺省值nullptr,因为不管是什么属性,都会走一遍初始化列表,有显式写就如上述代码一样,就用括号内给定的值进行初始化,若没有显式写则会使用缺省值进行初始化,若连缺省值都没有,那么编译器就会默认初始化了,可能是给定nullptr,也可能是给定随机值。

class vector
{
private:
	iterator _start = nullptr;
	iterator _finish = nullptr;
	iterator _end_of_storage = nullptr;
};

// 构造函数
vector()
{}

// 也可以这样写
// 这是强制编译器生成默认构造
vector() = default;

1.2 初始化列表构造

使用initializer_list进行构造,即对象可以像这样:对象名 = { val1,val2,…… },如此构造。

// 初始化列表构造
vector(initializer_list<T> il)
{
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

1.3 迭代器区间构造

此构造也是写成一个模板,以满足使用不同类型的迭代器区间进行构造,例如string对象的迭代器区间去构造一个vector对象。

// 迭代器区间构造
// 为何要使用模板,因为可以使用不同类型的迭代器区间去构造vector对象
// 可以不局限于一种类型
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

1.4 n个val构造

// n个val进行构造
vector(size_t n, T val = T())
{
	resize(n, val);
}

vector(int n, T val = T())
{
	resize(n, val);
}

n个val构造会和上面迭代器区间构造进行重合,若只想使用n个val构造则需要明确类型,要么都是size_t,要么都是int,否则都会调到迭代器区间构造去,并且n个val构造的参数都是整数,被调入迭代器区间构造还会报错:非法的间接寻址。

2.拷贝构造函数

// 拷贝构造 --- v2(v1)
vector(const vector<T>& v)
	// 要先初始化一下,要不然reserve开同样大小的空间的时候会出问题 --- 即这三个指针在不同编译器的默认初始化是给nullptr还是随机值
	// 初始化操作已经被缺省值代替了 
{
	reserve(v.capacity());
	for (auto& e : v)
	{
		push_back(e);
	}
}

3.析构函数

// 析构函数
~vector()
{
	// 当_start不为空时
	if (_start)
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

4.运算符重载

4.1 operator=

// 赋值运算符重载
// 现代写法
vector& operator=(const vector<T>& v)
{
	// 对this和v进行交换,同样达到赋值效果
	swap(v);  
	return *this;
}

4.2 operator[ ]

两个版本,针对const和非const对象。

T& operator[](size_t i)
{
	// i位置要合法
	assert(i < size());
	return _start[i];
}

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

二、遍历方式

1.下标 + [ ]

由于vector底层是用一个数组来存储数据的,所以可以使用下标+[ ]来遍历对象。

// 下标 + [ ]
for (size_t i = 0; i < v.size(); i++)
{
	cout << v[i] << " ";
}

2.迭代器

此迭代器由原生指针实现,也是由于底层是数组,可以使用指针来实现迭代器,解引用迭代器就是该位置上的元素,并且只实现四个版本,const正向迭代器和非const正向迭代器,反向迭代器不在这里实现,因为其后可以使用适配器直接由正向迭代器适配出反向迭代器。

typedef T* iterator;
typedef const T* const_iterator;

// 非const正向迭代器
iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

// const正向迭代器
const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

// 使用
// 迭代器
dyj::vector<int>::iterator it1 = v.begin();
dyj::vector<int>::iterator it2 = v.end();
for (it1; it1 != it2; it1++)
{
	cout << *it1 << " ";
}

3.范围for

范围for是基于迭代器实现的,实现了迭代器就能使用范围for。

// 范围for
// 这里v是vector对象
for (auto e : v)
{
	cout << e << " ";
}

三、增删改查

1.reserve()

扩容方法。

void reserve(size_t n)
{
	// 只实现扩容,缩容不实现
	if (n > capacity())
	{
		// 下面拷贝数据后因为_start先比_finish改变了,会导致size()计算错误,即不能使用拷贝后的size,要使用拷贝前的size
		size_t oldsize = size();

		// 需要扩容
		T* temp = new T[n];
		if (_start)
		{
			// 拷贝旧空间的数据到新空间
			// memcpy进行的是一个字节一个字节的拷贝,对于内置类型没有问题,但对于string这类的自定义类型就不行
			// 因为string的成员有一个_str,_str指向一个堆上开辟的空间,使用memcpy直接将旧的_str拷贝给新的_str
			// 导致新旧空间的_str都指向同一块空间,旧空间被释放,自然新空间指向的也就被释放,导致产生随机值。
			//memcpy(temp, _start, sizeof(T) * size());
			for (size_t i = 0; i < oldsize; i++)
			{
				temp[i] = _start[i];
			}
			delete[] _start;
		}
		_start = temp;
		_finish = _start + oldsize;
		_end_of_storage = _start + n;
	}
}

本方法有两个需要注意的点:
(1)扩容完成之后在更新指针时,_finish的更新需要使用扩容之前的size大小,因为size是由_finish 减去 _start得来的,而_start最先修改,会导致计算出一个错误的size值,所以这里要使用旧的size大小。
(2)旧空间拷贝到新空间的时候,对于memcpy函数,内置类型没有问题,内存块的直接复制,而对于自定义类型,例如string,它的成员属性有一个指向数组的指针_str,使用memcpy会导致新旧空间的_str都指向同一块空间,后续有一步delete,旧空间被释放,自然新空间指向的也就被释放,导致产生随机值,所以进行赋值操作,直接连同指向的空间一起拷贝。

2.resize()

扩容或者缩容方法。
当给定的参数n(size_t类型),大于容器size时,会扩容,并且填充给定的val值,大于容器的capacity时也是如此;当给定的参数n(size_t类型),小于容器size时,会缩容,缩小到给定的n值处。

// 这里模板参数给定缺省值使用匿名对象,由于T可以是任意一种类型,导致缺省值不能是某一种类型
// 所以这里使用匿名对象,直接通过构造函数来给定缺省值,并且在C++里,内置类型也进行了升级
// 可以像创建对象一样使用其语法
// 例如:int i = 0;
//       int j(1);
//       int k = int();
//       int l = int(10);
void resize(size_t n, T val = T())
{
    // 扩容
	if (n > size())
	{
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
	// 缩容
	else
	{
		_finish = _start + n;
	}
}

需要注意泛型给定缺省值可以使用匿名对象,直接通过构造函数来给定缺省值。

3.push_back()

尾插方法。

void push_back(const T& x)
{
	// 先判断是否扩容
	if (_finish == _end_of_storage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);
	}

	*_finish = x;
	++_finish;
}

4.pop_back()

尾删方法。

void pop_back()
{
	// 首先要判断是否能够尾删,即顺序表中还有元素 --- _start != _finish / !empty()
	//assert(_start != _finish);
	assert(!empty());
	--_finish;
}

5.insert()

任意位置插入方法。

iterator insert(iterator pos, const T& x)
{
	// 首先判断pos位置是否合法
	assert(pos >= _start && pos <= _finish);

	// 首先判断是否需要扩容
	if (_finish == _end_of_storage)
	{
		// 及时更新pos位置,避免迭代器失效
		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;
	}

	// 插入数据
	*pos = x;
	++_finish;

	return pos;
}

这里需要注意扩容会导致迭代器失效,扩容的迭代器失效是因为扩容之后会更新成员指针,但是pos位置的指针没有更新,导致迭代器失效,从而无法正确插入数据,对于失效的迭代器,要么坚决不再使用,要么及时更新指针

6.erase()

任意位置删除方法。

iterator erase(iterator pos)
{
	// 首先判断pos位置是否合法,即[_start,_finish),右边为开区间是因为_finish是最后一个有效位置的下一个位置
	// 对于erase来说位置是非法的
	assert(pos >= _start && pos < _finish);

	iterator it = pos + 1;
	while (it != _finish)
	{
		*(it - 1) = *it;
		++it;
	}

	--_finish;

	return pos;
}

同样erase方法也会导致迭代器失效,不过和insert有些区别。
下面有一个删除容器中所有的偶数的示例,可以直观的感受erase的迭代器失效。

dyj::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(4);
v.push_back(5);
 v.push_back(4);

Print(v);

// 实现一个程序,用来删除顺序表中所有的偶数
auto it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
	{
		it = v.erase(it);
	}
	else
	{
		// 此时迭代器也是失效的,因为VS编译器下对于迭代器失效的检查是非常严格的
		// 当erase(it)之后,it位置就已经失效了,之后再对it进行操作则会引发崩溃等检查措施
		// 所以解决方法就是要么不再使用此迭代器了,要么更新it的位置,使得不再失效
		++it;
	}
}
Print(v);

同样还是那句话 对于失效的迭代器,要么坚决不再使用,要么及时更新指针

7.empty()

判断容器是否为空的方法。

bool empty()
{
	// 判段顺序表是否为空 --- 为空(返回true)、不为空(返回false)
	return _start == _finish;
}

8.clear()

清空容器的方法。

void clear()
{
	_finish = _start;
}

四、其他

1.size()

计算容器的实际size大小。

// 实际空间大小
size_t size() const
{
	// 指针 - 指针
	return _finish - _start;
}

2.capacity()

计算容器的容量大小。

// 空间容量
size_t capacity() const
{
	return _end_of_storage - _start;
}

3.swap()

交换方法。

void swap(const vector<T>& v)
{
	_start = v._start;
	_finish = v._finish;
	_end_of_storage = v._end_of_storage;
}

五、总结

本次对于vector的简单实现,当中有两个注意点,一是迭代器失效(insert和erase),二是深层次的深浅拷贝问题(reserve中扩容),这都是非常需要注意的易错点。
对于迭代器失效要么坚决不再使用,要么及时更新指针**
对于深层次的深浅拷贝问题,直接使用空间复制,连同指向的空间也一并复制过来,避免指向同一块空间,导致析构两次,程序崩溃。


网站公告

今日签到

点亮在社区的每一天
去签到