当前位置: 首页 > news >正文

vector模板类的模拟实现

目录

vector模板类结构介绍

vector 迭代器失效问题

1. 会引起其底层空间改变的操作,都有可能是迭代器失效

2. 指定位置元素的删除操作--erase

​编辑

3.注意:Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。

4.与vector类似,string在insert/erase/reserve后,迭代器也会失效

vector模板类的模拟实现

迭代器、operator[ ]

capacity、size、empty

push_back

1.模拟实现push_back遇到的问题

(1)问题一:vector 初始容量为 0 时的扩容问题

(2)问题二:插入数据时自定义类型的初始化问题

2.代码实现

pop_back

resize

1.内置类型的构造函数

2.模拟实现resize遇到的问题

3.代码实现

insert

1.insert关于迭代器失效问题

2.代码实现

erase

1.代码实现

2.erase关于迭代器失效问题

2.1.不同编译器下 erase 后迭代器使用的检查差异

2.2.erase 后迭代器失效的不同场景

string的insert、erase以后迭代器失效问题

1.insert 以后迭代器失效问题

2.erase 以后迭代器失效问题

构造函数

默认构造函数 vector()

带参数构造函数 vector(size_t n, const T& val = T())

1.错误代码

2.正确代码

迭代器区间构造函数 template vector(InputIterator first, InputIterator last)

1.注意事项

2.代码实现

3. 迭代器区间构造函数使用介绍

构造函数的优化

1.优化思路

2.优化后代码

3. 参数匹配问题导致编译错误

使用memcpy拷贝问题

拷贝构造:vector(const vector & v)

1.浅拷贝的危害

2.深拷贝拷贝构造函数

2.1.传统写法(正确代码)

2.2.常见传统方式的错误写法

2.3.现代写法(正确代码)

2.4.对比string和vector的拷贝构造函数

reserve

1.错误代码

2.正确代码

赋值重载:vector & operator=(vector v)

1.案例:利用杨辉三角来说明浅拷贝的赋值重载函数的危害

2.深拷贝赋值重载函数operator=的实现

(1)写法1:传值传参,调用拷贝构造函数完成传参

(2)写法2(建议使用):传引用传参,减少拷贝

std::sort函数模板使用介绍

vector模板类模拟实现的整个工程


vector模板类结构介绍

//模拟实现的vector模板类
template<class T>
class vector
{
public:
	typedef T* iterator;
private:
	iterator _start;//指向当前vector已分配内存空间的起始位置,即第一个元素的存储地址
	iterator _finish;//指向当前vector中已使用内存空间的末尾位置(最后一个有效元素的下一个位置)
	iterator _end_of_storage;//指向当前vector已分配内存空间的末尾位置,即所拥有的最大内存边界
};

注意事项:模拟实现的vector模板类内存分配策略

  • ①内存分配与释放:模拟实现的 vector模板类直接使用 new[] 和 delete[] 来进行内存的分配和释放。例如,在需要扩容时,会使用 new T[n] 来分配大小为 n 的 T 类型数组的内存空间,在释放内存时会使用 delete[] 来释放这块内存。注意:使用 new T[n] 分配内存时,对于内置类型 T,不会进行初始化;对于自定义类型 T ,会调用其默认构造函数进行初始化。
  • ②扩容策略:当 vector 的容量不足时,一般也会采用指数级扩容策略,将容量扩大为原来的 2 倍。在扩容过程中,会使用 new[] 分配新的内存空间,将原有的元素复制到新的内存空间中,最后使用 delete[] 释放原有的内存空间。
  • new T[n]的原理:调用operator new[]函数,在operator new[ ]中实际调用operator new函数完成 n 个对象空间的申请。在申请的空间上执行n次构造函数。
  • delete[]的原理:在释放的对象空间上执行 n 次析构函数,完成 n 个对象中资源的清理。调用operator delete[ ]释放空间,实际在operator delete[ ]中调用operator delete来释放空间。

vector 迭代器失效问题

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

1. 会引起其底层空间改变的操作,都有可能是迭代器失效

比如:resize、reserve、insert、assign、push_back等。

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v{ 1,2,3,4,5,6 };
	auto it = v.begin();

	//1.将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容
	//v.resize(100, 8);


	//2.reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容量改变
	//v.reserve(100);


	//3.插入元素期间,可能会引起扩容,而导致原空间被释放
	//v.insert(v.begin(), 0);
	//v.push_back(8);
	

	//4.给vector重新赋值,可能会引起底层容量改变
	//v.assign(100, 8);

	
	//出错原因:以上 1 ~ 4 操作分别单独取消注释,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释放掉,
	//而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。

	//解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新
	//赋值即可。即在使用while循环之前重新用v.begin()给it赋值 it = v.begin().

	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

测试:

解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新
赋值即可。
例如:在使用while循环打印之前重新用v.begin()给it赋值 it = v.begin()。

注意:这里我只演示操作1迭代器失效的解决方式,其他操作都是类似的解决方式。

2. 指定位置元素的删除操作--erase

(1)erase以后,迭代器失效的原因说明

在 C++ 的标准模板库(STL)中,std::vector 是一种动态数组,它的元素在内存中是连续存储的。当使用 erase 方法删除 vector 中的元素时,会导致迭代器失效,下面详细解释其原因。

①std::vector 的内存布局特性:std::vector 会在内存中分配一块连续的存储空间来存储其元素。例如,当你创建一个 vector<int> v = {1, 2, 3, 4}; 时,v 中的元素 1234 会依次存储在一段连续的内存区域中。这种连续存储的特性使得随机访问元素变得高效,因为可以通过简单的指针算术来计算元素的地址。

②erase 操作的实现原理:当调用 vector 的 erase 方法(例如 v.erase(it),其中 it 是指向要删除元素的迭代器)时,erase 会执行以下操作:

  • 元素移动:为了保持元素的连续性,erase 会将被删除元素之后的所有元素向前移动一个位置,以填补被删除元素留下的空缺。例如,若要删除 v 中的元素 2erase 会将 34 依次向前移动一个位置,覆盖掉原来 2 的位置。
  • 调整大小erase 会更新 vector 的大小信息,将其大小减 1。

③迭代器失效的原因:由于 erase 操作会移动元素和调整 vector 的大小,这会导致迭代器失效,具体表现为以下两种情况。

  • 被删除元素的迭代器失效:当调用 erase(it) 删除一个元素时,it 所指向的元素已经被移除,该迭代器不再指向任何有效的元素,因此它失效了。继续使用这个失效的迭代器会导致未定义行为,例如可能会访问到已经被覆盖的数据 或者 已经释放的内存(注:例如使用erase删除最后一个元素后,迭代器it就指向已经释放的内存(此时 it 变成野指针),若此时继续使用迭代器it访问则会越界访问从而造成程序发生崩溃)。
  • 被删除元素之后的所有迭代器失效:因为 erase 会将被删除元素之后的所有元素向前移动一个位置,所以原本指向这些元素的迭代器现在指向的是错误的元素。例如,在删除元素 2 后,原本指向 3 的迭代器现在指向了 4,而原本指向 4 的迭代器现在指向了一个无效的位置(可能是 vector 之外的内存)。因此,被删除元素之后的所有迭代器都失效了。

示例代码说明

#include <iostream>
#include <vector>
using namespace std;

int main() 
{
    vector<int> v = {1, 2, 3, 4};
    auto it = v.begin() + 1;  //指向元素 2

    //删除元素 2
    v.erase(it);

    //此时 it 以及 it 之后的迭代器都失效了
    //如果继续使用 it,会导致未定义行为
    //cout << *it << endl;  //错误:使用失效的迭代器

    return 0;
}

综上所述,erase 导致迭代器失效的根本原因是erase会改变 vector 中元素的存储位置和数量,从而使得迭代器指向的位置不再对应正确的元素。在使用 erase 时,需要特别注意迭代器的更新,以避免使用失效的迭代器。

(2)案例1

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	int a[] = { 1, 2, 3, 4 };
	vector<int> v(a, a + sizeof(a) / sizeof(int));

	//使用find查找3所在位置的iterator
	vector<int>::iterator pos = find(v.begin(), v.end(), 3);

	//删除pos位置的数据,导致pos迭代器失效。
	v.erase(pos);
	cout << *pos << endl; //此处会导致非法访问

	return 0;
}

解析:erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。

(3)案例2:以下代码的功能是删除vector中所有的偶数。

①错误写法

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	//定义一个整数类型的数组 v,并初始化为包含 1, 2, 3, 4 四个元素
	vector<int> v{ 1, 2, 3, 4 };

	//定义一个迭代器 it,初始化为指向 v 的第一个元素
	auto it = v.begin();

	//开始循环,只要 it 不指向 v 的末尾,就继续循环
	while (it != v.end())
	{
		//检查当前迭代器 it 所指向的元素是否为偶数
		if (*it % 2 == 0)
		{
			//如果是偶数,调用 erase 函数删除该元素
			//注意:这里存在问题,调用 erase 以后,it 及其之后的迭代器会失效
			v.erase(it);
		}

		//无论是否删除元素,都将迭代器 it 向后移动一位
		//当之前删除了元素导致 it 失效时,继续使用失效的 it 会引发未定义行为
		++it;
	}

	return 0;
}

问题原因

  • 迭代器失效

    • vector 的 erase 方法会删除指定位置的元素,并且会使指向被删除元素以及之后元素的所有迭代器失效。当调用 v.erase(it) 时,it 所指向的元素被删除,此时 it 就变成了一个无效的迭代器。
    • 在 erase 操作之后,代码接着执行 ++it,这会导致未定义行为,因为 it 已经失效,对其进行递增操作是不安全的。
  • 跳过元素

    • 即使不考虑迭代器失效的问题,erase 操作会使后续元素向前移动填补被删除元素的位置。如果删除了一个元素,下一个元素会移动到当前位置,而代码中直接对 it 进行递增操作( it++ ),会跳过这个移动过来的元素,从而导致部分偶数没有被检查到。

②正确写法

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	//定义一个整数类型的数组 v,并初始化为包含 1, 2, 3, 4 四个元素
	vector<int> v{ 1, 2, 3, 4 };

	//定义一个迭代器 it,初始化为指向 v 的第一个元素
	auto it = v.begin();

	//开始循环,只要 it 不指向 v 的末尾,就继续循环
	while (it != v.end())
	{
		//判断当前迭代器 it 所指向的元素是否为偶数
		if (*it % 2 == 0)
		{
			//如果是偶数,调用 erase 函数删除该元素
			//erase 函数会返回指向被删除元素之后的元素的迭代器
			//将返回的迭代器赋值给 it,更新 it 使其指向有效元素
			it = v.erase(it);
		}
		else
		{
			//如果不是偶数,将迭代器 it 向后移动一位,指向下一个元素
			++it;
		}
	}

	return 0;
}

3.注意:Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。

注:从下面三个案例中可以看到:SGI STL中,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果it不在begin和end范围内,肯定会崩溃的。

(1)案例1:reserve扩容

#include <iostream>
#include <vector>
using namespace std;

