实用指南:【C++初阶】vector容器的模拟实现,各接口讲解

1.vector的介绍

使用STL的三个境界:能用,明理,能扩展 ,那么下面学习vector,我们也是按照这个方法去学习,以下是vector的官方文档

2.接口模拟实现总览

为了避免命名冲突,我们还是将实现的vector封装到单独的命名空间里

#pragma once
#include
#include
#include
namespace gjy
{
	template//这是一个类模板类内部所有用到的 T(如成员变量类型、函数参数 / 返回值类型)都依赖于这个模板参数,随 vector 的实例化类型而确定。
	class vector
	{
	public:
		//—————————迭代器—————————
		/*using iterator = T*;
		using const_iterator = const T*;*///这里为什么这样写,而不是直接在第一个迭代器前面加const?
		//是因为直接在前面加const是让指针本身不能修改,不是指针指向的内容
		typedef T* iterator;
		// 指向数据不能修改,本身可以修改
		typedef const T* const_iterator;
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}
//——————————1、默认成员函数——————————
	//1.1.构造函数
	//无参构造函数,使用缺省值来初始化列表
		vector()
		{}
	//迭代器区间构造函数
		template//模板函数,这里的class不是类的意义,也可以用typename
		vector(Inputerator first, Inputerator last)//使用缺省值初始化列表
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
	//有参构造(n个值为val的容器)
		vector(size_t n,const T&val)//这里的 T 就是最外层类模板的参数 T,代表当前 vector 存储的元素类型。
		{
			reserve(n);//调用reserve函数将容器容量设置为n
			for (size_t i = 0;i < n;i++)
			{
				push_back(val);//利用尾插函数插入数据
			}
		}
		//下面两个构造是跟微信那个问题有关
		vector(long n, const T& val)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reserve(n); //调用reserve函数将容器容量设置为n
			for (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
			{
				push_back(val);
			}
		}
		vector(int n, const T& val)
			:_start(nullptr)
			, _finish(nullptr)
			, _end_of_storage(nullptr)
		{
			reserve(n); //调用reserve函数将容器容量设置为n
			for (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
			{
				push_back(val);
			}
		}
		//拷贝构造
		//传统写法
		//vector(const vector& v)//后面不显式写初始化列表,编译器会用缺省值写
		//{
		//	_start = new T[v.capacity()];//开辟一块和容器V一样大小的空间
		//		for (size_t i = 0;i < v.size();i++)//左闭右开就是数据个数
		//		{
		//			_start[i] = v[i];
		//		}
		//	_finish = _start + v.size();//更新有效数据的尾
		//	_end_of_storage = _start + v.capacity();
		//}
		 //现代写法:拷贝并交换(Copy-and-Swap)
		vector(const vector& v)
		{
			// 1. 利用迭代器区间构造函数,创建临时对象tmp(拷贝v的所有元素)
			vector tmp(v.begin(), v.end());  // 复用已有构造函数,无需重复写拷贝逻辑
			// 2. 交换当前对象和临时对象的资源
			swap(tmp);  // 需实现swap成员函数
		}
		/*vector(const vector& v)
		{
			reserve(v.capacity());
			for (auto e : v)
			{
				push_back(e);
			}
		}*/
		~vector()
		{
			delete[] _start;
			_start = _finish = _end_of_storage = nullptr;
		}
		//赋值运算符重载
		//传统写法
		//vector& operator=(const vector& v)
		//{
		//	if (this != &v)//防止自赋值
		//	{
		//		delete[]_start;//释放原空间
		//		_start = _finish = _end_of_storage = nullptr;
		//		reserve(v.size());//复用reserve开和v一样的空间
		//		for (auto& e : v)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}
		//现代写法
		vector& operator=(vector v)
		{
			if (this !=& v)
			{
			swap(v);
			}
			return *this;
		}
		//—————————2.访问操作—————————
		T& operator[](size_t n)
		{
			assert(n < size());
			return *(_start + n);
		}
		const T& operator[](size_t n) const
		{
			assert(n < size());
			return *(_start + n);
		}
		//—————————3.容量操作—————————
		size_t size()const
		{
			return _finish - _start;
		}
		size_t capacity()const
		{
			return _end_of_storage - _start;
		}
		void reserve(size_t n)
		{
			if (capacity() < n)//判断是否需要扩容,reserve不对内容修改
			{
				size_t Old_size = size(); //记录当前容器当中有效数据的个数
				T* tmp = new T[n]; //开辟一块可以容纳n个数据的空间
				if (_start) //判断是否为空容器
				{
					for (size_t i = 0; i < Old_size; i++) //将容器当中的数据一个个拷贝到tmp当中
					{
						tmp[i] = _start[i];
					}
					delete[] _start; //将容器本身存储数据的空间释放
				}
				_start = tmp; //将tmp所维护的数据交给_start进行维护
				_finish = _start + Old_size; //容器有效数据的尾
				_end_of_storage = _start + n; //整个容器的尾
			}
		}
		void resize(size_t n, const T& val = T())
		{
			if (n < size())
			{
				_finish = _start + n;//直接覆盖掉后面的数据
			}
			else
			{
				reserve(n);
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}
		bool empty()
		{
			return(_start == _finish);
			//或者return!capacity()
		}
		//—————————4.修改操作—————————
		// 实现swap成员函数(交换两个vector的资源)
		void swap(vector& other)
		{
			// 交换三个指针即可(O(1)操作,无内存分配)
			std::swap(_start, other._start);
			std::swap(_finish, other._finish);
			std::swap(_end_of_storage, other._end_of_storage);
		}
		void push_back(const T& val)
		{
			if (_finish == _end_of_storage)
			{
				size_t newcapcity = capacity() == 0 ? 4 : 2 * capacity();//将容量扩大为二倍capacity
				reserve(newcapcity);//开辟新空间大小
			}
			*_finish = val;
			_finish++;
		}
		void pop_back()
		{
			assert(capacity());
			_finish--;
		}
		//insert迭代器失效问题
		void insert(iterator pos,const T& val)
		{
			assert(pos >= _start && pos <= _finish);
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;//避免后面的迭代器失效
				reserve(capacity() == 0 ? 4 : 2 * capacity());//reserve过后原来的start去新的地方了
				pos = _start + len;//这里是更新后的start
			}
			iterator it = _finish - 1;//最后一个数据位置
			while (it >= pos)
			{
				*(it + 1) = *it;//往后挪
				it--;
			}
			*pos = val;
			++_finish;
		}
		iterator erase(iterator pos)//这里不是void而是返回值,是因为vs检查机制很严格,只要删除过后,后面的迭代器都会失效,所以返回删除位置下一个元素的迭代器(其实--过后还是pos位置)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator it = pos + 1;//记录后一个元素
			while (it < _finish)
			{
				*(it - 1) = *it;//往前面挪
				++it;
			}
			--_finish;
			return pos;
		}
	private:
		iterator _start=nullptr;
		iterator _finish=nullptr;
		iterator _end_of_storage=nullptr;
	};
}

