小谈 vector 容器

本文涉及一些vector的相关知识,比如扩容等。

一、vector 的底层数据结构

vector和数组一样,都是占用一块连续的内存空间,区别是vector占用的是堆内存,而数组占用的是栈内存。
vector是stl的一个容器,也可以理解成一个模板类。它的底层原理是由三个迭代器(也可以说指针,vector容器的迭代器因为内存连续就是使用的指针)实现各种算法,分别是 iterator start (指向当前空间的头部,即第一个元素的位置)、iterator finish(指向当前已使用空间的尾部,即最后一个元素的末尾字节)和 iterator end_of_storage(指向当前可用空间的尾部)。

二、vector 的扩容机制

1、扩容的原理

当容量等于元素的大小时发生扩容。
1)重新开辟一块两倍或1.5被大小的堆内存空间
2)将源内存空间的数据拷贝到新的内存空间,然后构造新元素,并释放原有内存空间

2、扩容的大小如何选择

扩容的倍数关系是和操作系统的内存管理机制挂钩的。另外扩容时需要注意的点就是扩容大小是适度,避免太大浪费以及太小了又会频繁创建。同时还希望本次扩容释放的空间可以在下次扩容时得到重新使用,即当我们在第N次扩容时可以将前N-1释放的空间都用上。
1)在vs下采用1.5倍的扩容机制。因为win操作系统会将释放的空闲内存进行合并,从而使用1.5倍扩容,这样经过几次扩容后就可以重新使用之前释放的内存空间。从而可以避免频繁内存申请的开销。
2)linux下采用2倍的扩容机制。linux中stl中的空间配置器对于小内存空间的申请采用内存池化管理,维护16个链表,每个链表都是8的倍数,8 16 32 64,依次递增,因此采用2倍的分配策略 可以快速的完成内存的分配。

3、扩容时如何拷贝数据

vector在扩容时是统一采用uninitialized_copy函数进行数据拷贝的,同时根据数据类型的不同又进行了区分。这样可以最大限度的提高数据拷贝的效率
1)pod类型,使用的是copy函数(stl源码库中提供的底层函数)
2)自定义数据类型,使用的是construct函数,不断的循环调用进行构造
3)对于char*数据类型,直接使用 memmove函数(应该就是二进制拷贝)

4、添加元素的方式

1)push_back,仅支单个参数的传递。
首先调用构造函数建立临时对象,然后调用拷贝构造函数将临时对象放入容器,最后析构临时对象。
2)emplace_back,支持多个参数的传递。
直接在原地执行构造函数,避免了临时对象的创建和销毁。
3)区别。
一般情况下,emplace_back的效率是优于push_back的,但在传递临时对象或者对象实例时效率是一样的。另外就是,emplace_back支持传递多参数,而push_back只支持传递单参数。注意当传递临时对象或者对象实例时都会优先调用移动赋值运算符拷贝,对于移动赋值运算符拷贝的话需要使用 noexcept 关键字声明禁止抛出异常。

三、vector 的相关特性

1、如何避免扩容?

提前预估vector的大小,初始化时给足容量。

2、如何释放vector的占用内存空间?

正常情况下,vector占用的内存空间(堆区)只增不减。clear() 函数只能清空元素,即只会调用析构函数,是不能清空内存的。
只有强制使用 swap() 函数来帮忙释放内存。

vector<int> vec(20);
vector<int>().swap( vec );

3、vector 作为返回值的时候会不会发生拷贝构造?

会优先调用移动赋值运算符的重载函数。如果是自定义数据类型,需要进行移动赋值原算符的重载。

4、push_back() 的时间复杂度

注意,不是O(1),而是O(3)。因为需要扩容,在扩容的时候会进行元素拷贝造成复杂度的上升。

如有问题,欢迎探讨!