//1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
int main()
{
	vector<int> v{ 1,2,3,4,5 };

	for (size_t i = 0; i < v.size(); ++i)
		cout << v[i] << " ";
	cout << endl;

	auto it = v.begin();
	cout << "扩容之前,vector的容量为: " << v.capacity() << endl;

	//通过reserve将底层空间设置为100,目的是为了让vector的迭代器失效
	v.reserve(100);

	cout << "扩容之后,vector的容量为: " << v.capacity() << endl;

	//经过上述reserve之后,it迭代器肯定会失效,在vs下程序就直接崩溃了,但是linux下不会
	//虽然可能运行,但是输出的结果是不对的
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

//程序输出:
//1 2 3 4 5
//扩容之前,vector的容量为: 5
//扩容之后,vector的容量为 : 100

(2)案例2:erase删除中间任意位置的元素

//2. erase删除任意位置代码后,linux下迭代器并没有失效
//因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main()
{
	vector<int> v{ 1,2,3,4,5 };

	vector<int>::iterator it = find(v.begin(), v.end(), 3);

	v.erase(it);
	cout << *it << endl;

	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

//程序可以正常运行,并打印:
//4
//4 5

(3)案例3:erase删除最后一个元素

//3: erase删除的迭代器如果是最后一个元素,删除之后it已经超过end
//此时迭代器是无效的,++it导致程序崩溃
int main()
{
	//第一组数据
	vector<int> v{ 1,2,3,4,5 };

	//第二组数据
	//vector<int> v{1,2,3,4,5,6};

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

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

	return 0;
}

//========================================================
//使用第一组数据时,程序可以运行
//[sly@VM - 0 - 3 - centos 20250308]$ g++ testVector.cpp - std = c++11
//[sly@VM - 0 - 3 - centos 20250308]$ . / a.out
//1 3 5
//=========================================================

//使用第二组数据时,程序最终会崩溃
//[sly@VM - 0 - 3 - centos 20250308]$ vim testVector.cpp
//[sly@VM - 0 - 3 - centos 20250308]$ g++ testVector.cpp - std = c++11
//[sly@VM - 0 - 3 - centos 20250308]$ . / a.out
//Segmentation fault

4.与vector类似,string在insert/erase/reserve后,迭代器也会失效

(1)案例

#include <iostream>
#include <string>
using namespace std;

void TestString()
{
	//初始化一个字符串 s,内容为 "hello"
	string s("hello");
	//定义一个迭代器 it,指向字符串 s 的起始位置
	auto it = s.begin();

	// s.resize(20, '!');//若取消这行代码的注释,程序会崩溃。原因是 resize 方法将字符串容量扩展到 20 时,
	                     //若原内存空间不足以容纳新大小的字符串,string 会重新分配一块更大的内存空间,
	                     //并将原内容复制过去,然后释放旧的内存空间。此时,it 仍指向旧的已释放的内存空间,
	                     //迭代器失效。后续若再通过 it 访问内存,程序就会崩溃。

	//遍历字符串 s,逐个输出字符
	while (it != s.end())
	{
		cout << *it;
		++it;
	}
	cout << endl;

	//删除所有元素,将迭代器 it 重新指向字符串 s 的起始位置
	it = s.begin();

	错误写法
	//while (it != s.end())
	//{
	//	//调用 erase 方法删除 it 指向的字符后,it 以及 it 之后的迭代器都会失效。
	//	//因为 erase 操作会使后面的字符依次向前移动,填补被删除字符的位置,
	//	//导致 it 不再指向有效的字符。接着执行 ++it 会使用失效的迭代器,
	//	//这会引发未定义行为,可能导致程序崩溃。
	//	s.erase(it);
	//	++it;
	//}

	错误写法
	//it = s.begin();
	//while (it != s.end())
	//{
	//	//用 it 重新接收 erase 的返回值,此时 it 指向被删除元素的下一个元素,
	//	//解决了迭代器失效的问题。
	//	it = s.erase(it);
	//	//但这里直接 it++ 会跳过下一个元素。当 erase 删除最后一个元素时,
	//	//it 会指向 s.end()。再执行 it++ 后,it 就会指向 s.end() 之后的位置,
	//	//这是一个无效的位置,成为野指针。后续若再使用 it 进行操作,
	//	//会导致越界访问,造成程序崩溃。
	//	it++;
	//}

	//正确写法
	it = s.begin();
	while (it != s.end())
	{
		//s.erase(it) 会删除 it 指向的字符,并返回指向被删除字符下一个字符的迭代器。
		//将这个返回值赋给 it,使得 it 始终指向有效的字符位置,
		//这样可以避免迭代器失效问题,正确地删除字符串中的所有字符。
		it = s.erase(it);
	}
}

int main()
{
	TestString();
	return 0;
}

迭代器失效解决办法:在使用前,对迭代器重新赋值即可。

vector模板类的模拟实现

注意:为了防止 std 命名空间里标准库提供的 vector 模板类和我们自己模拟实现的 vector 模板类发生命名冲突,则此时我们需要在命名空间域 stl 中模拟实现 vector 模板类。

迭代器、operator[ ]

//模拟实现的vector模板类
namespace stl
{
	template<class T>
	class vector
	{
	public:
		//vector的迭代器是一个原生指针
		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;
		}

	private:
		iterator _start;   //指向存储容量的起始位置
		iterator _finish;  //指向有效数据的尾
		iterator _end_of_storage; //指向存储容量的尾
	};
}

capacity、size、empty

capacity()size()函数的模拟实现思路:在连续内存空间中,两个指针相减得到的就是它们之间的元素个数。

//capacity函数:返回vector当前分配的存储空间的大小
//即从_start指针到_end_of_storage指针之间的元素个数
size_t capacity() const
{
	return _end_of_storage - _start;
}

//size函数:返回vector中当前存储的有效元素的个数
//即从_start指针到_finish指针之间的元素个数
size_t size() const
{
	return _finish - _start;
}

//empty函数:判断vector是否为空
//如果_start指针和_finish指针相等,说明没有存储任何有效元素,返回true,否则返回false
bool empty()
{
	return _start == _finish;
}

push_back

1.模拟实现push_back遇到的问题

(1)问题一:vector 初始容量为 0 时的扩容问题

vector刚被构造时,其初始状态的容量capacity()为 0。若此时直接使用reserve(2*capacity())进行扩容后插入数据,会导致越界访问,因为2*capacity() 结果还是 0,相当于没有成功分配到空间。

错误写法:

void push_back(const T& x)
{
	if (_finish == _end_of_storage)
	{
		reserve(2 * capacity());
	}

	*_finish = x;
	++_finish;
}

解决方法:在push_backinsert这类插入操作函数中,单独判断capacity() == 0是否成立。若成立,先给capacity赋予一个初始值(如 4),之后再按照capacity * 2的方式进行后续扩容,从而避免越界问题。string类则是在构造函数中处理此问题,原因是string类重载了较多的push_backinsert函数,若在每个插入函数中都进行判断处理会很繁琐。

(2)问题二:插入数据时自定义类型的初始化问题

在当前模拟实现中,使用new T[n]vector分配空间。对于内置类型,new不做额外处理;对于自定义类型,new会调用其默认构造函数进行初始化。所以当vector空间未满时,可以直接在_finish位置通过赋值插入数据,无需担心自定义类型的初始化。

但如果是向空间配置器申请空间,直接赋值插入无法完成自定义类型的初始化,此时需要使用定位new (p) T(x)p为指向要初始化对象的指针,x为初始化的值)来完成初始化操作。

详细说明:

①使用new T[n]分配空间的情况

在 C++ 中,new操作符用于动态分配内存。当使用new T[n]vector分配空间时:

  • 对于内置类型:例如intdoublechar等,new仅仅负责从堆上分配一块能够存储n个对应内置类型元素的连续内存空间,并不会对这些空间进行初始化操作。这是因为内置类型本身不涉及复杂的构造和析构逻辑,后续在vectorpush_back等操作中,可以直接对这些未初始化的空间进行赋值操作,例如在push_back函数里将数据赋值到_finish指向的位置。
  • 对于自定义类型:当T是自定义类型时,new T[n]除了分配内存空间外,还会依次为每个元素调用该自定义类型的默认构造函数。这是 C++ 语言特性决定的,目的是为了确保对象在创建时能够完成必要的初始化工作,例如初始化成员变量、建立对象间的关联(例如在一个包含指针成员的类中,可能在默认构造函数里将指针初始化为指向其他已存在对象,以此建立对象间的关系,方便后续对象间的交互和数据传递)等。所以,在vector空间未满,执行push_back操作时,由于每个位置的自定义类型对象已经通过默认构造函数初始化好了,此时直接在_finish位置进行赋值操作(如*_finish = x)是安全可行的,不会出现未初始化的问题。

内存释放:使用 delete[] 操作符释放内存。因为 new T[n] 分配的是数组形式的连续内存空间,delete[] 会按顺序依次调用数组中每个元素的析构函数(对于内置类型,析构函数无实际操作;对于自定义类型,析构函数会释放成员变量占用的资源等),然后释放整个数组内存。例如在 vector 的析构函数中,通过 delete[] _start_start 指向分配内存起始位置)来实现内存释放。注意:由于栈后进先出的特性,delete[] 会从数组的最后一个元素开始,依次调用每个自定义类型对象的析构函数,进行资源清理工作。

②向空间配置器申请空间的情况

空间配置器(allocator)是 C++ 标准库中用于内存管理和对象构造析构的工具,它提供了比newdelete更灵活和底层的内存管理方式。

当向空间配置器申请空间时:

  • 内存分配特性:空间配置器分配的内存只是一块原始的、未被初始化的内存区域,它不会像new T[n]那样自动调用自定义类型的默认构造函数。这是因为空间配置器的设计理念是将内存分配和对象构造这两个过程分离,以提高内存管理的效率和灵活性。
  • 初始化问题及解决:在这种情况下,如果在vectorpush_back操作中直接进行赋值操作(如*_finish = x)来插入自定义类型对象,实际上只是将数据简单地拷贝到了未初始化的内存位置,而没有真正调用自定义类型的构造函数来完成对象的初始化工作。这会导致对象处于一种不一致的状态,可能引发各种运行时错误。为了解决这个问题,就需要使用定位new表达式,即(p) T(x) 。其中p是指向要初始化对象的指针(例如_finish指向的位置),x是用于初始化对象的值。定位new表达式会在指定的内存位置上调用对象的构造函数,从而正确地完成自定义类型对象的初始化,保证对象的完整性和正确性。

内存释放:在释放内存时,需要先调用对象的析构函数来进行资源清理,然后再使用空间配置器的内存释放函数(通常是 deallocate 函数)来释放内存。对于自定义类型对象,要调用其析构函数可以使用 _finish->~T() 这样的形式(_finish 指向要析构的对象),这会手动调用对象的析构函数,执行资源清理工作。之后,通过空间配置器的 deallocate(_start, capacity()) 函数(_start 是指向分配内存起始位置的指针,capacity() 返回分配的内存大小)来释放之前分配的内存空间。这样才能确保内存被正确释放,并且对象的资源得到妥善清理。

2.代码实现