类内部所有用到的 T(如成员变量类型、函数参数 / 返回值类型)都依赖于这个模板参数,随 vector 的实例化类型而确定。vector<int>就是存放int类型的vector容器,甚至还可以vector<vector<int>>创建一个二维数组,并且相对于C语言的二维数据,封装了各种接口可以使用

3、vector的成员变量和默认成员函数

3.1.成员变量介绍

我们先来讲讲vector的成员变量有哪些:_start、_finish、_end_of_storage
vector容器其实就是我们数据结构中学过的顺序表结构,实现这一结构的成员变量如下图

_start指向第一个元素,_finish指向结束的下一个位置,_end_of_storage指向整个容器的结尾。

3.2.构造函数

构造函数为什么还要写一个无参数的,跟后面现代写法swap函数传形式参数那里有关,一会再说有关是因为只要有构造函数,

1.无参构造函数

//无参构造函数
//没有显式写初始化列表就用缺省值
vector()
{
}

C++ 标准规定:如果用户显式定义了任何构造函数(如拷贝构造、赋值运算符等),编译器将不再自动生成默认构造函数(无参构造函数)

如果不手动定义默认构造函数 vector() {},编译器不会生成默认构造函数。此时,当需要创建无参的 vector 对象时(如 bit::vector<int> v;),会因找不到默认构造函数而编译报错。

 确保成员变量的缺省值正常生效

为成员变量设置了缺省值:

private:
    iterator _start = nullptr;
    iterator _finish = nullptr;
    iterator _end_of_storage = nullptr;

这些缺省值需要在对象初始化时生效。对于默认构造函数:

  • 如果显式定义了 vector() {},编译器会在该构造函数的初始化阶段,使用成员变量的缺省值对 _start_finish_end_of_storage 进行初始化(本质是编译器为默认构造函数隐式补充了初始化列表)。
  • 如果没有显式定义默认构造函数,且编译器也没有生成(因存在其他构造函数),则无法通过无参方式创建对象,成员变量的缺省值也就无从谈起。

2.有参构造函数(利用迭代器)

/迭代器区间构造函数
		template//模板函数,这里的class不是类的意义,也可以用typename
		vector(Inputerator first, Inputerator last)//使用缺省值初始化列表
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

这里的迭代器使用了模板参数,各种容器的迭代器都可以传参,适用于不同类型的迭代器

3.有参构造函数(n个val值的)

vector(size_t n,const T&val)//这里的 T 就是最外层类模板的参数 T,代表当前 vector 存储的元素类型。
{
	reserve(n);//调用reserve函数将容器容量设置为n
	for (size_t i = 0;i < n;i++)
	{
		push_back(val);//利用尾插函数插入数据
	}
}

该构造函数用于创建一个包含 n 个相同元素 val 的 vector。例如:vector<int> (5, 10):创建一个存储 int 的 vector,包含 5 个值为 10 的元素;vector<string> (3, "hello"):创建一个存储 string 的 vector,包含 3 个值为 "hello" 的元素。

