C++的Vector学习:从功能探索到底层实现

前言

在string的学习过程中,我们不难发现由于String比STL更早实现,而后再重新对String进行编写后,String的很多接口存在笼余问题,但是我们还是凭此对STL有了一个系统的认识,接下来我们将开始第二个标准库------Vector的学习。

目录

前言

一、Vector的介绍

二、Vector的使用

[2.1 vector的定义](#2.1 vector的定义)

[2.2 迭代器的使用](#2.2 迭代器的使用)

[2.3 空间增长变化问题](#2.3 空间增长变化问题)

[2.4 增删改查操作](#2.4 增删改查操作)

[2.5 迭代器失效问题](#2.5 迭代器失效问题)

[2.6 二维数组的实现](#2.6 二维数组的实现)

三、vector源码中的成员变量

四、函数成员的补充

[4.1 短小简单函数实现](#4.1 短小简单函数实现)

不带参的构造函数

begin、end

capacity、size、empty

[[] 运算符重载](#[] 运算符重载)

[4.2 reserve、push_back以及pop_back的实现](#4.2 reserve、push_back以及pop_back的实现)

[4.3 resize的实现:](#4.3 resize的实现:)

[4.4 erase、insert的实现](#4.4 erase、insert的实现)

[4.5 构造函数与析构函数](#4.5 构造函数与析构函数)


一、Vector的介绍

Vector翻译成中文为向量,那我们学习过数据结构后可以把底层看成顺序表,实际的源代码有不同但是其实底层看成顺序表是没问题的。

cpp 复制代码
private:
	int* _arr;
	int _size;
	int _capacity;

vector中存储的不一定就是int类型的,也可以是其他类型。这里要说明与string类不同,string类是类模板实例化后的结果,而vector还只是类模板,需要进行显示实例化成具体的类才能使用。

那我们依旧会根据文档进行功能探索向量 - C++ 参考

二、Vector的使用

我们来对一些常用的接口进行探索。

2.1 vector的定义

构造函数

代码展示:

cpp 复制代码
void test_vector1()
{
	vector<int> v1;
	vector<int> v2(10, 1);
	vector<int> v3(v2.begin()+2, v2.end()-1);
	vector<int> v4(v3);
}

结果展示:

2.2 迭代器的使用

所有标准库的迭代器用法是相同的,我们可以利用迭代器进行遍历vector实例化后的对象。

cpp 复制代码
void test_vector2()
{
	vector<int> v1(10,1);
	vector<int>::iterator it = v1.begin();
	int i = 0;
	while (it != v1.end())
	{
		*it = i++;
		cout << *it << ' ';
		it++;
	}
}

我们前面提过范围for的概念,其底层是迭代器,所以在这里同样适用。

2.3 空间增长变化问题

size()可以得到对象的有效元素的额个数,capacity()可以得到对象的空间大小。

**reserve()**的功能与string中的类似,都是当n大于实际空间时进行扩容,n小于实际空间时不会缩容,意思reserve只会进行扩容的操作。

我们设计一个代码来观察一下在vs编译器下,reserve的扩容机制:

cpp 复制代码
void test_vector3()
{
	vector<int> v1;
	cout << v1.capacity() << endl;
	for (int i = 0;i < 100;i++)
	{
		v1.push_back(i);
		if (v1.size() == v1.capacity())
		{
			cout << v1.capacity() << endl;
		}
	}
}

我们发现是1.5倍的扩容,向上取整,而在linux中g++是二倍扩容。

resize

我们看文档可以发现,在vector中如果n<size(),那么会缩小size,如果大于size(),小于capacity(),那么就会增加size()为n,补充的元素内容为val;如果n>capacity(),那么就会进行扩容并补充对应的元素。

cpp 复制代码
void test_vector4()
{
	//在vs编译器中reserve以及resize的逻辑都是大的扩容
	vector<int> v1(10,1);
	cout << v1.capacity() << endl;
	v1.reserve(100);//扩容
	cout << v1.capacity() << endl;
	v1.shrink_to_fit();
	//根据size对capacity进行调整
	cout << v1.size() << ' ' << v1.capacity() << endl;

	vector<int> v2(10, 1);
	cout << v2.capacity() << endl;
	v2.reserve(5);//不扩容
	cout << v2.capacity() << endl;

	vector<int> v3(10, 1);
	cout << v3.capacity() << endl;
	v3.resize(100);//扩容
	cout << v3.capacity() << endl;

	vector<int> v4(10, 1);
	cout << v4.capacity() << endl;
	v4.resize(5);//不扩容,但是改变size
	cout << v4.capacity() << endl;
	cout << v4.size() << endl;
}

capacity的代码在vs和g++下分别运行会发现,vscapacity是按1.5****倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。

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

resize在开空间的同时还会进行初始化,影响size。

2.4 增删改查操作

push_back以及pop_back我们应该已经很熟悉了,尾插尾删。

cpp 复制代码
void test_vector6()
{
	vector<int> v;
	for (int i = 0;i < 5;i++)
	{
		v.push_back(i);
	}
	for (auto it : v)
	{
		cout << it << ' ';
	}
	cout << endl;
	v.pop_back();
	for (auto it : v)
	{
		cout << it << ' ';
	}
}

insert在指定位置之前插入数据,我们看看相关参数:

我们发现函数参数中使用迭代器来进行元素的的定位,当然也很方便,我们只需要对begin()以及end()进行加减就可以改变位置,我们调用时很方便,但是但给我们尝试进行模拟实现时,可能会引发迭代器失效的问题,针对这个问题我们后面再进行解释。

代码展示:

cpp 复制代码
void test_vector7()
{
	vector<int> v;
	for (int i = 0;i < 5;i++)
	{
		v.push_back(i);
	}
	auto it = v.begin();
	v.insert(it + 2, 5);
	for (auto it : v)
	{
		cout << it << ' ';
	}
	cout << endl;
	v.insert(it + 2, 4, 8);
	for (auto it : v)
	{
		cout << it << ' ';
	}
	cout << endl;
}

erase的介绍

一样的,由于移动多个数据的位置,所以会导致效率比较低,函数中的参数同样也是迭代器

代码功能展示:

cpp 复制代码
void test_vector8()
{
	vector<int> v;
	for (int i = 0;i < 10;i++)
	{
		v.push_back(i);
	}
	auto it = v.begin();
	v.erase(it + 9);
	for (auto it : v)
	{
		cout << it << ' ';
	}
	cout << endl;
	v.erase(it + 2, it + 6);
	for (auto it : v)
	{
		cout << it << ' ';
	}
	v.clear();
}

访问操作

功能介绍其实我们学习过strin后就知道了,我们直接给出功能展示:

cpp 复制代码
void test_vector5()
{
	vector<int> v;
	for (int i = 0;i < 10;i++)
	{
		v.push_back(i);
	}
	cout << v[0] << ' ' << v.at(9) << endl;
	cout << v.front() << ' ' << v.back() << endl;
	int* start = v.data();//返回向量内部用于存储其自有元素的内存数组的直接指针
	cout << *start+5;
}

2.5 迭代器失效问题

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对 指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器 底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即 如果继续使用已经失效的迭代器,程序可能会崩溃)。 对于vector可能会导致其迭代器失效的操作有: 会引起其底层空间改变的操作,都有可能是迭代器失效......后面很多情况我们在底层实现时会提到。

2.6 二维数组的实现

cpp 复制代码
//展示一下二维数组
void test_vector9()
{
	/*vector<int> v1(5, 1);
	vector<vector<int>> v2(4, v1);
	for (int i = 0;i < 4;i++)
	{
		for (int j = 0;j < 5;j++)
		{
			cout << v2[i][j] << ' ';
		}
		cout << endl;
	}*/

	vector<int> v(5, 7);
	vector <vector<int>> vv(7, v);
	for (int i = 0;i < vv.size();i++)
	{
		for (int j = 0;j < vv[i].size();j++)//可能每行的列数不同
		{
			cout << vv[i][j] << ' ';
		}
		cout << endl;
	}
}

因为vector内的类型不一定是内置类型,也可以是一维数组,我们都知道二维数组其实是元素为一维数组的一维数组,抽象地看就是:

那么到这里我们就基本将vector的几个接口功能看完了,下面我们就开始进行底层的实现。

三、vector源码中的成员变量

在上面我们说,可以将vector看成顺序表,我们知道顺序表的成员变量是下面这样的:

cpp 复制代码
private:
	T* _val;
	int _size;
	int _capacity;

但是我们查看vector的源代码发现好像不是这样的,源代码中的成员变量是:

cpp 复制代码
private:
	//我们观察vector的源码发现
	//其底层是由三个迭代器实现的,但其实我们还是可以将其看成顺序表
	iterator _start;//指向第一个
	iterator _finish;//指向最后一个有效元素的下一位
	iterator _end_of_storage;//指向容器中的最后一个有效空间
	//T* _arr;
	//int _size;
	//int _capacity;
};

但是其实我们对比发现,只是将顺序表中的size以及capacity换了一种形式进行表示。

我们给出基本的框架,再对框架进行完善:

cpp 复制代码
#include<iostream>
namespace mine
{
	template<class T>//vector本质上是类模板
	class vector
	{
	public:
		typedef iterator T*;
		typedef const_iterator const T*;

	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

我们在介绍的过程中说过,vecotor为类模板,类模板不支持声明与定义分离在不同的文件中,否则会发生链接错误。而后我们将在此基础上进行常用功能的补充。

四、函数成员的补充

4.1 短小简单函数实现

我们说过,类中的函数时默认内联的,那么我们先实现短小简单的函数

不带参的构造函数
cpp 复制代码
vector()
	:_start(nullptr),
	_finish(nullptr),
	_end_of_storage(nullptr)
{}
begin、end
cpp 复制代码
iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
const_iterator begin() const
{
	return _start;
}
const_iterator end() const
{
	return _finish;
}
capacity、size、empty
cpp 复制代码
size_t size() const
{
	return _finish - _start;
}
size_t capacity() const
{
	return _end_of_storage - _start;
}
bool empty() const
{
	return size() == 0;
}
[] 运算符重载
cpp 复制代码
T& operator[](size_t n)
{
	assert(n < size());
	return _start[n];
}
const T& operator[](size_t n) const
{
	assert(n < size());
	return _start[n];
}

4.2 reserve、push_back以及pop_back的实现

我们先实现reserve,这在我们后面的代码中很常用。

我们再明确一下reserve的功能:当其中的参数n大于我们已有的空间时,才会进行扩容其他场景不予操作,好,那么我们开始写:

cpp 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		memcpy(tmp, _start, size()*sizeof(T));
		delete[] _start;
		_start = tmp;
		_finish = tmp + size();
		_end_of_storage = _start + n;
	}
}

嗯,好像没什么问题,那我们再写一个push_back然后测试看看:

cpp 复制代码
void push_back(const T& val)//防止当T为非内置类型时,进行多次拷贝所以采用引用
{
	if (size() == capacity())
	{
		reserve(capacity() == 0 ? 4 : 2 * capacity());
	}
	*_finish++ = val;
}

我们经过调试发现,_finish有问题,初始化列表没问题那么就是reserve出现了问题。

cpp 复制代码
_finish = tmp + size();

reserve中这个代码,当我们调用其中的size时,发现_start已经发生了改变了,指向的是tmp,那么这里肯定就会发生问题,指向的都不是同一个对象,那么_finish的结果就未知了,我们要解决这个问题就需要将原本的大小保存起来:

cpp 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t old_size = size();
		memcpy(tmp, _start, size()*sizeof(T));
		
		delete[] _start;
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = _start + n;
	}
}

调试后,我们发现可以正常运行,但是我们我们测试的是int型的,如果为string类型时,会引发问题。

问题分析:

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。

  2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅 拷贝。

所以我们需要设计为:

cpp 复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t old_size = size();
		for (int i = 0;i < old_size;i++)
		{
			tmp[i] = _start[i];//这里可以调用对应类型的赋值重载
		}
		delete[] _start;
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = _start + n;
	}
}

pop_back代码:

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

4.3 resize的实现:

cpp 复制代码
void resize(size_t n, T val = T())
{
	if (n <size())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);
		while (_finish < _end_of_storage)
		{
			*(_finish++) = val;
		}
	}
}

4.4 erase、insert的实现

cpp 复制代码
iterator insert(iterator pos, const T& val)
	//这里的pos指向的是原对象,如果进行了扩容,那么就会产生错误
	//所以我们需要存储pos位置在顺序表中的相对位置
{
	assert(pos >= _start&& pos <= _finish);
	if (size() == capacity())
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : 2 * capacity());
		pos = _start + len;
	}
	iterator end = _finish-1;//_finish指向的最后一个元素的最后一个字节
	while (end >= pos)
	{
		*(end+1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
	return pos;
}
iterator erase(iterator pos)
{
	assert(pos <= _finish && pos >= _start);
	iterator it = pos;
	while (it != _finish-1)
	{
		*it = *(it + 1);
		it++;
	}
	_finish--;
	return pos;
}

4.5 构造函数与析构函数

cpp 复制代码
vector(const vector<T>& v)
{
	reserve(v.size());
	for (auto& it : v)//使用引用是为了防止多次拷贝
	{
		push_back(it);
	}
}
//可以使用其他类型的迭代器,将数据放入vector中
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	InputIterator in = first;
	while (in != last)
	{
		push_back(*in);
		in++;
	}
}
vector(size_t n, const T& val = T())
{
	for (size_t i = 0;i < n;i++)
	{
		push_back(val);
	}
}
vector(int n, const T& val = T())
{
	for (size_t i = 0;i < n;i++)
	{
		push_back(val);
	}
}
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}
//v1=v2
vector<T>& operator=(const vector<T>& v)
{
	if (this != &v)
	{
		vector<T> tmp(v);   // 拷贝构造
		swap(tmp);          // 交换内部指针
	}
	return *this;
}
~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

其实很多代码都是很容易实=实现的,测试功能时有问题自己就多进行调试。

相关推荐
她说彩礼65万1 小时前
C语言 动态内存管理
c语言·开发语言·算法
傻啦嘿哟1 小时前
管好PPT的“骨架”:用Python控制页面与文档属性
开发语言·javascript·c#
凤凰院凶涛QAQ1 小时前
《C++转java快速入手系列》类与对象篇
java·开发语言·c++
时空系1 小时前
第8篇:模板与实例——面向对象编程入门(上)python中文编程
开发语言·python
故事还在继续吗1 小时前
常见的导致 coredump 的原因
开发语言·gdb
咸甜适中1 小时前
rust格式化输出(println!、format!、...)
开发语言·rust
CQU_JIAKE1 小时前
【a]4.25
开发语言
张健11564096481 小时前
std::ranges、std::views和懒加载
开发语言·c++
gCode Teacher 格码致知1 小时前
Javascript提高:一个彩色小球在画布边界内反弹并留下渐变轨迹-由Deepseek产生
开发语言·javascript