//尾插函数:向vector容器的尾部插入一个元素
//参数x是要插入的元素的常量引用,这样可以避免不必要的拷贝构造,提高效率
void push_back(const T& x)
{
	//判断当前vector容器是否已满,即已存储的元素数量达到了当前分配的存储空间大小
	//如果_finish指针和_end_of_storage指针相等,说明空间已满,需要进行扩容操作
	if (_finish == _end_of_storage)
	{
		//检查当前vector容器的容量。如果容量为0,说明这是首次插入元素且尚未分配空间
		//此时初始分配能够容纳4个元素的空间;否则,将当前容量扩大为原来的2倍
		//这样做是为了给新元素腾出足够的空间
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	//在_finish指针所指向的位置插入元素x。由于之前在构造vector时,如果是自定义类型
	//已经通过new T[n]调用了默认构造函数进行初始化(对于内置类型也已分配好空间)
	//所以这里可以直接通过赋值操作将元素x的值赋给_finish指向的位置
	*_finish = x;

	//将_finish指针向后移动一位,使其指向新插入元素之后的位置,即_finish更新到指向vector中有效数据的尾部位置
	++_finish;
}

pop_back

  • //pop_back函数:从vector容器中删除最后一个元素
    void pop_back()
    {
    	//使用assert断言来确保vector容器不为空。
    	//如果vector为空(即empty()函数返回true),程序会中断并输出错误信息,
    	//这样可以避免在空容器上执行删除操作导致未定义行为。
    	assert(!empty());
    
    	//将_finish指针向前移动一位。
    	//因为_finish指针指向的是最后一个有效元素的下一个位置,
    	//向前移动一位后,原来的最后一个元素就不再被视为有效元素,
    	//从而达到了删除最后一个元素的效果(这里只是逻辑上删除,实际内存空间并未释放)。
    	--_finish;
    }

resize

1.内置类型的构造函数

内置类型在模板上下文中的构造函数相关理论说明如下:

(1)理论基础:在 C++ 中,内置类型(如 intdoublechar 等)从严格意义上讲没有像自定义类那样显式定义的构造函数。构造函数一般是针对自定义类型而言的,当自定义类型中我们自己编写了构造函数,编译器就不会自动生成;若未编写,编译器则会自动生成默认构造函数。然而,当引入模板(类模板、函数模板)后,为了实现泛型编程,即编写与具体类型无关的代码,C++ 为内置类型提供了类似构造函数的行为机制。

(2)模板的泛型特性:模板的设计目的是实现代码的通用性,它能够处理各种类型,包括内置类型和自定义类型。为了让模板代码可以统一处理不同类型的数据,C++ 为内置类型赋予了一些类似构造函数的功能,以便在模板中对不同类型进行一致的操作。

(3)用匿名对象初始化内置类型

在 C++ 中,我们可以使用一种特殊的语法,通过匿名对象来初始化内置类型的变量。这种方式在模板编程等场景中具有重要的应用价值,能够实现对不同类型的统一处理。

对于内置类型,如整数类型(intlong 等)、浮点类型(floatdouble 等)以及指针类型,我们可以采用类似构造函数调用的语法形式来创建匿名对象,并利用该匿名对象对变量进行初始化。

案例如下:

  • 整数类型(如 int、long 等)变量通过匿名对象初始化时,变量的值被初始化为 0。例如: int a = int ();
  • 浮点类型(如 float、double 等)变量通过匿名对象初始化时,变量的值被初始化为 0.0。例如: double b = double ();
  • 整数类型(如 int、long 等)变量通过匿名对象初始化时,变量的值被初始化为 nullptr。例如:int* p = int*();

测试:

注意:这种使用匿名对象来初始化内置类型变量的语法形式,虽然看起来像是调用了内置类型的 “构造函数”,但需要注意的是,内置类型并没有真正意义上的构造函数。在模板编程中,这种方式为我们提供了一种统一的初始化机制,使得模板代码能够以相同的方式处理内置类型和自定义类型的初始化,增强了代码的通用性和可复用性,有助于实现更高效、简洁的泛型编程。

(4)总结:尽管内置类型没有真正显式定义的构造函数,但在模板编程的环境下,借助值初始化语法,内置类型能够展现出类似构造函数的行为。这一机制使得模板代码可以统一处理内置类型和自定义类型,极大地增强了代码的通用性和可复用性,满足了泛型编程的需求。

2.模拟实现resize遇到的问题

(1)问题一:resize 函数缺省参数 val 的缺省值设置问题

在 resize 函数 void resize(size_t n, T val = T()); 中,不能将 val 的缺省值设置为 0 。因为 vector 类模板是泛型编程,要能存放任意类型的数据。当 T 为整形、浮点型、指针类型时,val 缺省值为 0 可能没问题,但当 T 为 string 类型时,0 不能用于初始化 string 类型,所以 val 的缺省值应设置为 T()

(2)问题二:T() 作为匿名对象及 T val = T() 的本质

  • T() 是匿名对象T() 是一个匿名对象,即没有显式命名的临时对象,其生命周期局限于创建它的表达式所在的那一行语句。当 T 是内置类型时,T() 产生零初始化的值;当 T 是自定义类型时,T() 调用默认构造函数创建临时对象。
  • T val = T() 的本质T val = T() 从语法上是拷贝初始化,理论上会调用类的拷贝构造函数,将匿名对象 T() 的内容拷贝到 val 中。但现在编译器通常会应用 “返回值优化(RVO)” 或 “具名返回值优化(NRVO)”,优化后会直接在 val 的内存位置上调用默认构造函数,等价于直接用默认构造函数初始化 val

(3)问题三:resize 函数中插入操作的实现差异(模拟实现与 STL 实现对比)

  • 模拟实现的 vector 类模板:使用 new 和 delete 管理内存。当进行扩容操作时,如果模板参数 T 是自定义类型,new T[n] (n 为扩容后的大小)不仅会调用 operator new 从堆上为 n 个 T 类型对象分配连续的内存空间,还会依次调用自定义类型 T 的默认构造函数对每个对象进行初始化。所以在扩容完成后执行尾插操作时,由于新分配的内存空间中的对象已经过默认构造函数的初始化,此时只需调用 operator= 重载函数将待插入的值赋值给 _finish 指针指向的已初始化对象,即可完成插入操作。
  • STL 提供的 vector 类模板:使用空间配置器(allocator)来分配和管理内存。空间配置器的内存分配函数(如 allocate )仅仅负责从堆上分配一块足够存储 n 个 T 类型对象的原始内存区域,并不会自动调用自定义类型 T 的默认构造函数对该内存区域中的对象进行初始化。因此,在执行插入操作时,不能简单地通过赋值操作(调用 operator= )来完成 val 的插入。而是需要使用定位 new 表达式(例如 new((void*)_finish) T(val); ,其中 _finish 指向要插入对象的内存位置),显式地调用自定义类型 T 的构造函数(可以是默认构造函数或其他合适的构造函数,这里假设使用 val 进行初始化时调用的构造函数能满足需求),从而用 val 的值去正确地初始化新插入的自定义类型 T 的对象。
  • 总结
    • 采用 new 分配内存的方式,新分配空间中的对象已由 new 操作自动调用默认构造函数完成初始化,后续插入操作是对已存在且已初始化的对象进行赋值操作,即调用 operator= 重载函数。
    • 采用空间配置器分配内存的方式,新分配空间中的对象未进行初始化,后续插入操作需要使用定位 new 表达式在指定内存位置上定义并初始化对象,即显式调用对象的构造函数来完成初始化和插入过程。

3.代码实现

//resize函数功能:开空间 + 初始化 或者 删除数据  
//参数 n 表示调整后的容量大小(注:即使 n < capacity(),也不考虑缩容,因为缩容一般是异地缩容)
//参数 val 表示用于初始化新增加空间的默认值,默认为T()(注:T() 表示调用 T 的默认构造函数生成的匿名对象)
void resize(size_t n, T val = T())//注:T val = T()编译器优化成直接调用默认构造函数创建val
{
	//如果调整后的大小 n 小于当前已存储元素的数量size()
	//则需要删除多余的数据,将_finish指针移动到新的有效数据末尾位置
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		//如果n大于当前已分配的容量capacity(),则需要扩容
		if (n > capacity())
			reserve(n);

		//对新增加的空间进行初始化
		//从当前有效数据末尾_finish位置开始,直到新的容量大小 n 对应的位置
		while (_finish != _start + n)
		{
			//将val赋值给当前_finish指向的位置,完成插入操作
			//这里的操作根据内存分配方式的不同(new或空间配置器)有所差异
			//若使用new分配内存,空间已初始化,这里是赋值操作;
			//若使用空间配置器,空间未初始化,需要使用定位new初始化
			*_finish = val;

			//_finish指针后移,指向下一个待初始化的位置
			++_finish;
		}
	}
}

insert

1.insert关于迭代器失效问题

注意:图片中的insert是错误代码。

分析上面代码遇到的问题及对应解决方式

(1)循环条件设计相关问题

问题1:insert 中 while 循环条件的设计是否存在死循环的隐患

  • 解析:在 insert 函数实现中,当 pos 为首元素地址时,while 循环条件的设计需要谨慎。在 string 的 insert 实现中,while 循环造成死循环是因为 end 的类型是 size_t 。而在 vector 的 insert 中,end 的类型是指针。总体而言,如果 end 是下标(size_t 类型),使用类似 while (end >= pos) 这种设计时要特别小心,很可能会导致死循环;若 end 是指针类型,这种循环条件相对安全一些,但仍需正确处理。
  • 解决方案:在编写循环条件时,要明确 end 和 pos 的类型以及它们所指向的位置关系,确保循环在合理的情况下终止。可以通过在循环内部添加合适的边界检查或调整循环条件来避免死循环。

(2)迭代器失效相关问题

问题 1insert内部发生扩容会导致迭代器pos失效

  • 测试:
    void insert(iterator pos, const T& val)
    {
        assert(pos >= _start);
        assert(pos <= _finish);
    
        if (_finish == _end_of_storage)
        {
            reserve(capacity() == 0? 4 : capacity() * 2);
        }
    
        iterator end = _finish - 1;
        while (end >= pos)
        {
            *(end + 1) = *end;
            --end;
        }
    
        *pos = val;
        ++_finish;
    }
    
    //测试代码
    void test_vector3()
    {
    	vector<int> v1;
    	v1.push_back(1);
    	v1.push_back(2);
    	v1.push_back(3);
    	v1.push_back(4);
    	//v1.push_back(5);//注意:若该行代码没有被注释掉,则会以为上面的insert代码没有问题;
                          //若被注释掉,则该测试代码一定发生崩溃。
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    
    	v1.insert(v1.begin(), 0);//一定在析构函数处发生崩溃导致程序发生报错。
                                 //引发崩溃的原因是insert的实现有问题。
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    }
  • 解析:当 vector 进行扩容操作时,原有的内存空间会被重新分配,这会使得原有的迭代器失效。在当前 insert 函数实现中,如果在插入元素时触发扩容,而没有对迭代器进行相应处理,就会出现问题。以测试代码为例,先通过 push_back 插入 4 个元素使 vector 满员,此时若再用 insert 插入数据,insert 内部会进行扩容。扩容后,vector 的 _start_finish_end_of_storage 三个指针会指向新内存空间。然而,insert 函数的形参 pos 迭代器仍指向旧空间,成为野指针。后续执行类似 while (end >= pos) 的循环判断时,end 指向新空间,pos 指向旧空间,两者指向不一致,导致循环条件判断错误,陷入死循环,最终使程序崩溃。
  • 解决方式:在 insert 函数内部进行扩容操作后,需要重新定位迭代器 pos ,使其指向扩容后的新空间。具体做法是,在扩容前计算 pos 相对于 _start 的偏移量,扩容后根据新的 _start 和偏移量重新确定 pos 的位置。代码实现如下:
    //insert函数,在指定位置pos插入元素val,解决扩容导致的迭代器失效问题
    //pos为插入位置的迭代器,val为要插入的元素
    void insert(iterator pos, const T& val)
    {
        //断言确保插入位置在有效范围内
        assert(pos >= _start);
        assert(pos <= _finish);
    
        //如果当前空间已满,进行扩容
        if (_finish == _end_of_storage)
        {
            //计算pos相对于容器起始位置的偏移量
            size_t len = pos - _start;
            //进行扩容操作
            reserve(capacity() == 0? 4 : capacity() * 2);
            //根据之前计算的偏移量,在扩容后的容器中重新定位pos
            pos = _start + len;
        }
    
        //从容器末尾开始,将元素向后移动一位,为插入元素腾出空间
        iterator end = _finish - 1;
        while (end >= pos)
        {
            //将当前位置的元素向后移动一位
            *(end + 1) = *end;
    
            // 迭代器向前移动
            --end;
        }
    
        //在指定位置插入新元素
        *pos = val;
    
        // 更新有效元素的末尾位置
        ++_finish;
    }

