- vector如何扩展内存和释放内存
- STL中各种容器对比
- STL中的swap函数
- STL中哈希表扩容
- STL迭代器失效的情况和原因
- vector删除元素后如何避免当前迭代器会失效
- vector的iterator和const_iterator和const iterator
内存增长
1.5还是2倍扩容
gcc 二倍扩容,VS2015 1.5倍扩容
内存释放
size_type
STL容器中的一个成员变量,是一种用以保存不同容器的任意大小的类型,它与size_t一样,目的都是为了使用起来与具体机器无关。标准库类型将size_type定义为unsigned类型,比如string类的string::size_type 就是一个代表string抽象长度的无符号整型。也就是说,string::size_type从本质上来说,是一个整型数。关键是由于机器的环境,它的长度有可能不同。
size_t
size_t是一些C/C++标准在stddef.h中定义的。这个类型足以用来表示对象的大小。size_t的真实类型与操作系统有关。
在32位机器中被普遍定义为:
typedef unsigned int size_t;
而在64位机器中被定义为:
typedef unsigned long size_t;
size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。而int在不同架构下都是4字节,与size_t不同;且int为带符号数,size_t为无符号数。
为什么有时候不用int,而是用size_type或者size_t:
与int固定四个字节不同有所不同,size_t的取值范围是目标平台下最大可能的数组尺寸,一些平台下size_t的范围小于int的正数范围,又或者大于unsigned int. 使用Int既有可能浪费,又有可能范围不够大。
string str="123";
int a=2;
cout<
这段代码的输出并不是-1,在我64位win10系统中的输出是:4294967295,这是因为str.size()返回值是size_type一个无符号整数,而编译器在int与size_type做减法时,都视为了无符号整数。
容器 | 底层数据结构 | 时间复杂度 | 有无序 | 可不可重复 | 其他 |
---|---|---|---|---|---|
array | 数组 | 随机读改 O(1) | 无序 | 可重复 | 支持快速随机访问 |
vector | 数组 | 随机读改、尾部插入、尾部删除 O(1)、头部插入、头部删除 O(n) | 无序 | 可重复 | 支持快速随机访问 |
list | 双向链表 | 插入、删除 O(1)、随机读改 O(n) | 无序 | 可重复 | 支持快速增删 |
deque | 双端队列 | 头尾插入、头尾删除 O(1) | 无序 | 可重复 | 一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 |
stack | deque / list | 顶部插入、顶部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
queue | deque / list | 尾部插入、头部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 |
priority_queue | vector + max - heap | 插入、删除 O(log2n) | 有序 | 可重复 | vector容器 + heap处理规则 |
set | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multiset | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | |
map | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | |
multimap | 红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 |
散列函数,一个好的散列函数的值应尽可能平均分布。
处理冲突方法。
负载因子的大小。太大不一定就好,而且浪费空间严重,负载因子和散列函数是联动的。
class Person {
public:
int age;
public:
Person(int _age = 10) {
age = _age;
}
bool operator<(const Person& p) const {
return age < p.age;
}
};
int main(int argc, char** argv) {
map mp;
Person p1;
Person p2(20);
mp[p1] = 1;
mp[p2] = 2;
return 0;
}
利用function对普通的比较函数进行封装
class Person {
public:
int age;
public:
Person(int _age = 10) {
age = _age;
}
};
// 普通函数
bool cmp(const Person& p1, const Person& p2) {
return p1.age < p2.age;
}
int main(int argc, char** argv) {
map> mp(cmp); // function进行封装
Person p1;
Person p2(20);
mp[p1] = 1;
mp[p2] = 2;
return 0;
}
将比较函数打包成可以直接调用的类
#include
using namespace std;
class Person {
public:
int age;
public:
Person(int _age = 10) {
age = _age;
}
};
struct Compare
{
bool operator()(const Person& p1, const Person& p2) const {
return p1.age < p2.age;
}
};
int main(int argc, char** argv) {
map mp;
Person p1;
Person p2(20);
mp[p1] = 1;
mp[p2] = 2;
return 0;
}
前面几种方法,无论我们怎么定义,在声明group的时候都需要指定第3个参数,有什么方法是需要指定的呢?当然有啦。
通过map的定义可知,第三个参数的默认值是less。显而易见,less属于模板类。那么,我们可以对它进行模板定制,如下所示。
#include
using namespace std;
class Person {
public:
int age;
public:
Person(int _age = 10) {
age = _age;
}
};
template<>
struct std::less
{
public:
bool operator()(const Person& p1, const Person& p2) const {
return p1.age < p2.age;
}
};
int main(int argc, char** argv) {
map mp;
Person p1;
Person p2(20);
mp[p1] = 1;
mp[p2] = 2;
return 0;
}
迭代器是一种抽象的设计概念,是一种行为类似于指针的对象,指针最常见的行为就是解引用和成员访问。因此迭代器最重要的工作就是重载operator*和operator->。迭代器使得算法独立于使用的容器类型,使我们能够依序访问某个容器所含的各个元素,而又无需暴露该容器的内部表述方式。
迭代器使用类模板来实现。
迭代器相应型别
:迭代器所指之物的型别。可以通过函数模板的参数推导机制来实现。
template
void func_imp1(I iter, T t)
{
T tmp; // 这里T就是所指之物的型别,可用于变量声明之 用
// ...
};
template
inline
void func(I iter)
{
func_imp1(iter, *iter); // func的全部工作转移到func_imp1
}
int main() {
int i;
func(&i);
return 0;
}
用于萃取迭代器的5种型别 value type、difference type、pointer、reference、iterator catagory
value type
difference type
pointer type
reference type
iterator catagory
STL提供了一个 iterators class 如下,如果每个新设计的迭代器都继承自它,就可保证符合STL所需之规范。iterator class 不含任何成员,纯粹只是型别定义,所以继承它不会招致任何额外负担。
template
struct iterator {
typedef Category iterator_category;
typedef T value_type;
typedef Distance difference_type;
typedef Pointer pointer;
typedef Reference reference;
};
iterator设计:
首先定义5个迭代器类型,这些 classes 只作为标记用,所以不需要任何成员。用于针对不同的迭代器类型对某些函数进行重载。
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
定义一个迭代器基类
template
struct iterator {
typedef Category iterator_category;
typedef T value_type;
typedef Distance difference_type;
typedef Pointer pointer;
typedef Reference reference;
};
定义 iterator_traits 类
template
struct iterator_traits {
typedef typename Iterator::iterator_category iterator_category;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
}
针对原生指针 pointer 和 const-pointer 设计偏特化版的 iterator_traits 类
// pointer指针
template
struct iterator_traits {
typedef typename random_access_iterator_tag iterator_category;
typedef typename T value_type;
typedef typename ptrdiff_t difference_type;
typedef typename T* pointer;
typedef typename T& reference;
};
// const-pointer指针
template
struct iterator_traits {
typedef typename random_access_iterator_tag iterator_category;
typedef typename T value_type;
typedef typename ptrdiff_t difference_type;
typedef typename T* pointer;
typedef typename T& reference;
};
这个模板函数可以很方便地决定某个迭代器的类型(category)
template
inline typename iterator_traits::iterator_category
iterator_category(const Iterator&){
typedef typename iterator_traits::iterator_category category;
return category();
};
这个模板函数可以很方便地决定某个迭代器的distance type
template
inline typename iterator_traits::difference_type*
distance_type(const Iterator&){
return static_cast::difference_type*>(0);
};
这个模板函数可以很方便地决定某个迭代器的 value type
template
inline typename iterator_traits::value_type*
value_type(const Iterator&){
return static_cast::value_type*>(0);
};
**迭代器失效就是指:迭代器指向的位置在该操作的前后不再相同。**删除时,将当前的迭代器存起来。
迭代器失效分三种情况考虑,也是分三种数据结构考虑,分别为数组型,链表型,树型数据结构。
iter = cont.erase(iter);
//不要直接在循环条件中写++iter
for (iter = cont.begin(); iter != cont.end();)
{
(*it)->doSomething();
if (shouldDelete(*iter))
iter = cont.erase(iter); //erase删除元素,返回下一个迭代器
else
++iter;
}
链表型数据结构
树形数据结构
注意 :经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*ite
advance(a, distance(a, b));
其中,distance用于取得两个迭代器之间的距离,advance用于将一个迭代器移动指定的距离typedef deque IntDeque;
typedef IntDeque::iterator iter;
typedef IntDeque::const_iterator ConstIter;
IntDeque d;
ConstIter ci;
Iter i(d.begin());
advance(i,distance(i,ci));
sort 算法在数据量大的时候采用 快排,分段递归排序,一旦分段后的数据量小于某个门槛 16,改用 插入排序,,如果递归层次过深,还会改用 堆排
具体实现是这样的
sort(first, last);
首先计算 last - first 的长度,通过 log(last - first) (2^k <= n 的最大 k ) 计算最大递归深度 depth_limit,如果元素个数小于 16 ,执行插入排序;否则 判断是否到达最大递归深度,如果到达最大递归深度,执行堆排,否则进行快排。
在最后结束后,[first, last) 内会有很多“元素个数少于16”的子序列,每个子系列都有相当程度的排序,但尚未完全排序,此时再进行一次插入排序即可。
其中,快排采用三点中值方案来确定枢轴。
插入排序在数据量小时,不像其他复杂算法那样有着诸如递归调用等操作带来的额外负担,所以排序效果不错,且当序列接近有序时,插入排序效率也很不错。
在该排序算法的实现过程中,定义了一个类似于搬运作用的链表carry和具有中转站作用的链表counter,这里首先对counter[i]里面存储数据的规则进行分析;counter[i]里面最多存储数据个数为2^(i+1),若存储数据超过该数字,则向相邻高位进位,即把counter[i]链表里的内容都合并到counter[i+1]链表。carry负责取出原始链表的头一个数据节点和交换数据中转站作用
思想是:从链表中取1个节点,放入counter[0],再取1个节点,放入counter[0],此时counter[0]装满了,会转移到counter[1],然后counter[0]再存2个节点,转移到counter[1],此时counter[1]满了。就转移到counter[2],…,依此类推,合并几个counter,因为每一个counter里都是有序的,所以思想是归并排序。
通常 C++ 的内存配置操作和释放操作是这样的
class Foo {...};
Foo *pf = new Foo; // 配置内存,然后构造对象
delete pf; // 将对象析构,然后释放内存
这其中的 new 包含两阶段操作:
(1) 调用new配置内存,(2) 调用Foo:Foo() 构造对象内容;
delete 也包含两阶段操作:
(1) 调用Foo:~Foo()将对象析构,(2) 调用 delete 释放内存
为精密分工,STL allocator 将这两阶段操作区分开:
construct() 接受一个指针p和一个初值value,将初值设定到指针所指的空间上
destroy() 有两个版本,第一个版本接受一个指针,然后将该指针所指之物析构
第二个版本接受 first 和 last 两个迭代器,将 [ first, last ) 区间内的对象析构掉
SGI 设计了双层级内存配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:
当配置区块超过128 bytes 时,视之为“足够大”,便调用第一级配置器;
当配置区块小于 128 bytes时,视之为“过小”,为了避免太多小额区块造成内存的碎片以及降低额外负担(比如分配一个很小的块,但系统需要内存去记录这个块的大小,那么这个块越小,因为记录的内存大小固定,所以额外负担所占比例愈大),便采用复杂的memory pool 整理方式,而不再求助于第一级配置器。
内存池管理,又叫次层配置:每次配置一大块内存,并维护对应之自由链表 free-list 。下次若再有相同大小的内存需求,就直接从 free-list 中拨出。如果客端释还小额区块,就由配置器回收到 free-list 中(配置器除了负责配置,还负责回收)。为了方便管理,SGI第二级配置器会主动将任何小额区块内存需求量上调至 8 的倍数(例如客端要求30bytes,就自动调整为 32 bytes),并维护 16 个 free-list,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块。
首先判断区块大小,大于128 bytes 就调用第一级配置器,小于 128 bytes 就检查对应的 free-list,如果free-list有可用的区块,就直接拿来用,如果没有,就调用refill() 准备从内存池为free-list重新填充空间。
释放操作,首先判断区块大小,大于 128 bytes 就调用第一级配置器的 deallocate() 函数释放,小于 128 bytes 就找到对应的 free-list,将区块回收。
重新填充:当发现free-list中没有可用区块了时,就调用 refill() ,准备为 free-list 重新填充空间。新的空间将取自内存池。缺省取得20个新节点,但万一内存池空间不足,获得的节点数可能小于20。如果内存池空间不足,就调用配置heap空间,用来补充内存池,具体配置方式如下: