vector的扩容机制
我们都知道STL中的vector代表的就是一个可以动态扩容的数组,动态扩容也使它非常好用。但是使用之前,我们需要搞清楚它的底层原理,只有在知道它是如何扩容的,我们才能更安心的使用vector来存储我们的自定义类型。
这篇文章将会解答一下三个问题:
- 它怎么实现动态扩容的?
- 它扩容时候调用的是拷贝构造还是移动构造函数?
- 怎么让它调用我们的移动构造函数?
何时扩容
首先需要对vector的底层结构有一个初步的了解,知道vector如何实现之后,我们甚至可以自己写一个vector容器出来。
- vector类本身起始只有三个指针,
_start、_finish、_end_of_storage
,真正存储数据的是一个数组,这三个指针就分别指向这个数组的数据开始的地方,数据结束的地方,和数组的结尾,也就是我们平常使用的begin()、end()、capacity()
会用的东西。 - 然后是vector的迭代器,它的迭代器是所有容器里面最简单,就是原始指针,而其他容器的迭代器因为要实现更多复杂的功能,所以设计成了一个类,比如
deque
的迭代器就很复杂,为了模拟连续存储的数组。 - 那回到问题,什么时候扩容?当然就是当
_start
指针和_end_of_storage
指向了同一个地方,说明数组的空间已经用完了,这个时候就需要扩容了。
如何扩容
-
当数组空间用完了之后,就需要扩容了,但是数组本身无法扩容的,所以vector会申请一块更大的新的内存空间,作为新的数组存储数据。
-
然后再把旧的数组中的数据一个个拷贝过去,再把原来空间释放掉,就完成了一次扩容。
-
具体扩容的倍数,在VS中是两倍,不同编译器不同。
int main() {
vector<int> v;
v.push_back(1);
cout << v.capacity() << endl;
v.push_back(1);
cout << v.capacity() << endl;
v.push_back(1);
cout << v.capacity() << endl;
v.push_back(1);
v.push_back(1);
cout << v.capacity() << endl;
}
输出结果:
1
2
4
8
深浅拷贝
- 既然发生了拷贝,我们就需要考虑深拷贝与浅拷贝的问题了。
#include <iostream>
#include <vector>
using namespace std;
class A {
public:
int* data;
A(int x) : data(new int(x)) {}
A(const A& x) : data(x.data) {}
~A() {
delete data;
}
};
int main() {
A a_1(10);
A a_2(20);
vector<A> v{ a_1 };
v.push_back(a_2);
}
- 当我们这样子使用的时候,很正常的设计,在构造函数中
new
,在析构函数中delete
,结果最后却因为vector
的拷贝让指针重复delete
崩溃了,所以我们就设计了深拷贝。
移动构造
- 但是每次扩容都深拷贝的话,性能消耗太大了,就思考
vector
能不能调用我们设计好的移动构造
#include <iostream>
#include <vector>
using namespace std;
class A {
public:
A() {}
A(const A& other) {
cout << "拷贝" << endl;
}
A(const A&& other) {
cout << "移动" << endl;
}
};
int main() {
//A a;
vector<A> v;
v.push_back(A());
v.push_back(A());
v.push_back(A());
v.push_back(A());
}
输出结果:
移动
移动
拷贝
移动
拷贝
拷贝
移动
- 很遗憾,在我们插入的时候确实调用了移动构造(因为我们插入的是一个临时对象右值),但是扩容的时候仍是拷贝构造。
- 但是我们知道,
vector
中可以存放unique_ptr
(但是不能直接把一个独占指针直接存入,只能存入右值,因为插入的时候是拷贝的过程),但是unique_ptr
的拷贝构造和拷贝赋值都已经delete
了,所以在扩容的时候肯定调用的是移动构造函数。
vector<unique_ptr<int>> v;
v.push_back(unique_ptr<int>(new int(10)));
cout << v[0].get() << endl;
cout << v.capacity() << endl;
v.push_back(unique_ptr<int>(new int(10)));
cout << v[0].get() << endl;
cout << v.capacity() << endl;
输出结果:
0x711940
1
0x711940
2
- 所以我们将拷贝构造设置为
delete
就能使vector
,调用我们的移动构造函数了。
#include <iostream>
#include <vector>
using namespace std;
class A {
public:
A() {}
A(const A& other) = delete;
A(const A&& other) {
cout << "移动" << endl;
}
};
int main() {
//A a;
vector<A> v;
v.push_back(A());
v.push_back(A());
v.push_back(A());
v.push_back(A());
}
输出结果:
移动
移动
移动
移动
移动
移动
移动
- 但是这样我们就不能使用拷贝构造函数了,这肯定是不合理的,所以这里我们得知道为什么编译器不会调用移动构造函数。
- 原因在于,扩容的时候,一旦移动构造中发生错误,抛出异常就会使扩容失败,如果我们的移动构造函数没有写上
noexcept
,vector
就不敢在扩容的时候调用我们自己写的移动构造函数,所以我们需要给移动构造函数表上noexcept
#include <iostream>
#include <vector>
using namespace std;
class A {
public:
A() {}
A(const A& other) {
cout << "拷贝" << endl;
}
A(const A&& other) noexcept {
cout << "移动" << endl;
}
};
int main() {
//A a;
vector<A> v;
//v.push_back(a);
v.push_back(A());
v.push_back(A());
v.push_back(A());
v.push_back(A());
}
输出结果:
移动
移动
移动
移动
移动
移动
移动
- 这样我们既可以拷贝构造,也能让
vector
在扩容的时候使用我们的移动构造函数了