问题标题 2:pos 传值导致的迭代器失效后续使用问题

  • 测试:
  • ​
    void insert(iterator pos, const T& val)
    {
        assert(pos >= _start);
        assert(pos <= _finish);
    
        if (_finish == _end_of_storage)
        {
            size_t len = pos - _start;
            reserve(capacity() == 0? 4 : capacity() * 2);
            pos = _start + len;
        }
    
        iterator end = _finish - 1;
        while (end >= pos)
        {
            *(end + 1) = *end;
            --end;
        }
    
        *pos = val;
        ++_finish;
    }
    
    //测试代码
    void test_vector3()
    {
    	vector<int> v1;
    	v1.push_back(1);
    	v1.push_back(2);
    	v1.push_back(3);
    	v1.push_back(4);
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    
    	auto pos = find(v1.begin(), v1.end(), 3);
    	if (pos != v1.end())
    	{
    		v1.insert(pos, 30);
    	}
    
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    
    	//iterator insert(iterator pos, const T& val)//注:pos使用的是传值传参
    	//insert以后我们认为pos失效了,不能再使用。
    	(*pos)++;//注:erase以后,由于没有对迭代器pos重新赋值则此时pos变成野指针,
                 //若此时对pos进行解引用会造成程序发生崩溃。
    
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    }
    
    ​
  • 解析:由于 pos 是通过值传递给 insert 函数的,即使在 insert 内部解决了形参 pos 的迭代器失效问题,形参的改变也不会影响到实参。当 vector 扩容后,实参 pos 就会变成野指针(依然指向旧空间),此时再使用 pos 来访问 vector 中的数据,就会导致错误。例如在测试代码中,对 pos 进行 (pos)++ 操作也不会改变 vector 中的数据,因为此时的 pos 已经失效。

  • 解决方式

    • 方式1(不可使用),传引用传参:将 pos 以引用的方式传递给 insert 函数,这样在 insert 内部对 pos 的修改就能反映到外部的实参上,避免实参 pos 成为野指针,从而可以继续安全地使用 pos 访问数据。方式1不可取的原因在于:使用类似 v1.insert(v1.begin(), val) 这种调用形式时,v1.begin() 返回的是一个临时对象。而临时对象具有常属性,只能调用常成员函数,不能作为左值被修改 。若采用引用传递的方式将 pos 传递给 insert 函数,在函数内部对 pos 进行修改时,就相当于对一个具有常属性的临时对象进行修改,这会引发编译错误,所以该方式不可取。总的来说,迭代器pos若使用引用传参,当使用v1.begin()作为实参传递,就会造成insert的形参iterator& pos的权限放大。
    • 方式2(建议采用),传值传参:让 insert 函数以值传递的方式返回新的 pos 迭代器,该迭代器指向新插入数据的位置。调用者可以使用返回的迭代器来进行后续操作,而不再依赖原来可能已经失效的 pos 迭代器。代码实现如下:
      iterator insert(iterator pos, const T& val)
      {
          assert(pos >= _start);
          assert(pos <= _finish);
      
          if (_finish == _end_of_storage)
          {
              size_t len = pos - _start;
              reserve(capacity() == 0 ? 4 : capacity() * 2);
              pos = _start + len;
          }
      
          iterator end = _finish - 1;
          while (end >= pos)
          {
              *(end + 1) = *end;
              --end;
          }
      
          *pos = val;
          ++_finish;
      
          return pos;
      }

2.代码实现

//模拟vector的insert函数,在指定位置pos插入值val
iterator insert(iterator pos, const T& val) 
{
	//判断插入位置的合法性,确保pos在容器有效范围内
	assert(pos >= _start);
	assert(pos <= _finish);

	//如果当前vector已满,需要进行扩容
	if (_finish == _end_of_storage) 
	{
		//计算pos相对于容器起始位置的偏移量,用于扩容后重新定位pos
		size_t len = pos - _start;

		//扩容,初始容量为0时扩容到4,否则扩容为原来的2倍
		reserve(capacity() == 0 ? 4 : capacity() * 2);

		//根据之前计算的偏移量,在扩容后的容器中重新定位pos
		pos = _start + len;
	}

	//从容器末尾开始,将元素向后移动一位,为插入元素腾出空间
	iterator end = _finish - 1;

	while (end >= pos) 
	{
		//将当前位置的值赋给下一个位置
		*(end + 1) = *end;

		//向前移动迭代器
		--end;
	}

	//在指定位置pos插入新元素val
	*pos = val;

	//更新_finish指针,指向新的有效数据末尾
	++_finish;

	//返回插入元素后的pos迭代器(可根据实际需求选择是否返回,这里为了处理迭代器失效问题)
	return pos;
}
  • 测试:
    void test_vector3()
    {
    	vector<int> v1;
    	v1.push_back(1);
    	v1.push_back(2);
    	v1.push_back(3);
    	v1.push_back(4);
    	//v1.push_back(5);
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    
    	v1.insert(v1.begin(), 0);
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    
    	auto pos = find(v1.begin(), v1.end(), 3);
    	if (pos != v1.end())
    	{
    		pos = v1.insert(pos, 30);//注意:在这个测试代码中,若没有用pos接收
                                     //v1.insert(pos, 30)的返回值,而是直接写成
                                     //v1.insert(pos, 30); ,然后对pos进行解引用,
                                     //则一定会发生报错,因为此时pos是个野指针
                                     //(即迭代器pos失效了).
    	}
    
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    
    	//但是最好我们是别用pos,因为我们若是没有传值返回给pos接收,
        //则pos可能是野指针(即迭代器失效)
    	(*pos)++;//注:此时的pos没有失效,因为insert以后失效迭代器pos被重新赋值了。
    
    	for (auto e : v1)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    }
  • 结论:insert以后,我们认为迭代器失效了,最好后续不要使用失效迭代器。若是想继续使用失效迭代器则必须给失效迭代器重新赋值才行。

erase

1.代码实现

//erase函数,用于从vector中删除指定位置的元素,返回值为一个迭代器
//pos为要删除元素的迭代器位置
iterator erase(iterator pos)
{
    // 断言:确保要删除元素的位置pos在vector的有效范围内(包含_start,不包含_finish)
	assert(pos >= _start);
	assert(pos < _finish);

    // 定义迭代器start,指向要删除元素的下一个位置,准备进行元素向前移动操作
	iterator start = pos + 1;

    //当start迭代器没有到达vector有效元素的末尾时,执行循环
    //目的是将pos之后的元素依次向前移动一位,覆盖掉要删除的元素
	while (start != _finish)
	{
        //将当前start指向元素的值赋给其前一个位置,实现元素向前移动
		*(start - 1) = *start;
         //start迭代器向后移动一位,准备处理下一个元素
		++start;
	}

     //将_finish指针向前移动一位,表明vector中有效元素的数量减少了一个
	--_finish;

    //返回删除元素的下一个元素的新位置
	return pos;//即返回指向删除元素下一个位置的迭代器
}

2.erase关于迭代器失效问题

2.1.不同编译器下 erase 后迭代器使用的检查差异

测试代码:

