vector

在前面我们已经学习过string的模拟实现了,这里简单说一下两者的区别和联系。

  • vector和string的区别和联系

区别:

1、string有\0结尾。

2、vector和string比较大小的逻辑会有一定的差异:string使用ASCII进行比较,而vector可能会使用长度直接进行比较。

3、string 类型实际上可以看做是对字符序列的高级封装,它提供了许多方便的字符串操作方法,比如查找、替换、连接等,而且可以直接进行输入输出操作。

4、在模拟实现时,string中的_str总是要留出一个位置给\0,而vector不需要。

联系:

  1. 存储数据:std::vector 是一个能够存储任意类型的动态数组,而 std::string 则专门用于存储字符串,即字符序列。
  2. 动态大小:两者都可以动态地增加或减少其内部存储的元素,无需在编写代码时指定固定大小。
  3. 底层实现:在很多标准库的实现中,std::string 实际上就是使用 std::vector 来实现的,因此在内部结构上它们可能是类似的。两者底层结构都是顺序表。

一、vector的结构

vector 的底层就是一个动态的顺序表,其大小是可以动态改变的。

注意这里的动态改变通常是指当存储数据的空间不足时,它会自动扩容;当我们删除数据时,我们是不会进行缩容的,因为缩容的代价比较大。(这里我们在模拟实现string中讲到了)。

既然需要动态改变这个数组的大小,所以vector需要有一个指针指向空间起始位置 _start  ,有一个指针指向这块空间的末尾 _end_of_storage ;同时为了删除和增加数据方便还需要有一个指针指向数据的末尾 _finish .

private:
		iterator _start; // 指向第一个元素
		iterator _finish; // 指向最后一个元素的后一个地方(左闭右开)
		iterator _end_of_storage; // 指向这块空间的后一个位置

这里的iterator是一个迭代器,我们使用指针来进行的一个宏定义。

// 迭代器
typedef T* iterator;
typedef const T* const_iterator;

二、construct

先来看一下常用的构造函数模拟实现方法:

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

vector(size_t n, const T& val = T())
	:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
	reserve(n);
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

template 
vector(InputIterator first, InputIterator last)
	:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

第一种就是无参的构造函数,可以直接使用初始化列表对各个成员变量进行初始化,这种初始化的vector里边的内容是空的。

第二种,创建一个空间大小为n、并且使用一个初始值val对这块空间进行初始化的vector。

第三种,使用迭代器进行初始化。

需要注意的是,因为push_back()操作是需要使用到_start和_finish的,所以在进行push_back之前还要对这些成员变量进行初始化。

1、带参构造函数

对于 vector(size_t n, const T& val = T()) 类型的构造构造来说,有几个要注意的点:

1)缺省值

这里使用的 const T& val = T() ,还需要介绍一下:

首先,T() 相当调用一个默认构造函数,如果这个vector中存储的是一个Student自定义类型的对象,这行代码就相当于  const Student& val = Student();   这里使用 Student() 无参构造函数构建一个匿名对象,并将这个匿名对象取了一个别名 val,此时val的值就是这个匿名对象。

需要注意的是,对于自定义类型 T() 会调用该自定义类型的默认构造函数;对于几种基本类型来说,基本类型是没有默认构造函数的,但是为了适配这个场景下的用法,编译器是会为基本类型创建一个默认构造函数的。

int a = int();
double b = double();
cout << "a = " << a << endl;
cout << "b = " << b << endl;

vector_第1张图片

2)const延长匿名对象的声明周期

class A
{
public:
	A()
	{
		cout << "A()被调用" << endl;
	}
	~A()
	{
		cout << "~A()被调用" << endl;
	}
};

int main()
{
	A a1;
	A();
	A a3;
	return 0;
}

vector_第2张图片

在没有const修饰时,匿名对象的声明周期只在它定义的那一行(因为在这一行之后不会再使用这个对象了),这一行过后这个匿名对象就会被销毁,析构函数被调用。

int main()
{
	A a1;
	const A& a2 = A();
	A a3;
	return 0;
}

vector_第3张图片

如果使用const修饰匿名对象,就能够将这个匿名对象的周期延长至a2的生命周期(其实就相当于为这个匿名对象起一个名字后,它的声明周期就被延长了)。

