1、vector扩容,resize和reserve的区别?
eg: 代码示例
vector<int> array(4);
array.reserve(n1);
array.resize(n2, <初始化扩充内存处的值,不填则默认零值>);
- reserve增加的是capacity,但是实际被使用的内存大小size并没有改变!reserve是容器预留空间,但在空间内并不真正创建元素对象。所以在添加新的对象前,扩充的内存区域不能被访问。加入新元素时,要调用push_back() / insert()函数。
- resize改变容器的capacity同时也增加了它的size。resize在改变容器大小的同时,在新增内存处直接创建了对象(新的空间不覆盖原有元素占有的空间)。扩充区域可以直接被访问。此时再调用push_back()函数,是直接加在这个新空间后面的。
- 不管是调用resize还是reserve,二者对容器原有的元素都没有影响。
2、vector的动态扩容,为何是1.5倍或者是2倍?
为什么以倍数方式扩容?
以等长个数方式扩容,假设向vector容器中push_back() n个元素,每次扩容K个元素空间,则n个元素push_back()的总操作次数为:
以倍数方式进行扩容,假设向vector容器中push_back() n个元素,倍增因子为k,则n个元素push_back()的总操作次数为:
为什么选择1.5倍或者2倍方式扩容,而不是3倍4倍?
扩容原理为:申请新空间,拷贝元素,释放旧空间,理想的分配方案是在第N次扩容时如果能复用之前N-1次释放的空间就太好了,如果按照2倍方式扩容,第i次扩容空间大小如下:
可以看到,每次扩容时,前面释放的空间都不能使用。比如:第4次扩容时,第1次、第2次空间已经释放,第3次空间还没有释放(开辟新空间、拷贝元素、释放旧空间),即前面释放的空间只有1 + 2 = 3,假设第3次空间已经释放才只有1+2+4=7,而第4次需要8个空间,因此无法使用之前已释放的空间。
但是按照小于2倍方式扩容,多次扩容之后就可以复用之前释放的空间了。
结论:使用2倍(k=2)扩容机制扩容时,每次扩容后的新内存大小必定大于前面的总和。
使用1.5倍(k=1.5) 扩容机制扩容时,在几次扩展以后,可以重用之前的内存空间了。
Windows和Linux的扩容底层原理
Windows扩容底层
Windows中堆管理系统会对释放的堆块进行合并,因此VS下的vector扩容机制选择使用1.5倍的方式扩容,这样多次扩容以后,就可以使用之前已经释放的空间了。
Linux的扩容底层
linux下如果malloc的空间小于128KB,其内部通过brk()来扩张,如果大于128KB且arena中没有足够的空间时,通过mmap将内存映射到进程地址空间。
linux中引入伙伴系统为内核提供了一种用于分配一下连续的页而建立的一种高效的分配策略,对固定分区和动态分区方式的折中,固定分区存在内部碎片,动态分区存在外部碎片,而且动态分区回收时的合并以及分配时的切片是比较耗时的。
伙伴系统是将整个内存区域构建成基本大小basicSize的1倍、2倍、4倍、8倍、16倍等,即要求内存空间分区链均对应2的整次幂倍大小的空间,整齐划一。估计可能是这个原因SGI-STL选择以2倍方式进行扩容。
总结
- vector在push_back以倍数方式进行扩容可以在均摊后达到O(1)的事件复杂度,相对于以等长个数方式扩容的O(n)时间复杂度更好。
- 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用。
3、移动语义 move()
一句话概括:std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
在C++11中,标准库在中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值引用强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。
4、shard_ptr是线程安全的吗?为什么不保证线程安全?
对于shard_ptr对象的读写不是线程安全的。
- 一个 shared_ptr 对象实体可被多个线程同时读取;
- 两个 shared_ptr 对象实体可以被两个线程同时写入;
- 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁;
shared_ptr 有两个数据成员,读写操作不能原子化。使得多线程读写同一个 shared_ptr 对象需要加锁。
shared_ptr 的数据结构
shared_ptr 是引用计数型智能指针,几乎所有的实现都采用在堆上放个计数值的办法。具体来说,shared_ptr 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针指向堆上的 ref_count 对象。
shared_ptr x(new Foo); //步骤一
shared_ptr y = x;
步骤一执行结果
中间步骤(1) 中间步骤(2)
步骤二执行结果
上述图中可以看出,y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。 如果没有 mutex 保护,那么在多线程里就有竞争情况。
5、智能指针—weak_ptr
std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期。
● 如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
● 当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
当一个shared_ptr给另一个shared_ptr智能指针赋值时,会对改变引用计数的值。然后根据该计数值是否为0进行实际内存的释放。对于智能指针而言引用计数分为两个一个是强引用计数一个是弱引用计数,前者是作为是否可以释放内存的依据,而后者不是。
对weak_ptr指针进行赋值的时候则不会造成强引用计数值的改变,而是改变弱引用计数。
如图中所示,当使用shared_ptr给两个weak_ptr赋值时,strong ref仍旧是1,而weak ref是2。
由于强引用计数归0的时候会自动释放内存,在释放内存后使用weak_ptr就会出现野指针问题,从这一点看出weak_ptr并不适用于直接使用,它的主要作用其实是观察shared_ptr的状态。
成员函数
lock()
上面提到当强引用计数归0时内存会自动析构,而判断内存是否真实存在是weak_ptr的功能之一。
当一个shared_ptr离开其构造的作用域后就析构了,同时,作为观察的weak_ptr的数据显示中也没有strong ref,而只有weak ref。
这是可以使用lock()进行判断一下,如果shared_ptr所指向的地址是可用的就返回shared_ptr,否则返回nullptr,示例代码如下:
#include <iostream>
#include <memory>
using namespace std;
class Student
{
public:
Student() { static int count = 0; cout << "structure " << ++count << endl; }
~Student() { static int count = 0; cout << "destruct " << --count << endl; }
};
int main()
{
weak_ptr<Student> student_wptr;
{
shared_ptr<Student> student_sptr = make_shared<Student>();
student_wptr = student_sptr;
}
if (student_wptr.lock() == nullptr) {
cout << "内存已经释放" << endl;
}
system("pause");
return 0;
}
expired()
该函数可以直接检测出指向的真实地址是否有效,有效返回true,无效返回false。
t main()
{
// ......
if (student_wptr.expired() == nullptr) {
cout << "内存已经释放" << endl;
}
system("pause");
return 0;
}