void test_vector4()
{
	std::vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto pos = find(v1.begin(), v1.end(), 2);
	if (pos != v1.end())
	{
		v1.erase(pos);
	}

	(*pos)++;

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

(1)不同编译器对 erase 后迭代器使用的检查及报错问题:
注意:在 C++ 中模拟实现 vector 的 erase 函数时,不同的编译器对待 erase 操作后迭代器的使用规则存在明显差异。

①在 Visual Studio(VS)环境下,std::vector 对迭代器的有效性有着严格的检查机制。当执行 erase 操作后,相关的迭代器(如用于指定删除位置的 pos 迭代器)会被视为失效状态。如果后续代码继续使用该失效迭代器进行操作,例如解引用(*pos)或者自增(pos++)等,std::vector 会检测到这种非法操作,并抛出运行时错误,以提示开发者迭代器已失效,防止程序出现未定义行为。

②在 Linux 的 g++ 编译器环境下,情况有所不同。g++ 对 erase 操作后迭代器的使用并没有像 VS 那样的强制检查机制。这意味着即使在 erase 操作后继续使用可能已经失效的迭代器,程序在编译阶段不会报错,甚至在某些情况下能够正常运行。然而,这并不代表操作是正确和安全的。实际上,使用失效迭代器会导致程序出现未定义行为,可能在运行过程中的某个时刻突然崩溃,或者产生不符合预期的结果,而且这种问题往往难以调试和定位。

(2)解决方式:鉴于不同编译器的这种差异,为了确保程序的稳定性和正确性,我们不应依赖编译器的检查机制。无论在 VS 还是 g++ 环境下,都应该明确知晓 erase 操作会使相关迭代器失效这一事实。在 erase 操作之后,如果需要继续对 vector 进行操作,应避免直接使用 erase 前的迭代器。正确的做法是使用 erase 函数的返回值,该返回值是一个指向删除元素下一个位置的迭代器。通过使用这个返回的迭代器来更新后续操作所使用的迭代器,能够保证迭代器始终指向有效的内存位置,从而避免因迭代器失效引发的各种问题。

2.2.erase 后迭代器失效的不同场景

(1)场景1删除位置不是最后一个有效数据时的迭代器失效问题

解析:当要删除的位置的实参 pos 不是 vector 中最后一个有效数据的位置时,erase 操作后,实参 pos 所代表的迭代器不会失效,仍可安全使用。因为此时 vector 后续的数据会向前移动来填补删除元素的位置,迭代器 pos 仍然指向有效的内存位置。然而,这并不意味着可以无限制地随意使用该迭代器。虽然它仍然指向一个有效的内存位置,但由于元素的移动,其指向的元素已经发生了改变。而且,如果在后续的操作中,继续对 vector 进行插入或删除操作,可能会导致 vector 进行扩容或内存重新分配,此时 pos 迭代器就可能会失效。此外,在多线程环境下,其他线程对 vector 的修改也可能会影响 pos 迭代器的有效性。

(2)场景2:删除位置是最后一个有效数据时的迭代器失效问题


解析:当要删除的位置的实参 pos 是 vector 中最后一个有效数据的位置时,erase 操作后,实参 pos 会变成野指针,即迭代器失效。这是因为删除最后一个元素后,vector 的 _finish 指针会向前移动一位,原 pos 所指向的位置不再有效。如果在 erase 操作后继续使用这个失效的迭代器 pos,例如尝试对其进行解引用操作(*pos)来访问数据,或者进行自增(pos++)等操作,将会导致未定义行为。这是因为该迭代器指向的内存位置可能已经被操作系统重新分配给其他程序使用,或者存储着无效的数据。在这种情况下,程序可能会突然崩溃,或者出现数据错误、逻辑混乱等难以调试的问题。

解决方式:在 erase 操作后,不要使用原迭代器 pos,而是使用 erase 函数返回的迭代器(指向删除元素下一个位置,在此场景下实际指向 _finish 位置)来更新迭代器,以确保后续操作的安全性。

string的insert、erase以后迭代器失效问题

在 C++ 中,string 类确实存在迭代器失效的概念。

string 类提供了通过下标 pos 访问数据的方式,使用下标访问时,不存在像迭代器那样因容器内部结构改变而导致的失效问题。这是因为下标本质上是基于 string 对象内部连续存储的字符数组的偏移量,只要 string 对象的内存没有发生释放或重新分配,基于固定偏移量的下标访问始终能正确定位到相应字符。

然而,当 string 的 insert 和 erase 函数使用迭代器进行操作时,即 iterator insert (iterator p, char c) 和 iterator erase (iterator p) 形式,在这些操作执行后,迭代器 p 会面临失效问题。

1.insert 以后迭代器失效问题

(1)形参 pos 迭代器失效的原因及解决方式

  • 失效原因:当 string 的 insert 函数内部进行扩容时,string 对象的内存空间会重新分配。原本指向旧内存空间的形参迭代器 pos 就会变成野指针,因为它所指向的位置已经不存在于新的内存空间中,从而导致形参迭代器 pos 失效。
  • 解决方式:在 insert 函数内部进行扩容操作前,先计算形参迭代器 pos 相对于旧空间起始位置的偏移量。在扩容完成后,根据新的内存空间起始位置和之前计算的偏移量,重新确定 pos 在新空间中的位置,使其指向正确的位置,从而避免迭代器失效。

(2)实参 pos 迭代器失效的原因及解决方式

  • 失效原因insert 函数的迭代器 pos 采用值传递的方式传参,这意味着函数内部的形参 pos 是实参 pos 的一个临时拷贝。当 insert 函数执行过程中发生扩容等操作导致内存空间变化时,实参 pos 仍然指向旧的内存空间,而形参 pos 可能已经在函数内部被调整(如上述解决形参失效的操作)。函数执行完毕后,实参 pos 就变成了野指针,若继续使用它来访问 string 对象,将会引发程序崩溃或未定义行为。
  • 解决方式:将 insert 函数设计为传值返回,返回值为指向插入元素位置的迭代器。在调用 insert 函数后,若还想继续使用原来的迭代器 pos 来操作 string 对象,就需要用实参 pos 接收 insert 函数的返回值。这样实参 pos 就会被更新为指向正确位置的迭代器,从而解决实参迭代器 pos 失效的问题。

(3)insert 函数不能使用传引用方式传递迭代器 pos 的原因

string 的 begin() 函数是传值返回一个具有常属性的临时变量。如果在调用 insert 函数时,直接将 begin() 的返回值作为迭代器 pos 的实参,并且 insert 函数的形参 pos 采用引用传递,那么就会出现形参把实参的权限放大的情况(因为实参是常属性的临时变量,而引用传递可能会允许对其进行修改),这不符合 C++ 的语法规则,导致编译无法通过。

同时,若将形参 pos 声明为 const 引用,虽然可以避免权限放大的问题,但在 insert 函数体内部就无法对 pos 进行解引用操作来访问和修改数据,这与 insert 函数需要在指定位置插入元素并可能修改该位置及后续数据的功能相违背,所以也不能使用 const 引用的方式来传递迭代器 pos

2.erase 以后迭代器失效问题

(1)实参 pos 迭代器失效的原因

string 的 erase 函数通过挪动数据的方式来完成删除操作。当调用 erase 函数删除 pos 位置的元素后,实参迭代器 pos 所指向的位置不再是原来被删除的元素,而是变成了被删除元素的下一个元素的位置。如果在使用迭代器 pos 遍历 string 并调用 erase 删除数据后,直接对实参 pos 进行 pos++ 操作,就会跳过一些还未遍历的数据,导致遍历不完整,进而可能使代码的执行结果出现未定义的情况。

(2)解决实参 pos 迭代器失效的方式

将 erase 函数设计为传值返回,返回值为指向删除元素下一个位置的迭代器。在调用 erase 函数后,用实参迭代器 pos 接收 erase 函数的返回值,然后从更新后的 pos 位置重新开始遍历 string 对象,而不是直接对原 pos 进行 pos++ 操作跳过数据。这样可以确保迭代器 pos 始终指向正确的位置,避免因迭代器失效而导致的遍历错误和结果未定义的问题。

构造函数

默认构造函数 vector()

vector()
	:_start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{}
  • 实现思路:vector 类通常包含三个关键的成员指针:_start 指向 vector 内部存储元素的起始位置,_finish 指向最后一个有效元素的下一个位置,_end_of_storage 指向已分配内存空间的末尾的下一个位置。在默认构造函数中,将这三个指针都初始化为 nullptr,表明此时 vector 为空,未分配实际内存空间。即这表示在创建 vector 对象时,它还没有分配任何实际的内存空间来存储元素,也没有任何有效元素存在。
  • 注意:如果不将这些指针初始化为 nullptr,它们将处于未初始化状态。在后续对 vector 对象进行操作时,访问未初始化的指针会导致未定义行为,可能会引发程序崩溃或产生错误的结果。

带参数构造函数 vector(size_t n, const T& val = T())

参数解析:

  • size_t n:表示要在vector中创建的元素个数
  • const T& val = T():缺省参数,用于指定每个位置初始化的值,T() 是调用 T 类型的默认构造函数创建的匿名对象,用该匿名对象初始化 val(注:表达式const T& val = T()编译器优化为直接调用默认构造函数初始化val)。匿名对象生命周期通常只在其定义的那一行,但通过 const T& 传参,可延长其生命周期到引用域结束,即const引用可以延长匿名对象的生周期。
  • 测试1:匿名对象生命周期只在其定义的那一行。
  • 测试2:const引用可以延长匿名对象的生周期。
  • 注意:匿名对象、临时对象具有常属性,若使用引用 / 指针 指向 匿名对象 / 临时对象 时必须使用const引用 / const指针。

1.错误代码

//vector的构造函数,用于创建一个包含n个值为val的元素的vector对象
//n:表示要在vector中创建的元素个数,类型为size_t
//(无符号整数类型,常用于表示数量、大小等)

//const T& val:表示要用来初始化vector中元素的值,采用const引用传参,这样可以避免
//不必要的对象拷贝,提高效率,并且T()为默认值,表示如果调用构造函数时未传入该参数,
//则会调用T类型的默认构造函数来创建一个匿名对象作为val的初始值

//错误代码
vector(size_t n, const T& val = T())
{
    //循环n次,将val插入到vector中,从而创建n个值为val的元素
    for (size_t i = 0; i < n; ++i)
    {
        //push_back函数用于在vector的末尾添加一个元素,这里将val添加到vector中
        push_back(val); 
    }
}

(1)错误代码分析

①问题 1:在原实现中,直接使用push_back函数向vector中插入元素,而没有提前开辟足够的空间。这样做会导致在插入元素的过程中,如果vector的现有空间不足,就会频繁触发扩容操作。扩容操作涉及到重新分配内存、拷贝元素等开销较大的操作,频繁进行会严重影响程序的性能。解决这个问题的方法是在插入元素之前,使用reserve函数预先开辟指定大小的空间,从而减少扩容的次数,提高程序的运行效率。

②问题 2:在使用reserve函数进行空间开辟时,如果没有提前对vector的三个指针成员变量_start_finish_end_of_storage进行初始化,那么在reserve函数内部访问这些未初始化的指针时,就会导致程序崩溃。这是因为未初始化的指针可能指向任意的内存地址,对其进行访问是未定义的行为。解决这个问题的方式是在构造函数的初始化列表中对这三个指针进行初始化,确保它们在reserve操作之前处于一个已知的、有效的状态。reserve实现如下所示:

  • void reserve(size_t n)
    {
    	if (n > capacity())
    	{
    		size_t sz = size();
    		T* tmp = new T[n];
    		if (_start)
    		{
    			for (size_t i = 0; i < sz; ++i)
    			{
    				tmp[i] = _start[i];
    			}
    			delete[] _start;
    		}
    
    		_start = tmp;
    		_finish = _start + sz;
    		_end_of_storage = _start + n;
    	}
    }

2.正确代码

vector(int n, const T& val = T())
    :_start(nullptr)
    ,_finish(nullptr)
    ,_end_of_storage(nullptr)
{
    //预先分配能容纳n个元素的内存空间,减少后续插入时
    //的扩容次数,提高效率
    reserve(n);
    for (int i = 0; i < n; ++i)
    {
        push_back(val);
    }
}

(1)解析

在这个正确的实现中,首先在初始化列表中对三个指针成员变量进行了初始化,将它们设置为nullptr。然后调用reserve(n)函数预先开辟了能够容纳n个元素的空间。最后,通过一个循环,使用push_back(val)将指定的值val插入到vectorn次,完成vector的构造。这样的实现顺序保证了在进行插入操作之前,vector已经具备了足够的空间,并且指针成员变量处于有效的状态,避免了之前提到的两个问题。

(2)带参数构造函数vector(int n, const T& val = T())通过尾插(push_back)来实现主要有以下几方面原因:

  • 符合vector动态数组特性vector底层是动态数组结构,尾插操作在这种结构中实现较为自然和高效。在动态数组中,在尾部添加元素通常只需要简单地将元素放置在当前有效元素的下一个位置(如果空间足够),或者在扩容后进行放置。通过push_back进行尾插,不需要对已有元素的位置进行大规模调整,只涉及新元素的插入和相关指针(如_finish)的调整,时间复杂度在不扩容时为常数级别O(1) ,即使扩容,平均时间复杂度也是分摊后的接近O(1),能够高效地构建包含多个相同元素的vector
  • 复用已有功能vector类通常会独立实现push_back函数,该函数已经处理了诸如空间检查、扩容、元素赋值等一系列复杂操作。在构造函数中使用push_back来插入元素,能够复用这些已经实现并经过测试的功能,减少代码冗余。同时,也降低了编写构造函数时出错的可能性,因为push_back函数经过封装,其内部逻辑相对稳定和可靠。
  • 便于实现和理解:从代码实现和理解的角度来看,通过循环调用push_back来构造vector符合直观的编程思维。对于我们而言,很容易理解这是在逐个将元素添加到vector中,代码逻辑清晰易懂。相比其他可能的实现方式(如一次性初始化多个元素到内存中,但需要处理更多的内存分配和元素初始化细节),使用push_back的方式更加简洁明了,便于开发和调试。

迭代器区间构造函数 template <class InputIterator> vector(InputIterator first, InputIterator last)

1.注意事项

(1)vector类模板及其成员函数

vector是 C++ 中的一个类模板,它提供了动态数组的功能。作为类模板,vector可以根据不同的类型参数实例化出各种具体类型的vector容器,比如vector<int>vector<string>等。而vector类模板中的成员函数,也可以是函数模板,这为vector的使用带来了更多的灵活性和通用性。

(2)两种迭代器区间构造函数形式的对比

①vector(iterator first, iterator last)形式

  • 在这种构造函数形式中,参数firstlast要求是vector自身的迭代器类型。这里的iteratorvector类内部定义的迭代器类型,它与vector的内部数据存储结构紧密相关。
  • 这种形式的局限性在于,它只能接受vector自身的迭代器来进行构造。这意味着,如果我们想要从其他容器(如listdeque等)构造一个vector,或者使用自定义数据结构的迭代器来构造vector,就无法直接使用这个构造函数,因为其他容器或自定义数据结构的迭代器类型与vector自身的迭代器类型通常是不同的。这在一定程度上限制了vector的使用场景,降低了代码的复用性和通用性。

template <class InputIterator> vector(InputIterator first, InputIterator last)形式

  • 这是一个函数模板形式的构造函数。其中,template <class InputIterator>声明了一个模板类型参数InputIterator。这意味着在调用这个构造函数时,InputIterator可以是任意符合输入迭代器(Input Iterator)概念的类型。
  • 输入迭代器的概念规定了一些必须支持的操作,例如可以进行解引用操作(*)以访问迭代器所指向的元素,支持前置递增操作(++)以移动到下一个元素等。只要一个类型满足这些基本的输入迭代器操作要求,并且所存储的数据类型与要构造的vector的数据类型一致(例如,要构造vector<int>,则传入的迭代器所指向的元素类型也应该是int;如果vector存储char类型数据,那么可以传入string的迭代器区间来构造vector,因为string存储的是char类型数据),就可以作为InputIterator类型传入这个构造函数。
  • 通过这种函数模板的方式,vector的迭代器区间构造函数可以接受各种不同类型的迭代器,包括listdeque等容器的迭代器,甚至是自定义数据结构的迭代器。这大大增强了vector的通用性,使得vector可以方便地与其他容器或数据结构进行数据交互,提高了代码的复用性。例如,我们可以很容易地从一个list<int>容器构造一个vector<int>,只需要将list<int>的迭代器区间传入这个函数模板构造函数即可。

③总结:综上所述,使用template <class InputIterator> vector(InputIterator first, InputIterator last)这种函数模板形式的构造函数,能够克服vector(iterator first, iterator last)形式的局限性,为vector提供更广泛的适用场景和更强的通用性。因此,在实现vector的迭代器区间构造函数时,不应该写成vector(iterator first, iterator last)这种形式,而应该采用函数模板的形式,以充分发挥vector作为类模板的灵活性和强大功能,提高代码的可扩展性和复用性。

2.代码实现

// [first, last) //注:所有迭代器区间都是左闭右开
template <class InputIterator>
vector(InputIterator first, InputIterator last)
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr)
{
	//复用push_back来插入数据
	//注:当使用迭代器遍历时,一定不要把循环条件写成first < last,
	//而是应该写成first != last。原因:若是用迭代器遍历链表时,后面
	//的结点不一定比前面的结点大,若此时用first < last作为循环条件,
	//则结果是未定义的,极大可能会发生报错。
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

3. 迭代器区间构造函数使用介绍

(1)传自身的迭代器区间进行构造

void test_vector()
{
	vector<int> v1(10, 5);
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//测试:传自己的迭代器区间范围进行构造
	//注意:若写成这样vector<int> v2(++(v1.begin()), --(v1.end()));,
	//则代码会发生报错,因为begin()、end()都是传值返回,而传参返回是参数临时
	//对象,而临时对象具有常属性,若是直接给begin()、end()的返回值进行++、--等操作
	//则会直接引发程序报错。
	vector<int> v2(v1.begin() + 1, v1.end() - 1);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
}

解析:可以使用一个vector对象的部分迭代器区间来构造另一个vector对象。例如,vector<int> v1(10, 5); vector<int> v2(v1.begin() + 1, v1.end() - 1);,这样v2就会包含v1中除了第一个和最后一个元素之外的其他元素。需要注意的是,vectorbegin()end()函数不能使用引用返回。这是因为如果返回引用,当vector对象的生命周期结束时,引用所指向的内存可能已经被释放,从而导致悬空引用的问题。而返回值类型可以避免这个问题,确保每次调用begin()end()都能得到有效的迭代器。

(2)传其他容器的迭代器区间进行构造

void test_vector()
{
	//测试:传其他容器的迭代器区间范围进行构造
	//注:只要string容器和vector容器中存放的数据类型是一致的,则就可以
	//使用string的迭代器区间范围来构造vector
	std::string s1("hello");
	vector<int> v3(s1.begin(), s1.end());
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;
}

解析:如果其他容器存储的数据类型与vector一致,就可以使用该容器的迭代器区间来构造vector。例如,string s = "hello"; vector<char> v3(s.begin(), s.end());,这里使用了string的迭代器区间来构造一个存储char类型的vector。这种方式使得不同容器之间的数据转换变得更加方便,可以根据具体的需求选择合适的容器进行数据存储和处理,并在需要时轻松地将数据转换为vector的形式。

(3)传静态数组区间范围进行构造

void test_vector()
{
	//测试:使用静态数组的区间范围进行构造
	//注:只有原生指针指向数组时,原生指针才可以当做天然的迭代器。
	//可以使用数组的区间范围进行构造的原因:原生指针可以当做天然的迭代器。
	int a[] = { 10, 20, 30 };
	vector<int> v4(a, a + 3);//注:使用静态数组区间范围进行构造
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;
}

解析:由于静态数组的地址可以当作迭代器使用,因此可以使用静态数组的区间范围来构造vector。例如,int arr[] = {10, 20, 30}; vector<int> v4(arr, arr + 3);,这样就创建了一个包含arr数组中前三个元素的vector。这种方式为从静态数组数据创建vector对象提供了一种便捷的途径,特别是在处理已经存在的数组数据时,可以避免手动逐个插入元素的繁琐操作。

构造函数的优化

1.优化思路

观察前面介绍的几个构造函数可以发现,每个构造函数都需要对三个指针成员变量_start_finish_end_of_storage进行初始化,以确保在插入数据时程序不会出错。为了减少代码冗余,可以在类成员变量声明处提供缺省值,这样在构造函数中如果没有在初始化列表中显式初始化这些成员变量时,编译器会自动使用成员变量声明处的缺省值进行初始化。这种方式既减少了代码的重复编写,又保证了成员变量在构造函数执行时能够得到正确的初始化。

2.优化后代码

template<class T>
class vector
{
public:
    typedef T* iterator;
    typedef const T* const_iterator;

    vector() {}

    vector(size_t n, const T& val = T())
    {
        reserve(n);
        for (size_t i = 0; i < n; ++i)
        {
            push_back(val);
        }
    }

    // [first, last) 左闭右开
    template <class InputIterator>
    vector(InputIterator first, InputIterator last)
    {
        while (first != last)
        {
            push_back(*first);
            ++first;
        }
    }

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

3. 参数匹配问题导致编译错误

(1) 参数匹配和模板推导过程

当执行 vector<int> v1(10, 5); 时:

  • 普通构造函数匹配:实参 10 是 int 类型,而普通构造函数的形参 n 是 size_t 类型,int 到 size_t 需要进行隐式类型转换。实参 5 用于推导模板参数 T,会推导出 T 为 int 类型。从类型转换和推导角度,这个构造函数理论上可以匹配,但存在类型转换。
  • 模板构造函数匹配:根据模板参数推导规则,实参 10 会被用于推导模板构造函数中形参 first 的类型参数 InputIterator10 是 int 类型,所以 InputIterator 被推导为 int 类型;同理,实参 5 会将形参 last 的类型参数 InputIterator 也推导为 int 类型。

(2)编译错误产生的原因

  • 模板构造函数优先级与匹配:在 C++ 的函数重载解析规则中,对于模板函数和普通函数的匹配,在某些情况下模板函数会被优先考虑 。这里编译器优先选择了模板构造函数进行匹配。
  • 不合法的操作:模板构造函数内部使用了 push_back(*first) 语句,其意图是将 first 指向的元素插入到 vector 中。然而,由于 InputIterator 被推导为 int 类型,而 int 类型不是迭代器类型,不支持解引用(*)操作。对 int 类型的 first 进行 *first 操作属于非法的间接寻址,这就导致了编译错误 C2100 非法的间接寻址

(3)总结

综上所述,编译错误是由于编译器在匹配构造函数时优先选择了模板构造函数,而模板构造函数在参数推导后对非迭代器类型(int)进行了不合法的解引用操作。可以通过重载一个接受 int 类型参数的普通构造函数 vector(int n, const T& val = T()) ,让编译器在遇到 vector<int> v1(10, 5); 这样的调用时优先匹配这个更合适的构造函数,从而避免该编译错误。

(4)解决方式

①标准模板库(STL)解决此问题的方式

在标准模板库的 vector 实现中,同样采用了函数重载的策略来处理这种参数类型匹配的情况。STL 提供了多个不同参数类型的构造函数重载,例如:

  • vector(size_type n, const T& value) :接受 size_type 类型(通常是无符号整型,类似 size_t)的参数 n 表示元素个数,以及 const T& 类型的 value 表示元素初始值。
  • vector(int n, const T& value) :专门针对 int 类型的参数 n 进行重载,方便用户传入 int 类型的元素个数来构造 vector
  • vector(long n, const T& value) :针对 long 类型的参数 n 进行重载。

通过提供这些不同参数类型的构造函数重载,STL 中的 vector 能够根据传入参数的具体类型,准确地匹配到合适的构造函数,避免因参数类型匹配问题导致选择错误的构造函数(如模板构造函数错误匹配),从而有效解决了参数类型匹配引发的编译错误问题,让用户可以更加灵活和准确地使用 vector 来创建对象。

②模拟实现vector模板类解决此问题的方式

为了解决这个问题,可以通过函数重载的方式,新增一个参数类型为 int 的构造函数 vector(int n, const T& val = T()) 。具体作用和原理如下:

  • 明确匹配规则:新增的 vector(int n, const T& val = T()) 构造函数,使得当传入 int 类型的参数创建 vector 对象时,编译器会优先匹配这个构造函数,而不是模板构造函数。因为对于 vector<int> v(10, 5); 这样的调用,int 类型与 vector(int n, const T& val = T()) 的形参类型直接匹配,无需进行复杂的模板参数推导,避免了模板构造函数中错误的类型推导和非法操作。
  • 代码实现:
    template<class T>
    class vector
    {
    public:
    	typedef T* iterator;
    	typedef const T* const_iterator;
    
    	vector()
    	{}
    
    	vector(size_t n, const T& val = T())
    	{
    		reserve(n);
    		for (size_t i = 0; i < n; ++i)
    		{
    			push_back(val);
    		}
    	}
    
    	vector(int n, const T& val = T())
    	{
    		reserve(n);
    		for (int i = 0; i < n; ++i)
    		{
    			push_back(val);
    		}
    	}
    
    	// [first, last)
    	template <class InputIterator>
    	vector(InputIterator first, InputIterator last)
    	{
    		while (first != last)
    		{
    			push_back(*first);
    			++first;
    		}
    	}
    
    private:
    	iterator _start = nullptr;
    	iterator _finish = nullptr;
    	iterator _end_of_storage = nullptr;
    };
  •  测试:

使用memcpy拷贝问题

假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?

//错误写法的reserve
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
            //memcpy对自定义类型 T 进行浅拷贝
			memcpy(tmp, _start, sizeof(T)*size());
			delete[] _start;
		}

		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

void test_vector()
{
	stl::vector<std::string> v;
	v.push_back("1111");
	v.push_back("2222");
	v.push_back("3333");
}

问题分析:

  • 1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  • 2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

拷贝构造:vector(const vector<T>& v)

1.浅拷贝的危害

如果没有自定义vector类的拷贝构造函数,编译器会自动生成默认拷贝构造函数。该默认拷贝构造函数对于内置类型成员变量执行按位拷贝(浅拷贝),对于自定义类型成员变量则会调用其拷贝构造函数来完成初始化。浅拷贝在vector类中会带来严重问题,因为vector内部通过指针管理动态内存(如_start_finish_end_of_storage指针),浅拷贝只是简单复制指针值,这会导致多个vector对象指向同一块动态内存。当这些对象生命周期结束,析构函数被调用时,同一块内存可能会被多次释放,从而引发程序崩溃。

2.深拷贝拷贝构造函数

2.1.传统写法(正确代码)

(1)写法1

注意:下面这段拷贝构造函数属于传统写法。

  • 传统写法的核心思路是先开辟与原容器同样大小(或根据容量开辟)的空间,然后逐个拷贝原容器中的元素 。代码中reserve(v.capacity());语句复用reserve函数来开辟空间,替代了直接使用new来分配内存;通过for循环结合push_back将原容器v中的元素逐个插入到新构造的vector中,完成了数据的拷贝,符合传统写法的特征。
  • 与传统写法相对的现代写法,通常是利用迭代器区间初始化构造一个临时对象,然后将临时对象与当前对象进行交换 。
//注意:3个指针成员变量的初始化都是用成员变量声明的缺省值nullptr进行初始化的。

//传统写法的深拷贝
vector(const vector<T>& v)
{
	//复用reserve开空间来代替用new开空间、指向新空间
	reserve(v.capacity());//注意:这里也可以使用resize来开空间。
	//复用push_back插入数据来代替拷贝数据
	for (auto e : v)
	{
		push_back(e);
	}
}

(2)写法2

//注意:3个指针成员变量的初始化都是用成员变量声明的缺省值nullptr进行初始化的。

//更加传统的写法
//拷贝构造函数,用于创建一个新的 vector 对象,该对象是另一个同类型 vector 对象 v 的副本
vector(const vector<T>& v)
{
    //为新的 vector 对象分配内存,大小为原 vector 对象 v 的容量
    //使用 new[] 动态分配一个能容纳 v.capacity() 个 T 类型元素的数组
    //并将起始地址赋值给 _start 指针
    _start = new T[v.capacity()];

    //遍历原 vector 对象 v 中的每个元素
    for (size_t i = 0; i < v.size(); ++i)
    {
        //将原 vector 对象 v 中第 i 个元素的值赋给新 vector 对象的第 i 个元素
        //如果 T 是自定义类型,这里会调用 T 类型的赋值运算符重载函数
        _start[i] = v._start[i];
    }

    //计算新 vector 对象中已使用元素的结束位置
    //_finish 指针指向最后一个已使用元素的下一个位置
    //通过 _start 指针加上 v 的元素数量来确定
    _finish = _start + v.size();

    //计算新 vector 对象的内存空间结束位置
    //_end_of_storage 指针指向分配的内存空间的末尾
    //通过 _start 指针加上 v 的容量来确定
    _end_of_storage = _start + v.capacity();
}

注意:上面这段代码模拟实现vector的拷贝构造函数,属于传统写法。

其遵循传统拷贝构造的步骤:

  • 开空间_start = new T[v.capacity()];使用new动态分配了与原vectorv)容量相同大小的空间,_start指向新分配空间的起始位置 。
  • 拷贝数据:通过for循环遍历原vector中的有效元素,将每个元素从原vector的起始位置v._start拷贝(注:其本质是赋值即调用operator=完成拷贝)到新分配空间对应的位置_start 。
  • 更新指针:更新了模拟vector内部表示有效元素结束位置的_finish指针和表示存储空间结束位置的_end_of_storage指针,保证新构造的vector内部状态正确。