注:能够延长声明周期是因为加了const关键字,而不是引用的作用。

int main()
{
	A a1;
	A& a2 = A();
	A a3;
	return 0;
}

如果这样使用会发生以下报错:

这是因为匿名对象和临时对象具有常性

3)调用优先顺序问题 

在利用 vector v1(10, 5); 构造一个vector时,可能会出现错误。

vector(size_t n, const T& val = T())
	:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
	reserve(n);
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

template 
vector(InputIterator first, InputIterator last)
	:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

void test4_vector()
{
	vector v1(10, 5);

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

这是因为,在 v1(10, 5) 进行构造函数时,10和5都是int整形的,所以在调用构造函数时,会调用

vector(InputIterator first, InputIterator last)-----vector(int first, int last),然后内部会对int进行解引用(*first),发生错误。

而预期上,我们是想要调用 vector(size_t n, const T& val = T()),但是因为 n 是 size_t 的,由整形 int 10需要进行强制类型转化才能被 n 接收,而使用上面一个构造函数vector(int first, int last)是不需要进行强制类型转化的,所以会调用上面一个构造函数。

至于改进方法,可以利用函数重载或将 n 的实参设为 unsigned int 类型:

1、函数重载

vector(int n, const T& val = T())
		:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
	{
		reserve(n);
		for (int i = 0; i < n; i++)
		{
			push_back(val);
		}
	}

2、实参类型使用unsigned int

void test4_vector()
{
	vector v1(10u, 5);

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

2、使用迭代器的构造函数

在vector进行初始化时,可以使用迭代器进行初始化,并且这个迭代器并不一定非要是vector的迭代器,也可以是其他类型的迭代器,只要能保证数据类型匹配就行了。

下面这个在v进行初始化时传递的是 string 的迭代器( s1.begin(), s1.end() ),因为字符可以转化成ASCII码,ASCII是整形的,所以可以这样使用,如:

int main()
{
	string s1("abcdefg");
	vector v1(s1.begin(), s1.end());
	for (auto n : v1)
	{
		cout << n << " ";
	}
	cout << endl;
	vector v2(s1.begin()+2, --s1.end());
	for (auto n : v2)
	{
		cout << n << " ";
	}
	cout << endl;
	return 0;
}

vector_第4张图片

结果打印出的字符的ASCII码。

vector和string的迭代器都是随机迭代器,是可以进行++,--,+x

3、拷贝构造函数(两次深拷贝)

先来看一下最终拷贝构造函数的写法:

vector& operator=(vector v)
{
	swap(v);
	return *this;
}
// 第一种
vector(const vector& v)
	: _start(nullptr)
	, _finish(nullptr)
	, _endOfStorage(nullptr)
{
	reserve(v.capacity());
	iterator it = begin();
	const_iterator vit = v.cbegin();
	while (vit != v.cend())
	{
		*it++ = *vit++;
	}
	_finish = it;
}

// 第二种
vector(const vector& 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& v)
	:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
{
	reserve(v.capacity());
	for (const auto& e v)
	push_back(e);
}

从下面开始,我们来看一下,拷贝构造函数是如何一步步写成这样的。

初始版本:

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

这个拷贝构造函数(与string拷贝构造函数实现相同),对于一般情况是正确的。

void test5_vector()
{
	vector v1(10, 5);

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

	vector v2(v1);
	for (size_t i = 0; i < v2.size(); ++i)
	{
		cout << v2[i] << " ";
	}
	cout << endl;
}

vector_第5张图片

但是当在下面这种情况使用这个拷贝构造函数使用时,会出现程序崩溃的错误:

Ⅰ. vector
void test5_vector()
{
	vector v3(3, "111111111111111     ");

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

	vector v4(v3);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
}

图解:

vector_第6张图片

我们发现,vector存储的对象string中,它的字符数组和我们拷贝后vector对象中的字符数组指向同一块空间。

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

这时,在调用析构函数时会出现错误,在delete[] _start; 这一步中,在释放vector每一个元素string时,都会再调用string的析构函数,最后string中_str空间会被释放两次,发生错误。

只要当vector中存储的元素有深拷贝问题时,都会出现上面这个问题。

改进:

memcpy是浅拷贝,所以我们不能再使用memcpy进行拷贝vector内部的数据

因为vector内部元素是 string,而在实现string中我们知道:operator=是深拷贝,所以可以采用循环 + operator= 的方式进行拷贝vector内部的数据。

vector(const vector& 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_第7张图片

vector_第8张图片

还需要注意的是,只要是有拷贝过程都要考虑深浅拷贝的问题,不仅仅是在拷贝构造中。

Ⅱ. reserve

在扩容时,我们开辟一块指定大小空间后,还要将原先数据拷贝到这块新空间中……

所以,扩容操作也注意不能使用memcpy进行拷贝,而应该使用赋值操作进行拷贝:

vector_第9张图片

void reserve(size_t n)
{
	// 判断n与capacity的关系,避免缩容
	if (n > capacity())
	{
		T* temp = new T[n];
		if (_start) // 如果_start为空,就不进行拷贝
		{
			// 深拷贝问题
			//memcpy(temp, _start, sizeof(T) * size());
			for (size_t i = 0; i < size(); i++)
			{
				temp[i] = _start[i];
			}
			delete[] _start;
		}

		size_t sz = size();
		_start = temp;
		_finish = _start + sz; // temp + (_finish - _start);
		_end_of_storage = _start + n;
	}
}

这样,就解决了vector中存放string对象的拷贝构造函数了。

那么如果vector中存放的是vector对象的拷贝构造函数又怎么实现呢?

Ⅲ. vector>

同样的道理,我们还是不能使用memcpy进行拷贝数据(无论是  vector外壳的成员变量  还是 内部元素的成员变量 ),拷贝内部元素时采用   循环 + operator=    的方式进行拷贝。


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

vector& operator= (vector v) // 注意这里不能使用&
{
	swap(v);
	return *this;
}

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

	_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_第10张图片

这里operator=实现深拷贝的操作是直接交换两块空间的指针,而保持内容不变,这样效率比较高。

需要注意的是,如果我们自己没有进行operator=重载为深拷贝,编译器自己生成的operator=是浅拷贝,所以对于vector>类型的类,我们需要实现一个深拷贝的operator=的重载。

Ⅳ. 拷贝构造的现代写法
void swap(vector& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}

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

vector(const vector& v)
{
	vector temp(v.begin(), v.end());
	swap(temp);
}

分析:

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

int main()
{
	vector s1(5);
	vector s2;
	s2 = s1;
	return 0;
}

这里s2 = s1;语句会调用operator=操作符重载,并利用拷贝构造函数将s1作为参数传递给v,再利用交换指针的方法,将s2的指针指向v,v的指针指向s2,最后operator=函数调用结束后v指向的空间会被析构函数处理,也就是s1原本的空间会被释放掉。

vector_第11张图片

注意:这里的operator=的参数不能使用引用:如果使用引用接收参数的话,会导致s1和s2的指针发生交换,s1的值会发生变化,不能达到赋值的功能。

三、空间配置器初始化问题

使用空间配置器申请空间时,不会对已申请的空间进行初始化;

如果使用new进行申请空间时,是会对这块空间进行初始化的。(操作 = 开空间+调用构造函数)

对未初始化的空间进行直接赋值操作是一种未定义行为,这意味着它可能会导致程序的不可预测结果。C++编译器不会对未初始化的空间进行任何默认初始化,因此尝试直接对其进行赋值可能会导致访问未定义的内存。

在C++中,为了安全起见,应该始终确保在使用变量之前将其初始化。这可以通过以下几种方式实现:

  1. 在定义变量时进行初始化:

int x = 0; // 初始化为0

在声明变量后立即进行初始化:

使用构造函数进行初始化(适用于类对象):

class MyClass {
public:
    int x;
    MyClass() : x(0) {} // 构造函数初始化
};

MyClass obj; // 通过构造函数进行初始化

使用空间配置器申请空间通常要配合着定位new使用。

四、迭代器失效问题

在vector的插入和删除操作需要用到迭代器,所以我们在讲解迭代器失效时,也会顺便实现一下插入和删除操作。

1)扩容导致迭代器指向的空间被释放。

如果插入函数是以下这种方式实现的,在执行下面一个程序时就会出错:

void insert(iterator pos, const T& val)
{
	assert(pos <= _finish);
	assert(pos >= _start);
	// 判断空间是否足够
	if (_finish == _end_of_storage)
	{
		size_t sz = pos - _finish;
		reserve(capacity() == 0 ? 4 : capacity() * 2); // 扩容
	}
	// 移动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val; // 在pos位置插入目标值
	_finish++;
}

void test2_vector()
{
	vector v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);


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

	auto pos = find(v1.begin(), v1.end(), 3);
	v1.insert(pos, 30); // 在3之前插入30
	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
	cout << v1.size() << endl;

}

vector_第12张图片

该程序没有成功地在3的前面插入30.

分析:

先使用find找到指向指定位置的迭代器后,由于在插入函数reserve()进行了扩容操作导致原先的空间被释放,即pos指向的空间被释放了,pos不在指向3了,之后再利用pos迭代器进行移动数据和*pos = val 操作时就会出现错误。所以在插入函数中进行扩容后要更新pos的位置。

解决方法:

在插入函数内部更新迭代器的位置。

void insert(iterator pos, const T& val)
{
	assert(pos <= _finish);
	assert(pos >= _start);
	// 判断空间是否足够
	if (_finish == _end_of_storage)
	{
		size_t sz = pos - _finish;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _finish + sz; // 更新pos的位置
	}
	// 移动数据
	iterator end = _finish - 1;
	while (end>=pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
}

2)插入数据后,外部迭代器没有更新。

这种情况又分为两种情况:在插入时进行扩容和在插入时没有进行扩容操作。

a)插入时进行扩容操作

