c++顺序表初识(vector)

前言

std是一个容器和算法相关的库,顺序表作为一个常见的容器也在标准库中有相应的实现--vector。今天我们就来简单的认识一下vector的使用,并且简单的模拟实现一个我们的vector

具体vector类的描述可以参考vector - C++ Reference (cplusplus.com)

在不同的编译器下string类的实现各有差异,这里我们使用的是Microsoft Visual Studio Community 2022 (64 位) - Current 版本 17.8.5

构造函数

介绍

了解一个类,首先我们要先了解它的构造函数

c++顺序表初识(vector)_第1张图片

声明一下,allocator_type是std六大组件之一的空间配置器,目前我们可以直接将其忽略。

首先vector提供了一个默认的构造函数--default(1),我们简单的写一段程序来观察一下

c++顺序表初识(vector)_第2张图片

此时我们首先可以得到一个结论,那就是默认的构造函数会将属性赋值为0(这里泛指各种0,例如数字0,0.0亦或是指针nullptr)。其次我们发现标准库使用的是三个指针来维护这个顺序表的。

我们合理猜测_Myfirst标识这块空间的开始位置,_Mylast标识这块空间最后一个有效元素的下一个位置,_Myend标识这块空间结束的下一个位置。

这样_Mylast-_Myfirst就能得到size(有效元素个数),_Myend_Myfirst就能得到capacity(有效元素个数)

第二个构造函数可以一次性初始化n个元素,这个元素可以指定,不指定则使用默认值(这里会调用元素类型的默认构造)

第三个构造函数可以使用迭代器区间(左开右闭)来初始化,这可以和其他容器适配(因为容器是std六大组件之一,std的使用容器都支持迭代器)

第四个就是一个拷贝构造,这里还是要注意深浅拷贝的问题

实现

首先vector应该可以存储各种各样的数据类型,我们也实现一个类模板来适配各种数据类型,我们也学习标准库里用三个指针维护顺序表的方式实现

namespace zzzyh {
	template
	class vector
	{
	public:
        typedef T* iterator;
    private:
	    iterator _start = nullptr;//开始位置
	    iterator _finish = nullptr;//有效元素的下一个位置
	    iterator _end_of_storage = nullptr;//结束位置的下一个位置
    };
}

这里T类型就是要存储的数据类型,我们也发现顺序表这种结构下的指针就是天然的迭代器,我们定义T*为迭代器(当然标准库的实现要比我们这复杂很多,包括但不限于各种检查,我们这里就实现它基础的功能)

有了类的框架,我们就可以来实现他的构造函数

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

默认构造很简单,直接全部赋为空值即可

vector(int size,const T& x = T())
{
	T* tmp = new T[size];
	_start = _finish = tmp;
	_end_of_storage = tmp + size;
	for (size_t i = 0; i < size; i++)
	{
		push_back(x);
	}
}

这里我们可以初始化n个元素,并且指定初始化的值。这里我们复用了push_back方法,它的作用是尾插一个指定元素,但它的实现还是略微有点复杂,还牵扯到扩容(reserve)的逻辑,这两块内容我们在后面展开,这里先都直接使用了

我们再来实现一个拷贝构造

vector(const vector& tmp)
{
	size_t size = tmp.size();
	reserve(size);
	for (auto& x : tmp) {
		push_back(x);
	}
}

这里还是要注意深浅拷贝的问题,不能简答的理解为赋值操作,而是要开辟一块独立的空间出来

最后我们要来实现一个迭代器区间的构造函数

template 
vector(InputIterator first, InputIterator last)
{
	while (first != last) {
		push_back(*first);
		first++;
	}
}

在类模板中我们依然可以使用函数模板,此时使用函数模板就可以实现接收各种各样的迭代器

注意,while条件里最好还是使用!=号,因为某些容器在内存上是不连续的(例如链表)

析构函数

介绍

显然,这个类是有资源需要释放的,那么使用我们默认生成的析构函数是有内存泄露的风险的,需要我们再自己实现一份

实现

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

这里如果_start是nullptr不会做任何处理,我们就不检查了

operator=

operator=也是默认生成的函数,但我们在拷贝构造的部分就讲了,vector是存在深浅拷贝的问题就的,所以我们同样还需要手动实现operator=

//template 类的内部不需要
vector& operator=(const vector& tmp) {
	if (this == &tmp) {
		return *this;
	}
	vector v(tmp);
	swap(v);
	return *this;
}

迭代器

这里的迭代器用法和功能上和我们再string里讲的完全相同,我们就挑前四个简单实现一下

c++顺序表初识(vector)_第3张图片

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

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

 下面两组关于逆遍历的迭代器我们就先不展开了

容量相关操作

对于一个容器,容量相关的操作尤为重要,下面我们边介绍边实现一些常用的容量操作函数

size

得到数据的个数

size_t size() const 
{
	return _finish - _start;
}

capacity

得到最大容量

size_t capacity() const
{
	return _end_of_storage - _start;
}

empty

判断容器是否为空,即有没有有效元素,空返回true,否则返回false

bool empty() {
	return _finish == _start;
}

reserve

reserve可以将容量增加到不小于指定大小。如果指定大小小于等于当前容量不做任何处理,大于当前容量会将容量增加到不小于指定大小,具体多大取决于编译器的实现,只保证不会影响数据