这种写法手动管理内存,逐个元素拷贝,是传统的实现方式。

2.2.常见传统方式的错误写法

(1)写法1(错误代码,原因:使用memcpy拷贝数据时,memcpy对于自定义类型 T 执行浅拷贝)

vector(const vector<T>& v)
{
    reserve(v.capacity());//注意:reserve只是开自己容器的空间
    memcpy(_start, v._start, sizeof(T)*v.size());//把其他容器的数据拷贝到自己容器中
    //拷贝完数据之后,必须单独处理_finish
    _finish = _start + v.size();
    //注意:在拷贝完数据之后一定让_finish重写指向最后一个有效数据的下一个位置
}

(2)写法2(错误代码,原因:使用memcpy拷贝数据时,memcpy对于自定义类型T执行浅拷贝)

vector(const vector<T>& v)
{
    _start = new T[v.capacity()];
    memcpy(_start, v._start, sizeof(T)*v.size());
    _finish = _start + v.size();
    _end_of_storage = _start + v.capacity();
}

测试: 

  • ①测试1:T 是内置类型
  • 解析:vector存储的数据l类型 T 是int类型时,上述错误的拷贝构造函数不会出现问题。这是因为int是内置类型,其数据存储在固定大小的内存区域中,不涉及动态内存分配。memcpy函数按字节复制int类型的数据可以正确地实现拷贝功能,不会引发内存管理方面的冲突。即使在v1v2析构时,不会出现内存错误,因为int类型的内存管理相对简单,不存在共享动态内存的问题。
  • ②测试2:T 是自定义类型
  • 解析:vector存储的数据是string类型(自定义类型,内部涉及动态内存管理)时,会出现问题。string类内部通过动态分配内存来存储字符串内容。当使用错误的拷贝构造函数(依赖memcpy进行浅拷贝)创建新的vector对象时,新对象和原对象中的string元素会共享同一块动态内存。即在v3v4析构时,由于它们的string元素共享同一块内存,同一块内存会被多次释放,导致程序崩溃。具体来说,string对象在析构时会释放其内部动态分配的字符串内存,而浅拷贝使得多个string对象指向同一块内存,多次释放就会破坏内存结构,引发运行时错误。