void test2_vector()
{
	vector v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);

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

	cout << v1.size() << endl;
	auto pos = find(v1.begin(), v1.end(), 3);
	v1.insert(pos, 30); // 在3之前插入一个30

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

	(*pos)++;
	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
}

vector_第13张图片

这个程序第一步成功地在3的前面插入了30;第二步利用pos迭代器将3进行++操作,但我们发现,3的值并没有被改变。

分析:

在插入之前,空间为4,而此时的数据已经有四个了,所以再进行插入操作时,会进行扩容操作,也就代表着原先的空间会被释放,pos指向的空间被释放了。

虽然在插入中修改pos的位置,可以正确地将30插入到3的前面。但是因为插入函数的迭代器是使用值传递,函数内部更新的迭代器只是形参,外部实参pos的位置没有改变 ,这样外部pos还是指向一块已经被释放的空间,最终导致不仅没有修改pos位置的值,同时还存在野指针解引用的问题。

b)插入时没有进行扩容操作

如果没有进行扩容操作还是会有迭代器失效的问题:由于插入一个数后,迭代器的指向没有动态变化,此时通过迭代器修改的值并不是原先的值。

void test2_vector()
{
	vector v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);


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

	auto pos = find(v1.begin(), v1.end(), 3);
	v1.insert(pos, 30);

	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
	cout << v1.size() << endl;
	(*pos)++;

	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
}