void reserve(size_t n) {
			size_t sz = size();
			size_t oldCapacity = capacity();
			if (n <= oldCapacity) {
				return;
			}
			int newCapacity = n > oldCapacity*2 ? n : oldCapacity * 2;
			T* rem = new T[newCapacity];
			if (sz != 0) {
				//memmove(rem, _start, sizeof(T) * sz);
				for (size_t i = 0; i < sz; i++)
				{
					rem[i] = _start[i];
				}
				//delete[] _start;
			}
			delete[] _start;
			_start = rem;
			_finish = rem + sz;
			_end_of_storage = rem + newCapacity;
}

reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代
价缺陷问题

resize

resize则会影响数据。如果指定大小<原大小,会将数据减少至指定大小;如果指定大小==原大小数据层面可以理解为不做处理,如果指定大小>原大小则会扩容至指定大小,可以指定扩容使用的值,不指定依然使用默认值。resize在开空间的同时还会进行初始化,影响size

void resize(size_t n,const T& x = T()) {
			if (n == size()) {
			}
			else if (n < size()) {
				_finish = _start + n;
			}
			else {
				reserve(n);
				int i = n-size() ;
				while (i > 0)
				{
					this->push_back(x);
					i--;
				}
			}
			return;
}

增删改查操作

push_back

这可以尾插一个指定的元素,前面我们多次使用它,现在我们就来简单的实现一下

void push_back(const T& data) {
			if (_finish == _end_of_storage) {
				reserve(capacity() == 0 ? 1 : capacity() * 2);
			}
			new(_finish)T(data);//这里和下面注释的代码起到相同的效果,不过这里是拷贝构造
			//*_finish = data; 这里是赋值重载
			_finish++;
}

pop_back

尾删一个元素,并且弹出元素的值(标准库中未弹出,没有返回值,我们这里实现一个弹出版本的)

T pop_back() {
	if (!empty()) {
		T ret = *(_finish - 1);
		_finish--;
		return ret;
	}
}

find

查询第一个符合指定的元素,如果查询到返回此元素的迭代器,否则返回有效元素的下一个位置

注意,这是std算法模块提供的函数,所以我们在vector中不提供具体实现,只简单介绍其用法

c++顺序表初识(vector)_第4张图片

insert

在pos位置之前插入一个指定的元素

iterator insert(iterator pos, const T& data) {
	assert(pos >= _start && pos <= _finish);
	if (pos == _finish) {
		push_back(data);
		return pos+1;
	}
	if (_finish == _end_of_storage) {
		int i = pos - _start;
		reserve(capacity() == 0 ? 1 : capacity() * 2);
		pos = _start + i;
	}
	iterator end = this->end()-1;
	while (end >= pos) {
		//memmove(end + 1, end, sizeof(T));
		*(end + 1) = *(end);
		end--;
	}
	new(pos)T(data);
	_finish++;
	return pos+1;
}

clear

清除所有元素

void clear() {
			_finish = _start;
		}

 operator[]

得到指定下标的元素

		T& operator[](int i) {
			return *(_start + i);
		}

		const T& operator[](int i) const 
		{
			return *(_start + i);
		}

swap

交换两个容器的内容

void swap(vector& tmp) {
			std::swap(this->_start, tmp._start);
			std::swap(this->_finish, tmp._finish);
			std::swap(this->_end_of_storage, tmp._end_of_storage);
		}

erase

删除指定位置的元素

iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);
			if (pos == _finish - 1) {
				pop_back();
				return pos-1;
			}
			//delete (this[pos - _start]);
			//int i = pos - _start;
			//memmove(pos, pos + 1, sizeof(T) * (size() - 1 - (pos - _start)));
			iterator left = pos;
			while (left!=_finish-1) {
				*left = *(left + 1);
				left++;
			}
			//pos = _start+i;
			_finish--;
			return pos;
		}

迭代器失效

迭代器在vector中的本质就是指针,在我们进行容量相关的操作的时候,不可避免的会对元素存储位置进行修改(例如扩容时会释放旧空间,开辟新空间),如果我们在使用未更新的迭代器会产生类似于野指针的问题,这就是迭代器失效

迭代器失效的原因:

  • 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、
    assign、push_back等
  • 指定位置元素的删除操作--erase

等……

注意,vs对迭代器的检查是较为严格的,使用失效的迭代器程序就直接崩溃,但在Linux中gcc编译下检查的不严格不一定会崩溃。但是失效的迭代器我们认为使用是有风险的,建议更新迭代器后再使用,这也是为什么前面我们在实现容量相关函数的时候,返回值是一个迭代器,就是为了解决迭代器失效的问题

string的迭代器也同样存在这样的问题,不过string的迭代器与下标相比并不常用,所以没在string中强调迭代器失效的问题

迭代器失效的解决办法就是及时更新迭代器

memmove/memcpy在移动类时的问题

memmove/memcpy只是单纯的复制拷贝,并不会开辟额外的空间

如果我们在vector中存储类,例如string,在扩容等容量操作时进行单纯的复制拷贝,此时进行的是浅拷贝,会导致新旧两块空间指向同一块内存,此时我们再释放旧空间会导致新空间跟着一起被释放

所以我们在实现这些容量相关的操作时建议使用深拷贝

结语

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!

你可能感兴趣的:(c++,开发语言)