(3)传统方式的正确写法

vector(const vector<T>& v)
{
	_start = new T[v.capacity()];

	for (size_t i = 0; i < v.size(); ++i)
	{
		//当 T 是自定义类型时,则此时_start[i] = v._start[i]这里调用的是自定义
		//类型 T 的深拷贝赋值重载函数operator=来完成对自定义类型 T 的深拷贝
		_start[i] = v._start[i];//调用赋值深拷贝构造
        //注意:1.若是用内存池分配空间,则要调用定位new(即用 定位new 来调用自定义类型 T 的
        //默认构造函数 自定义类型 T 对进行初始化) + 加深拷贝赋值重载.
        //2.赋值重载函数是对已有的对象进行赋值操作。执行赋值时左右操作数都是已有的对象
	}

	_finish = _start + v.size();
	_end_of_storage = _start + v.capacity();
}

代码解析:该正确的拷贝构造函数首先为新的vector对象动态分配与原vector对象v相同容量的内存空间,确保有足够的空间来存储拷贝的元素。然后,通过一个for循环遍历原vector对象v中的每一个元素。对于每个元素v._start[i],使用赋值操作_start[i] = v._start[i];将其复制到新vector对象的对应位置_start[i]。当T是自定义类型时,这个赋值操作会调用自定义类型T的赋值重载函数operator=。只要自定义类型T的赋值重载函数正确实现了深拷贝逻辑(例如,对于自定义类型内部的指针成员,重新分配内存并复制指针所指向的内容),就能保证每个元素在新vector对象中都有自己独立的内存空间,从而实现了深拷贝。最后,正确设置_finish_end_of_storage指针,使其分别指向新vector对象中有

测试:

2.3.现代写法(正确代码)
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}

//现代写法
vector(const vector<T>& v)//或者写成vector(const vector& v),但是不建议这样写。
{
	vector<T> tmp(v.begin(), v.end());
	swap(tmp);//注:这里不能使用memcpy把临时对象tmp的指针成员拷贝给当前对象*this,因为
              //临时对象tmp出拷贝构造函数作用域之后会调用析构函数释放自己占用的资源,
              //则此时当前对象*this的指针成员就会变成野指针,所以正确做法是使用swap
              //来交换临时对象tmp和当前对象*this的指针成员。
}

现代写法的思路:通常是利用迭代器区间初始化构造一个临时对象,然后将临时对象与当前对象进行交换 。

代码解析:先创建一个临时vector对象tmp,使用迭代器区间构造函数将原vector对象v的元素拷贝到tmp中(此过程会调用正确的拷贝逻辑)。然后通过自定义的swap函数,交换当前对象和临时对象tmp的指针成员。这样当前对象就拥有了tmp的资源,而tmp在析构时会释放原对象的资源,从而实现了深拷贝,同时这种写法更加简洁和高效。

2.4.对比stringvector的拷贝构造函数

(1)string的拷贝构造string类存储的数据类型是char(内置类型),虽然string类内部管理动态内存来存储字符串,但由于char类型本身是简单的字节数据,其拷贝操作相对直接。string的拷贝构造函数可以使用strcpy等函数来拷贝数据,因为char类型的按字节拷贝不会引发复杂的动态内存管理问题。

例如,string s1 = "example";string s2(s1);string的拷贝构造函数会正确地复制char数组内容到新的string对象中,并且独立管理其动态内存,不会出现共享内存导致的错误。

(2)vector的拷贝构造vector类的拷贝构造相对复杂,因为它可能存储各种类型的元素,包括自定义类型。当存储自定义类型时,自定义类型可能涉及动态内存分配。memcpy函数执行的浅拷贝无法正确处理自定义类型内部的动态内存,会导致多个vector对象中的自定义类型元素共享同一块动态内存。

因此,vector的拷贝构造函数不能简单地使用memcpy对自定义类型T执行浅拷贝来完成拷贝数据。而是应该调用自定义类型T的深拷贝的赋值重载函数operator=,确保自定义类型的每个元素在新vector对象中都有独立的内存空间,从而实现整个vector对象的深拷贝,避免因共享内存而引发的内存错误和程序崩溃。

reserve

1.错误代码

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
            //memcpy对自定义类型 T 进行浅拷贝
			memcpy(tmp, _start, sizeof(T)*size());
			delete[] _start;
		}

		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

①错误原因
reserve函数的作用是调整vector的容量,确保它至少能够容纳n个元素。当需要扩容时(即n > capacity()),上述代码使用memcpy函数将旧空间中T类型的数据拷贝到新空间。然而,当模板类型参数T是自定义类型且涉及动态内存分配(例如自定义类型中有指针成员指向动态分配的内存)时,memcpy执行的是浅拷贝。这意味着它只是简单地按字节复制数据,包括指针的值,但不会复制指针所指向的动态内存。

在拷贝完成后,自定义类型中涉及动态内存分配的指针仍然指向旧空间。随后,旧空间被释放(delete[] _start;),这些指针就变成了野指针。当vector对象生命周期结束,调用析构函数~vector时,会自动调用T自定义类型的析构函数来释放T所占用的资源。但此时由于指针是野指针,访问这些指针会导致程序崩溃。

②例如:对于vector<std::string> v3("11111111111111111111");,当使用上述reserve函数对v3进行扩容时,memcpy浅拷贝string对象(string内部涉及动态内存管理),在析构时就会直接发生程序崩溃。因为string对象的内部指针在浅拷贝后指向了已释放的旧空间,在析构时访问这些指针会引发错误。

2.正确代码

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			for (size_t i = 0; i < sz; ++i)
			{
                //调用自定义类型 T 自己的深拷贝赋值重载函数完成拷贝数据的操作
				tmp[i] = _start[i];
			}
			delete[] _start;
		}

		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

代码解析:正确代码先为新的空间分配内存(T* tmp = new T[n];)。然后,通过循环逐个元素地将旧空间中的元素赋值到新空间(tmp[i] = _start[i];)。当T是自定义类型时,这个赋值操作会调用自定义类型T的赋值重载函数。如果自定义类型T的赋值重载函数实现了深拷贝(即对内部动态分配的内存也进行了正确的拷贝),那么就可以确保每个元素在新空间中都有独立的内存,避免了野指针的问题。在完成元素拷贝后,释放旧空间(delete[] _start;),并更新_start_finish_end_of_storage指针,使其正确指向新的内存空间和元素范围。

测试:

赋值重载:vector<T>& operator=(vector<T> v)

传统写法深拷贝构造函数

//传统写法的深拷贝拷贝构造函数
vector(const vector<T>& v)
{
    _start = new T[v.capacity()];
    for (size_t i = 0; i < v.size(); ++i)
    {
        //当 T 是自定义类型(成员变量涉及动态内存分配)时,则此时的赋值操作就是调用
        //自定义类型 T 的深拷贝的赋值重载函数operator=来完成赋值操作
        _start[i] = v._start[i];
    }
    _finish = _start + v.size();
    _end_of_storage = _start + v.capacity();
}

杨辉三角代码实现

118. 杨辉三角 - 力扣(LeetCode)

#include <vector>
using namespace std;

//传值返回
vector<vector<int>> generate(int numRows) 
{
    //定义外层对象
    vector<vector<int>> vv;
    //用resize把需要的numRows行(个)vector<int>对象的空间给开辟出来了,初始化为空的vector<int>对象
    vv.resize(numRows, vector<int>()); 

    //用operator[]遍历 vv 类对象来控制它的每一行
    for (size_t i = 0; i < vv.size(); ++i)
    {
        //继续用resize把每一行需要的 i + 1 个int类型数据的空间给开辟出来了,并初始化为0
        vv[i].resize(i + 1, 0); 

        //每一行的第一个 和 最后一个 都初始化为1
        vv[i][0] = vv[i][vv[i].size() - 1] = 1; 
    }

    //遍历二维数组填充杨辉三角中间的元素
    //从第二行开始(i = 1),因为第一行只有一个元素,不需要计算
    for (size_t i = 1; i < vv.size(); ++i)
    {
        for (size_t j = 1; j < vv[i].size() - 1; ++j)
        {
            //杨辉三角中间元素的值等于上一行相同列和前一列元素之和
            vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
        }
    }

    //调用深拷贝构造函数进行传值返回
    return vv; 
}

//总结:在结构上和使用上, vector<vector<int>> vv 好像是个二维数组,
//但是实际上 vector<vector<int>> vv是个对象数组(即存放vector<int>对象的数组),
//里面的vector<int>对象又指向一个存放int类型数据的数组。

1.案例:利用杨辉三角来说明浅拷贝的赋值重载函数的危害

注:深拷贝的拷贝构造函数使用传统写法,赋值重载函数使用编译器自动生成的默认赋值重载函数operator=(执行浅拷贝)。

(1)案例描述

在 C++ 编程中,当涉及到复杂数据结构的传递时,对象的拷贝操作显得尤为重要。这里以两层嵌套的vector对象为例,当定义一个两层嵌套vector的对象ret去接收由函数返回的两层嵌套vector对象vv时,会触发拷贝机制。在这种情况下,通常会调用深拷贝构造函数来创建临时变量,以便完成返回操作。然而,对于这种具有多层嵌套结构的vector对象,传统的深拷贝构造函数存在局限性。它仅能确保第一层vector的深拷贝,而对于第二层vector,却只能实现浅拷贝(注:传统写法的深拷贝的拷贝构造函数内部使用编译器自动生成的默认赋值重载函数来执行浅拷贝)。这种不完全的深拷贝可能会在后续的程序运行中引发严重的问题。

(2)生成杨辉三角前5行的动态二维数组的图形解析

std::vector<std::vector<int>> vv;   vv.resize(n, std::vector<int>()); 构造一个vv动态二维数组,vv中总共有n个元素,每个元素都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:

vv中元素填充完成之后,如下图所示:

(3)测试用例

具体的测试场景是利用两层vector对象ret来接收计算杨辉三角结果的两层vector对象vv,对应的代码为vector<vector<int>> ret = Solution().generate(5); ,其中generate函数负责生成包含杨辉三角数据的vv对象。需要着重指出的是,在这个测试环境中,虽然已经正确实现了vector的深拷贝构造函数,但在处理赋值操作时,并没有自定义实现深拷贝赋值重载函数,而是依赖编译器自动生成的默认赋值重载函数。而默认的赋值重载函数执行的是浅拷贝操作,这正是后续问题产生的根源。