注意:
  • 该构造函数明确知道需要存储n个数据,因此建议使用reserve函数一次性分配足够的空间。这样可以避免后续调用push_back时多次扩容,从而提高效率。
  • 该构造函数还需要实现两个重载版本,如下:
    vector(long n, const T& val)
    	:_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {
    	reserve(n); //调用reserve函数将容器容量设置为n
    	for (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
    	{
    		push_back(val);
    	}
    }
    vector(int n, const T& val)
    	:_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {
    	reserve(n); //调用reserve函数将容器容量设置为n
    	for (int i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
    	{
    		push_back(val);
    	}
    }

    这两个重载函数的区别在于参数n类型不同,如果不这样写的话,当我们传参如下

    vector(10,1)//本意是调用第三个构造函数的

    会发生如下图一样的问题,我们的编译器会默认选择更合适的函数,因为两个int,就会调用我们使用迭代器的那个构造函数,而int类型不能解引用,因此会报错

  • C++重载决议优先选择"最匹配"的函数,不需要类型转换的版本优先于需要类型转换的版本

  • 模板函数能精确匹配时,优先于需要隐式转换的非模板函数

解决方法

        1.提供合适的重载类型就是再写一个int类型的,以及long类型的构造函数

        2.使用强制类型转换传参,调用时可以使用vector<int>{10u,7},强制转换第一个参数为unsigned。    

3.3.拷贝构造

动态开辟了资源因此也需要深拷贝,关于vector的深拷贝构造函数,提供两种实现方式:

1.传统写法:

        先开辟与原容器一样大的空间,再逐个拷贝原容器中的数据,最后更新_finish和_end_of_storage指针即可

//传统写法
vector(const vector& v)//后面不显式写初始化列表,编译器会用缺省值写
{
	_start = new T[v.capacity()];//开辟一块和容器V一样大小的空间
		for (size_t i = 0;i < v.size();i++)//左闭右开就是数据个数
		{
			_start[i] = v[i];
		}
	_finish = _start + v.size();//更新有效数据的尾
	_end_of_storage = _start + v.capacity();
}

注意事项:

        当在逐个拷贝容器时候应该尽量避免使用memcpy函数,当vector存储的是内置类型或者无需深拷贝的自定义类型时候,还能使用,当需要重新分配资源时就不行,这个在后面reserve那里迭代器失效也有关系。如果拷贝的容器里面存放的是string类型

每一个string都指向自己所存储的字符串

        若使用memcpy函数进行拷贝构造,新构造的vector中每个string对象的成员变量将和vector中的string对象完全相同,这意味着两个vector中对应的string成员会指向相同的字符串存储空间。

那我们的代码是如何解决的呢?

for (size_t i = 0; i < v.size(); i++)
{
    start[i] = v[i];
}

这里的赋值实际上调用的是赋值运算符重载,这里面会重新分配资源

实际流程是当拷贝 vector<string> 时:

  1. 先为 vector 开辟新的内存空间(new T[v.capacity()],这里 T 是 string)。
  2. 对每个 string 元素,通过 string 的拷贝赋值运算符完成深拷贝。
  3. 最终得到的新 vector<string> 与原 vector 存储的 string 元素完全独立,修改其中一个的元素不会影响另一个。

简言之,拷贝 vector<string> 时,会间接调用 string 类的拷贝赋值运算符重载,保证字符串内容的深拷贝。

2.现代写法:

// 现代写法:拷贝并交换(Copy-and-Swap)
vector(const vector& v)
{
	// 1. 利用迭代器区间构造函数,创建临时对象tmp(拷贝v的所有元素)
	vector tmp(v.begin(), v.end());  // 复用已有构造函数,无需重复写拷贝逻辑
	// 2. 交换当前对象和临时对象的资源
	swap(tmp);  // 需实现swap成员函数
}
	// 实现swap成员函数(交换两个vector的资源)
	void swap(vector& other)
	{
		// 交换三个指针即可(O(1)操作,无内存分配)
		std::swap(_start, other._start);
		std::swap(_finish, other._finish);
		std::swap(_end_of_storage, other._end_of_storage);
	}

        利用构造函数构造一个临时对象,这个临时对象和要拷贝的对象拥有一样大的空间和数据,我们只需要交换this和这个临时对象的值和空间,出了作用域临时对象销毁顺带释放了this原来的空间。我们调试会发现传参那一步跳转到了拷贝构造函数,然后就出现了错误

注意:

        出现错误的原因是因为我们传值传参调用拷贝构造,拷贝构造函数中实参v3传递给形参v,将v,这里reserve是this调用的,因为要拷贝v3的那个对象没有初始化,没有构造那个对象,因此reserve,push_back这些接口使用就会报错,所以需要我们要么初始化列表中显示初始化,要么就给成员变量缺省值

vector(const vector& v)
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	//...
}

3.写法三.

        使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。

vector(const vector& v)
{
	reserve(v.capacity());
	for (auto e : v)
	{
		push_back(e);
	}
}

4.析构函数

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

析构函数直接删除数据即可,如果vector存放的是自定义类型会调用其内部的析构函数释放资源


5.赋值运算符重载

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

1.传统写法

vector& operator=(const vector& v)
{
	if (this != &v)//防止自赋值
	{
		delete[]_start;//释放原空间
		_start = _finish = _end_of_storage = nullptr;
		reserve(v.size());//复用reserve开和v一样的空间
		for (auto& e : v)
		{
			push_back(e);
		}
	}
	return *this;
}

复用reserve和尾插,将右值传给左值。

2.现代写法

 还是利用swap函数,传值传参调用拷贝构造生成临时对象,交换this和临时对象两个对象的值,出了作用域利用临时对象的销毁释放原空间,就不需要在手动delete[]了.

vector& operator=(const vector v)
{
	swap(v);
	return *this;
}


4、迭代器函数和访问元素操作

4.1.begin()和end()

依然分为const迭代器和普通迭代器,const迭代器不能直接在普通迭代器前面加const,那是是迭代器不能修改,而不是指向内容不能修改,应该在后面加const代表返回值是const类型

/*using iterator = T*;
using const_iterator = const T*;*///这里为什么这样写,而不是直接在第一个迭代器前面加const?
//是因为直接在前面加const是让指针本身不能修改,不是指针指向的内容
typedef T* iterator;
// 指向数据不能修改,本身可以修改
typedef const T* const_iterator;
iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
const_iterator begin() const
{
	return _start;
}
const_iterator end() const
{
	return _finish;
}

我们实现了迭代器,编译器自己就也实现范围for,可以用范围for对容器进行遍历

4.2.operator[]

 因为底层数据结构是数组,所以vector也支持我们使用“下标+[ ]”的方式对容器当中的数据进行访问,实现时直接返回对应位置的数据即可。我们需要重载两个形式,普通的和const类型的函数。

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

5、容量操作

5.1.size()和capacity()

我们知道两个指针相减返回的是相差的个数而不是空间大小,比如int*p1-int*p2返回的是(p1-p2)/sizeof(int*),因此可以利用指针相减的值返回size和capacity

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

5.2.reserve()

        void reserve(size_t n)
		{
			if (capacity() < n)//判断是否需要扩容,reserve不修改内容和size
			{
				size_t Old_size = size(); //记录当前容器当中有效数据的个数
				T* tmp = new T[n]; //开辟一块可以容纳n个数据的空间
				if (_start) //判断是否为空容器
				{
					for (size_t i = 0; i < Old_size; i++) //将容器当中的数据一个个拷贝到tmp当中
					{
						tmp[i] = _start[i];
					}
					delete[] _start; //将容器本身存储数据的空间释放
				}
				_start = tmp; //将tmp所维护的数据交给_start进行维护
				_finish = _start + Old_size; //容器有效数据的尾,加新的start
				_end_of_storage = _start + n; //整个容器的尾
			}
		}

执行扩容操作前需要先保存当前容器中的有效元素数量。 因为最终需要更新_finish指针的位置,而_finish的正确位置等于_start指针加上原始元素数量。如果在_start指针改变后再通过_finish - _start计算size,得到的结果将是无效的随机值。


5.3.resize() 

resize规则:

  1. 当n大于size时,将size扩展到n,新增元素初始化为val,如果没有提供val,则使用该类型的默认构造函数生成默认值。
  2. 当n小于size时,将size减到n。

先比较n和size的大小,当n小时,直接将_finish置到n的位置,当n比较大时,复用reserve函数,开辟空间,再进行赋值操作,不用操心是否扩容的问题了

void resize(size_t n,const T& val)
{
	if (n < size())
	{
		_finish = _start + n;//直接覆盖掉后面的数据
	}
	else
	{
		reserve(n);
		while (_finish != _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
}


5.4.empty()

就是简单的判空即可,比较start和finish的值即可,若返回位置相同即为空

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

6、修改操作

6.1.push_back()

void push_back(const T& val)
{
	if (_finish == _end_of_storage)
	{
		size_t newcapcity = capacity() == 0 ? 4 : 2 * capacity();//将容量扩大为二倍capacity
		reserve(newcapcity);//开辟新空间大小
	}
	*_finish = val;
	_finish++;
}

需要注意的点时检查是否已经满了,满了要扩容,这里我们用三目表达式来写

6.2.pop_back()

尾删之前需要检查是否为空vector,若空则做断言处理,若不为空只需要将_finish--即可

void pop_back()
{
	assert(!empty());//或者finish>start
	_finish--;
}

6.3.insert()有迭代器失效情况

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

//insert迭代器失效问题
void insert(iterator pos,const T& val)
{
	assert(pos >= _start && pos <= _finish);
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//避免后面的迭代器失效
		reserve(capacity() == 0 ? 4 : 2 * capacity());//reserve过后原来的start去新的地方了
		pos = _start + len;//这里是更新后的start
	}
	iterator it = _finish - 1;//最后一个数据位置
	while (it >= pos)
	{
		*(it + 1) = *it;//往后挪
		it--;
	}
	*pos = val;
	++_finish;
}

6.4.erase()有迭代器失效情况

		iterator erase(iterator pos)//这里不是void而是返回值,是因为vs检查机制很严格,只要删除过后,后面的迭代器都会失效,所以返回删除位置下一个元素的迭代器(其实--过后还是pos位置)
		{
			assert(pos >= _start);
			assert(pos < _finish);
            //assert(empty());前面断言了那两种就包含了这一个了
			iterator it = pos + 1;//记录后一个元素
			while (it < _finish)
			{
				*(it - 1) = *it;//往前面挪
				++it;
			}
			--_finish;
			return pos;
		}

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

6.5.swap()

调用标准库里面的swap函数

void swap(vector& other)
{
	// 交换三个指针即可(O(1)操作,无内存分配)
	std::swap(_start, other._start);
	std::swap(_finish, other._finish);
	std::swap(_end_of_storage, other._end_of_storage);
}

7.迭代器失效问题下一篇博客见

posted on 2026-01-26 22:29  ljbguanli  阅读(0)  评论(0)    收藏  举报