vector_第14张图片

在插入之前,空间有8个,数据只有5个,所以在插入一个数据时不会进行扩容操作。上述代码中在插入一个数据时没有进行扩容,同时迭代器pos指向3,但是在插入30之后,迭代器pos指向30而不再是3,所以修改的值为30,最后30变成31,而3却没有变成4.

c)解决方法

导致这种错误的根源就在于insert内部的改变并不影响外部迭代器的指向。

Ⅰ. 使用引用参数

但是传引用会导致下边这种情况:

// 使用引用接收参数
void insert(iterator& pos, const T& val)
{
	assert(pos <= _finish);
	assert(pos >= _start);
	// 判断空间是否足够
	if (_finish == _end_of_storage)
	{
		size_t sz = pos - _finish;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _finish + sz; // 更新pos
	}
	// 移动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	*pos = val;
	_finish++;
}

iterator begin()
{
	return _start;
}

void test2_vector()
{
	vector v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);


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

	cout << v1.size() << endl;
	v1.insert(v1.begin() + 2, 10);

}

报错信息:

这是因为begin()函数的返回值是传值返回,而传值返回是通过创建一个临时变量返回给接收变量,又因为临时变量具有常性,所以返回值的的类型为const iterator,const iterator 不能传递给iterator pos(权限被放大了),如果将pos参数类型改为const iterator& ,这样就会导致pos指向的内容不能被修改,这就不能进行*pos = val 插入目标值了。