(3)程序崩溃原因

  • ①拷贝过程剖析:当执行vector<vector<int>> ret = Solution().generate(5);语句时,首先调用的是传统写法的深拷贝构造函数。该构造函数通过new操作符为新对象分配内存空间,从而成功解决了ret第一层vector的深拷贝问题,即第一层vector中的指针成员指向了独立分配的内存区域。然而,对于第二层vector,由于缺乏自定义的深拷贝赋值重载函数,编译器生成的默认赋值重载函数开始起作用。默认赋值重载函数仅仅是简单地复制指针值,而不会复制指针所指向的内存内容,这就导致了第二层vector的浅拷贝现象。
  • ②内存地址分析:通过对内存地址的详细观察和对比,可以清晰地看到:第一层vector实现了深拷贝,表现为vv._startret._start的内存地址不同,这意味着它们分别拥有独立的内存空间来存储指向第二层vector的指针。然而,第二层vector却呈现出浅拷贝的特征,即vv[i]._startret[i]._start的地址相同,这表明它们共享同一块内存空间来存储具体的数据元素。
  • ③崩溃原因总结:当vv对象超出其作用域时,它所占用的内存资源会被释放,其中包括第二层vector所指向的内存。此时,vv[i]._start指针变为野指针。随后,当ret对象也超出作用域并尝试释放资源时,由于其第二层vector的指针与vv的指针指向相同的已释放内存区域,在析构函数执行过程中,就会因为对野指针的非法访问而导致程序崩溃。简而言之,程序崩溃的根本原因在于第二层vector的浅拷贝,而这又归咎于vector类未能提供自定义的深拷贝赋值重载函数。

2.深拷贝赋值重载函数operator=的实现

(1)写法1:传值传参,调用拷贝构造函数完成传参

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
//深拷贝赋值重载函数
//写法1:传值传参
//由于赋值重载函数使用传值传参,则形参v是实参的临时拷贝。
//当形参v出了赋值重载函数的作用域之后就会销毁,则我们可以
//通过复用交换函数swap来交换*this 和 形参v的所有成员变量,
//从而实现深拷贝的赋值重载函数。
//注意:通过交换两个对象的所有成员变量的值来实现深拷贝
//的赋值重载函数的好处是不用自己用new开空间、拷贝数据,指向新空间。

//vector& operator=(vector v);//注:若赋值重载函数在模板类中实现,
//则可以不用加模板参数列表<T>。但是最好不要这样做,建议加上模板参数列表<T>。
//因为vector<T>表示具体类型,更易我们理解代码。
//注:std::vector提供的深拷贝构造函数std::vector(const vector& x);
//就是没有加类型参数列表<T>。因为语法允许。
//std::vector(const vector& x)可以写成std::vector(const vector<T>& x).
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}
  • 代码解析: :该赋值重载函数采用传值传参的方式,当形参v传入函数时,会调用拷贝构造函数创建一个临时对象(即实参的拷贝)。然后通过自定义的swap函数交换*this对象和形参v的所有成员变量(_start_finish_end_of_storage)的值。这样,*this对象就拥有了原来v对象的资源,而原来*this对象的资源则被转移到了即将析构的形参v中。由于形参v是实参的拷贝,这种方式实现了深拷贝,且不需要手动使用new来开辟新空间和拷贝数据。
  • 注意事项:虽然在模板类中实现赋值重载函数时语法上可以不加模板参数列表<T>,但为了代码的易读性和明确性,建议加上模板参数列表<T> 。

(2)写法2(建议使用):传引用传参,减少拷贝

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
//深拷贝赋值重载函数
//写法2:传引用传参
//由于赋值重载函数使用传引用传参,则形参v是实参的别名,
//则改变形参就会改变实参,由于operator=的功能是改变左
//操作数,所以为了防止形参v被修改则形参v要用const修饰。
//由于右操作数v不能修改,则我们通过调用形参v的迭代器区间
//来构造临时对象tmp,则我们就可以复用交换函数swap来
//交换*this 和 临时对象tmp的所有成员变量来完成深拷贝的
//赋值重载函数。
//注意:通过交换两个对象的所有成员变量的值来实现深拷贝
//的赋值重载函数的好处是不用自己用new开空间、拷贝数据,指向新空间。
vector<T>& operator=(const vector<T>& v)
{
	vector<T> tmp(v.begin(), v.end());
	swap(tmp);
	return *this;
}
  • 代码解析:此赋值重载函数采用传引用传参的方式,形参v是实参的别名,避免了一次额外的拷贝构造。函数内部先创建一个临时vector对象tmp,并通过迭代器区间构造函数vector<T>(v.begin(), v.end())v的元素深拷贝到tmp中。然后通过swap函数交换*this对象和临时对象tmp的成员变量,使得*this对象拥有v的资源,实现深拷贝。
  • 注意事项:由于operator=的功能是改变左操作数,为了防止形参v被修改,形参v使用const修饰。同时,这种方式同样不需要手动使用new来开辟新空间和拷贝数据,通过交换对象成员变量的值来完成深拷贝。

std::sort函数模板使用介绍

sort - C++ Reference (cplusplus.com)

(1)std::sort 函数模板概述

std::sort是 C++ 标准库中的一个函数模板,用于对指定范围内的元素进行排序。其模板类型参数RandomAccessIterator要求传入的迭代器是随机访问迭代器。随机访问迭代器的特性决定了std::sort函数适用于对底层是数组(即具有连续物理空间)的数据结构进行排序,而不能直接对链表进行排序,因为链表不支持随机访问迭代器所需的操作。

(2)函数原型及参数说明

std::sort函数有两种常见形式:

  • 形式1template <class RandomAccessIterator> void sort (RandomAccessIterator first, RandomAccessIterator last);
  • 形式2template <class RandomAccessIterator, class Compare> void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

参数解释

  • first:随机访问迭代器,指向要排序序列的起始位置,排序范围是[first, last),即包含first指向的元素,但不包含last指向的元素。
  • last:随机访问迭代器,指向要排序序列的结束位置。
  • comp(可选):二元谓词函数,用于指定排序的比较规则。该函数接受两个参数,当第一个参数应该排在第二个参数前面时,返回true,否则返回false。如果不提供此参数,默认使用小于号<作为比较规则,即进行升序排序。

(3)排序规则说明

  • 当使用std::sort排序时,如果不指定仿函数comp,默认的仿函数是<,即按照升序排列元素。
  • 如果希望进行降序排序,可以使用greater<int>仿函数。该仿函数定义了大于号>的比较规则,使得元素按照从大到小的顺序排列。

(4)使用 std::sort 对 stl::vector 进行排序

void test_vector()
{
    int a[] = { 4, 5, 1, 3, 2 };
    vector<int> v1(a, a + 5);
    for (auto e : v1)
    {
        cout << e << " ";
    }
    cout << endl;

    //使用迭代器区间进行排序:升序
    sort(v1.begin(), v1.end());

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

    //降序写法1
    greater<int> g;
    sort(v1.begin(), v1.end(), g);
    for (auto e : v1)
    {
        cout << e << " ";
    }
    cout << endl;

    //降序写法2
    sort(v1.begin(), v1.end(), greater<int>());
    for (auto e : v1)
    {
        cout << e << " ";
    }
    cout << endl;

    //使用指向数组的原生指针区间进行排序:升序
    sort(a, a + sizeof(a) / sizeof(int));
    for (auto e : a)
    {
        cout << e << " ";
    }
    cout << endl;

    //降序写法1
    sort(a, a + sizeof(a) / sizeof(int), g);
    for (auto e : a)
    {
        cout << e << " ";
    }
    cout << endl;

    //降序写法2
    sort(a, a + sizeof(a) / sizeof(int), greater<int>());
    for (auto e : a)
    {
        cout << e << " ";
    }
    cout << endl;
}

代码解析:在上述代码中,首先创建了一个vector对象v1并初始化,然后分别展示了使用std::sort对vector进行升序和降序排序的不同方式。升序排序直接调用sort(v1.begin(), v1.end()),因为没有指定比较函数,默认采用升序规则。降序排序则通过引入greater<int>仿函数来实现,有两种写法:一种是先定义greater<int>对象g,再将其作为参数传入sort函数;另一种是直接使用greater<int>()匿名对象作为参数。

同时,代码中还展示了对普通数组进行排序的方法,原理与对vector排序类似,通过计算数组元素个数确定排序范围,使用原生指针作为迭代器传入std::sort函数。

测试: 

vector模板类模拟实现的整个工程

//vector.h
//注意:类模板的声明和定义一定不要分离因为会发生链接错误,声明和定义最好都放在
//头文件中,所以这里我在头文件中实现vector模板类。

#pragma once
#include<assert.h>
#include <algorithm>
namespace stl
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//默认构造函数
		vector()
		{}

		//vector<int> v(10, 5);
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}

		//带参构造函数:用n个val构造
		vector(int n, const T& val = T())
		{
			reserve(n);
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}

		//迭代器区间构造函数
		// [first, last) 左闭右开
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//深拷贝的拷贝构造函数
		//传统写法
		//写法1:
		/*vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (auto e : v)
			{
				push_back(e);
			}
		}*/
		//写法2:
		/*vector(const vector<T>& v)
		{
			_start = new T[v.capacity()];
			for (size_t i = 0; i < v.size(); ++i)
			{
				_start[i] = v._start[i];
			}

			_finish = _start + v.size();
			_end_of_storage = _start + v.capacity();
		}*/

		//现代写法
		vector(const vector<T>& v)
		{
			vector<T> tmp(v.begin(), v.end());
			swap(tmp);
		}

		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

		//深拷贝的赋值重载函数
		//写法1:传值传参,调用拷贝构造函数完成传参
		/*vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}*/

		//写法2:(建议使用):传引用传参,减少拷贝
		vector<T>& operator=(const vector<T>& v)
		{
			vector<T> tmp(v.begin(), v.end());
			swap(tmp);
			return *this;
		}

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

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

		void resize(size_t n, T val = T())
		{
			if (n < size())
			{
				//删除数据
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())
					reserve(n);

				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					for (size_t i = 0; i < sz; ++i)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;
				}

				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}

			*_finish = x;
			++_finish;
		}

		void pop_back()
		{
			assert(!empty());

			--_finish;
		}

		iterator insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);

			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);

				//扩容后更新pos,解决pos失效的问题
				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}

			*pos = val;
			++_finish;

			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

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

			--_finish;

			return pos;
		}

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

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

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

		T& operator[](size_t pos)
		{
			assert(pos < size());

			return _start[pos];
		}

		const T& operator[](size_t pos) const
		{
			assert(pos < size());

			return _start[pos];
		}

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

相关文章:

  • JVM垃圾回收面试题及原理
  • 代码随想录二刷|图论4
  • 实现一个日期类(类和对象实践项目)
  • 使用 potrace.js实现图像矢量化教程
  • Windows控制台函数:标准输入输出流交互函数GetStdHandle()
  • 基于Spring Boot的城市垃圾分类管理系统的设计与实现(LW+源码+讲解)
  • 使用 Python 开发的简单招聘信息采集系统
  • 人工智能里的深度学习指的是什么?
  • Next.js 的基本了解
  • 【工具使用】IDEA 社区版如何创建 Spring Boot 项目(详细教程)
  • 蓝耘赋能通义万相 2.1:用 C++ 构建高效 AI 视频生成生态
  • CSS定位布局-五个定位实现自由布局(Static, Relative, Absolute, Fixed, Sticky)
  • 力扣刷题DAY8(动态规划)
  • C/C++实现显微镜玻片球状细胞识别与计数
  • 计算机组成原理(第三章 存储系统)
  • 【自学笔记】R语言基础知识点总览-持续更新
  • 爬虫案例六用协程爬取趣笔阁
  • 13.【线性代数】——复习课
  • MyBatis增删改查:静态与动态SQL语句拼接及SQL注入问题解析
  • 如何选择开源向量数据库
  • 手机网站建设的企业/竞价代运营外包公司
  • 优化网站的目的/域名查询ip爱站网
  • 临朐门户网站/下载百度2023最新版安装
  • 西安网站开发有哪些公司/域名交易
  • 毕业设计代做网站jsp/地推是什么
  • 动态网站开发作业/主要推广手段免费