显然这个方法不完美。

Ⅱ. 利用返回值

看一下库里面的vector是怎么实现的。

它通过一个返回值来修改外部的迭代器,这个返回值返回的是指向新插入元素的迭代器。这样如果想要在外部使用迭代器,就需要自己手动地接收这个返回值来更新pos

3)reserve的实现

错误写法:

void reserve(size_t n)
{
	// 判断n与capacity的关系,避免缩容
	if (n > capacity())
	{
		T* temp = new T[n];
		if (_start) // 如果_start为空,就不进行拷贝
		{
			// 拷贝操作……
		}


		_start = temp;
		_finish = _start + size(); // temp + (_finish - _start);
		_end_of_storage = _start + capacity();
	}
}

在该程序中,拷贝操作完成之后,将_start指向新空间的开头,之后利用_start + size更新_finish,这一步就出错了,因为_size = _finish - _start,所以_finish = _start + size(); --> _finish = _start + _finish - _start; 而这里的_finish 还是指向原来空间的数据末尾,所以_finish没有更新成功。

其做法就是先将原来空间中_finish和_start的相对位置记录下来,再利用这个相对位置更新_finish。同理,_end_of_strrage也需要进行同样的操作。

再加上上面拷贝构造函数深拷贝的讲解,可以得到正确写法:

void reserve(size_t n)
{
	// 判断n与capacity的关系,避免缩容
	if (n > capacity())
	{
		T* temp = new T[n];
		if (_start) // 如果_start为空,就不进行拷贝
		{
			// 深拷贝问题
			//memcpy(temp, _start, sizeof(T) * size());
			for (size_t i = 0; i < size(); i++)
			{
				temp[i] = _start[i];
			}
			delete[] _start;
		}

		size_t sz = size(); // 记录相对位置
		_start = temp;
		_finish = _start + sz; 
		_end_of_storage = _start + n;
	}
}

4)insert最终实现

iterator insert(iterator pos, const T& x)
{
	assert(pos <= _finish);

	// 空间不够先进行增容
	if (_finish == _endOfStorage)
	{
		//size_t size = size();
		size_t newCapacity = (0 == capacity()) ? 1 : capacity() * 2;
		reserve(newCapacity);

		// 如果发生了增容,需要重置pos
		pos = _start + size();
	}

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

	*pos = x;
	++_finish;
	return pos;
}

测试:

void test2_vector()
{
	vector v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);


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


	auto pos = find(v1.begin(), v1.end(), 3);
	pos = v1.insert(pos, 30); // 利用返回值更新迭代器

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

	(*pos)++;
	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
}

vector_第15张图片

5)erase实现

void erase(iterator pos)
{
	// 检查位置是否合法
	assert(pos >= _start && pos < _finish);
	iterator start = pos + 1;
	while (start != _finish)
	{
		*(start - 1) = *start;
		start++;
	}
	_finish--;
}

如果如上述方法实现erase(),在删除一个数据后,迭代器同样会失效。

void test2_vector()
{
	vector v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	pos = find(v1.begin(), v1.end(), 5);
	v1.erase(pos);
	(*pos)++;
	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
}

vector_第16张图片

此时,显然通过pos访问5,并进行++是不合法的。并且在VS库中的 vector 是不允许这样使用的,所以我们在实现时也应该使用已经失效的迭代器。

同时,这样可能会导致行为结果未定义--在不同编译器下,同样的代码结果可能会不同:在VS标准库中利用迭代器删除一个数据后,这个迭代器会被强制检查;而在gcc编译器上,是不会进行报错的。

所以erase()也应该设计一个返回值来更新迭代器:


iterator erase(iterator pos)
{
	// 挪动数据进行删除
	iterator begin = pos + 1;
	while (begin != _finish) {
		*(begin - 1) = *begin;
		++begin;
	}

	--_finish;
	return pos;
}

总结:

总结一下,对于插入和删除操作后的迭代器,我们认为都是失效的,所以在插入和删除后尽量不要在使用这个迭代器了,如果还想使用就需要使用函数返回值来更新迭代器。

五、辨析

std::vector::at 和 std::vector::operator[]。

区别:

  1. 返回值类型:std::vector::at 返回的是对应位置的元素的引用,而 std::vector::operator[] 返回的是对应位置的元素的引用或者常引用(取决于 vector 对象是否为 const)。
  2. 异常处理:当指定的位置超出向量的有效范围时,std::vector::at 会抛出 std::out_of_range 异常,而 std::vector::operator[] 不会进行边界检查,如果访问越界则会导致未定义行为。

联系:

  1. 二者都可用于访问 std::vector 的元素,通过指定位置的索引来获取元素的引用。
  2. 它们都允许使用索引访问向量中的元素,提供了对向量元素的直接访问能力。

需要注意的是,在使用 std::vector::operator[] 时,需要自行确保索引的有效性,以避免访问越界导致未定义行为。而 std::vector::at 则在访问越界时提供了一种安全的异常处理机制。因此,在需要对越界情况进行处理时,推荐使用 std::vector::at;如果确定不会发生越界,可以使用 std::vector::operator[] 进行更高效的访问。

注意:

vs系列编译器,debug模式下

at() 和 operator[] 都是根据下标获取任意位置元素的,在debug模式下两者都会去做边界检查。

当发生越界行为时,at 是抛异常,operator[] 内部的assert会触发。

补充:打印中文问题

int main()
{
	vector vstr;

	string s1("张三");
	vstr.push_back(s1); // 第一种插入方法

	vstr.push_back(string("李四")); // 第二种插入方法

	vstr.push_back("王五"); // 第三种插入方法:隐式类型转化

	for (const auto& e : vstr)
		cout << e;
	cout << endl;

	cout << vstr[0][0]; // 只取出了'张'的第一个字节,不能打印出'张'
	return 0;
}

vector_第17张图片

vstr[0] == string s1,vstr[0][0] == string s1[0];并且一个汉字占两个字节,只有将两个字符完整地取出来汉字才能正确的打印出来。

int main()
{
	vector vstr;

	string s1("张三");
	vstr.push_back(s1); // 第一种插入方法

	vstr.push_back(string("李四")); // 第二种插入方法

	vstr.push_back("王五"); // 第三种插入方法:隐式类型转化

	for (const auto& e : vstr)
		cout << e;
	cout << endl;

	cout << vstr[0][0]; // 只取出了'张'的第一个字节
	cout << vstr[0][1] << endl; // 取出了'张'的第二个字节
	return 0;
}

vector_第18张图片

同时,要注意endl的作用:

std::endl 是一个 C++ 标准库中的输出流控制符,它的作用有两个方面:

  1. 换行std::endl 在输出流中插入一个换行符,并刷新输出缓冲区,使数据立即被输出到目标设备(比如屏幕)上。这意味着在使用 std::cout 输出内容时,可以通过插入 std::endl 来实现换行效果。

  2. 刷新缓冲区:除了换行之外,std::endl 还会刷新输出缓冲区。输出缓冲区是为了提高程序的效率而引入的,它会将输出数据先存储在缓冲区中,然后一次性地输出到目标设备上。但有时候我们需要立即将缓冲区的内容输出,比如在程序崩溃或需要立即观察输出结果的情况下。这时,可以使用 std::endl 来强制刷新输出缓冲区,确保数据被立即输出。

使用 std::endl 的语法是在输出流中插入该控制符,例如 std::cout << "Hello" << std::endl;。与 '\n' 不同,std::endl 是一个函数模板,而不是字符常量,这使得它更加灵活和可移植。

需要注意的是,频繁地使用 std::endl 可能会导致性能下降,因为每次插入 std::endl 都会引发一次刷新操作。如果只需要换行而不需要刷新缓冲区,可以使用 '\n' 字符来实现相同的效果,例如 std::cout << "Hello\n";


今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

你可能感兴趣的:(c++)