- 基于对象告诉我们要将函数和数据封装到一个类里,但标准库数据和算法是分离的,是不同的思想
- 数据结构+算法 —》 增删改查
- 标准库接口,规则是标准的,但GUN和VC的标准库是各自实现的,没有统一,具体实现会有一些局部的差别
- 标准库容器使用模板技术,其类内包含成员类型(别名),使得更好的标识元素对应的类型
容器
容器的共通能力和共通操作
- 所有容器提供的都是“value语意”而非“reference语意”
容器进行元素的安插操作时,内部实施的是拷贝操作,置于容器内。因此STL容器的每一个元素都必须能够被拷贝。如果你打算存放的对象不具有public copy构造函数,或者你要的不是副本(例如你要的是被多个容器共同容纳的元素〉,那么容器元素就只能是指针(指向对象)。 - 根据迭代器获取元素
- 调用者需保证传给操作函数的参数符合要求
- swap仅是指针的交换(start,finish,end_of_storage)
- 几乎所有的容器都维持着两个迭代器,来指向容器的头和尾
顺序容器
- 元素按照放入顺序,容器有先后顺序之分,而关联容器和乱序容器都不具备,关联容器自动排序,乱序容器乱序
vector
vector的数据成员结构start、finish、end_of_storage组成,大小为12字节
- 支持随机存取
- 迭代器是随机存取迭代器,对任何一个STL算法都支持
- 末端添加或删除效率高,前端或中端改动元素效率一般
- vector优异性能的秘诀之一,就是配置比所容纳元素所需更多的容量。
- 一旦扩容vector元素相关所有reference、pointers、iterators都会失效
- 在很多版本中的vector,当你第一次安插元素时其就会一口气配置一大块内存(例如2K),所以如果你有一大堆vector,而每个vector的实际元素却寥寥无几,那么浪费的内存会相当多
- 既然vectors 的容量不会缩减,我们便可确定,即使删除元素,其references、pointers、iterators也会继续有效,继续指向动作发生前的位置。然而安插操作却可能使references、 pointers 、 iterators 失效
- swap修改的是指针(start、finish、end_of_storage),迭代器等会失效
capacity() 返回vector实际能够容纳的元素数量,等于end_of_storage - start 如果超越该数,即会发送扩容
reserve() 设置适当容量,其改变了end_of_storage的指向,reserve设置比当前size,则无事发生
std::vector<T>(v).swap(v); //可用于缩减容量
deque
-
map由一个vector形成,其内指针指向各个buffer,buffer的顺序由vector决定
-
迭代器的node数据指向map(控制中心)之内,frist、last指向分段buffer的头和尾(标兵),cur表示迭代器指向buffer的位置
-
deque 的接口和vector几乎一样
一般与vector比较
- 两端都能快速修改元素,其inset相对vector智能会比较插入位置和头尾的位置关系
- 存取元素,deque内部结果会多一个中间过程,所以元素的存取和迭代器的动作会稍微慢一点
- 迭代器需要在不同的区块间跳转,所以必须是特殊的智能型指针(++的操作符重载,能自动在不同区块调整),而非一般指针
- 迭代器属于random aceess iterator
- deque使用不止一块内存,其每次扩展大小为一个buffer块
- deque 的内存区块不再被使用时,会被释放。deque的内存大小是可缩减的。不过,是不是这么做,以及究竟怎么做,由实作版本定义之。
list
- list是一个带头节点的循环双向链表
forward_list
- 单向链表,相对list节约了指针空间,中间的安插删除动作比双向链表慢,提供了push_front头插和front,而没有尾插
- end()指向nullptr
容器适配器
- 容器适配器没有迭代器,其不提供迭代器相关操作(保证其容器的独特性质,先进先出,先进后出,不会去修改其内容)
stack
queue
关联容器
- 通过key,快速关联(搜寻)value,小型数据库通过key–》value
- 涉及大量查找,结构从二叉树和哈希表出发
set
- key不会重复,当发生key重复安插时,其什么都不做(可根据获取返回值判断是否插入成功),保持现状,根据set的特性可以进行筛选数据
- 自动排序的性质,其在insert插入时,元素会被自动维持 sorted 状态
- 其底层是一个red-black tree
红黑树在改变元素数量和元素搜寻方面很出色,它保证节点安插时最多只会做两个重新连接(调整),而且到达某一元素最长路径深度,最多只是最短路径深度的两倍。且自动排序使得二叉树搜寻函数算法具有对数复杂度 - 但是,自动排序造成sets和multisets的一个重要限制:你不能直接修改元素,因为这样会打乱原本正确的顺序。
因此,要改变元素值,必须先删除旧元素,再插入新元素。
multiset
map
- key不会重复,key-value对应到数据库 key就是主键(不重复,value就是一个包含事物信息的结构体
- 当操作对象为 map 容器中已存储的键值对时,则借助 [ ] 运算符,既可以获取指定键对应的值,还能对指定键对应的值进行修改;反之,若 map 容器内部没有存储以 [ ] 运算符内指定数据为键的键值对,则使用 [ ] 运算符会向当前 map 容器中添加一个新的键值对。
- 对于map的inset成员函数和set相同,返回值有点特殊,当存在相应key值,为无效插入,可根据获取返回值判断是否插入成功,其inset其实是对rb_tree的操作
- pair为其元素,可重试<<操作符来进行打印,嘻嘻
- map 的[ ]操作符—》关联数组–》由于map根据key排序,通过排序好的规则直接通过下标获取对应的data,而set不存在关联数组性质,不提供[ ]操作符,但效率没有直接insert高,其先会二分搜索 lower_bound,但更直观
- 根据key的排序准则自动将元素排序
- set,multisets,map,multimaps使用相同的数据结构,可以将set,multisets分别视为特殊的map和multimaps,其拥有set,multisets所有能力和操作函数
multimap
- map key不能重复,multimap key可以重复,其有自己的操作函数来处理管理重复的key对应的value区间
- 如果key是unique(主键),那么选取map、如果key no unique 那么应该选用multimap,其他性质和map类似
- multimap 不能使用 [ ] 作为插入和获取元素,因为其key对应着不同的value
关联式容器的迭代器
rb_tree
- map和set的所有操作,都转调了底层的rb_tree的操作,从这层意义上来看,set和map未尝不是一个container adapter
rb_tree提供“遍历”操作及iterator
按正常规则(++ite)遍历,便能获得排序状态(sorted)
我们不能使用rb_tree的迭代器改变元素值(因为元素有其严谨的排序规则)。编程层面并未阻止此事。如此设计是正确的,而rb_tree即将为set和map服务(作为其底层支持),而map允许元素的data被修改,只有元素的是不可被修改的
乱序容器
- 其也算是关联容器,但元素是混乱分布的
哈希函数–将key对应映射到表的某个位置
确定hash code
比较函数–对于ordered_map和ordered_set,其避免了重复key
对于ordered_multimap和ordered_multiset其底层有hashtable,而hashtable需要该比较函数,也许还有其他原因还得看源码分析
unordered_multiset
- key可重复
hashtable 哈希表(散列表)
- hashtable的设计有多种(为了处理碰撞),链表法是相对较好的一种,其对应每一栏都可有一个链表
- rehash 当篮子数和所放置的元素个数相同,就进行rehash
- 处理key的哈希碰撞,利用哈希函数来运算一个key的代码来尽量产生相同的key
hashfunction
4.9版本后C++内部支持的hashfunction
- 对key进行算术处理变为hashcode,而hashfunction的设计是需要更加数据的分布,特性来进行设计的
- 整形的key = hashcode
- G2.9不支持字符串,其在G4.9开始支持
- 以下是hashfunction的接口
- template struct hash{};//表示一个泛化模板
一个万用的hash funtion
hashtable的应用
- 处理大数问题
- 为hashmap和hash_set服务—》快速搜索
hashtable容器的迭代器
- 我们可以利用hashtable iterator改变元素的data,但不能改变元素 key(因为hashtable根据key实现严谨的元素排列)
大数据容器移动拷贝构造和拷贝构造的效率
总结
- swap仅是指针的交换
- vector为什么移动拷贝和拷贝构造效率差距如此大,只要是因为其会扩容
array
- _M_intance是数组名
- 数组typedef int T[100]
string
- string是一个类别名–》std::basic_string的别名
- string 不能被nullptr初始化,其空的语义为" "
- 使用了引用计数的手段,拷贝共享—》copy on write
容器结构与分类
迭代器
对容器的操作
遍历
- 从容器头到容器尾咯
给容器添加元素
- push_back
- insert
几乎所有的容器都维持着两个迭代器,来指向容器的头和尾
- 随机存取迭代器对任何一个STL算法的都可以凑效
- iterator除了vector,其他都是一种smart pointer 类
- ++it 返回值为引用,效率高,但要避免失效引用,it++返回值
当存在多个运算符重载时,如*、++, 如++this,则this作为++操作符的参数使用,而不会发生运算符重载
迭代器的分类没有使用枚举,而是使用了类–》类型推演,另外public继承,是一种的概念,子类能自动赋值为父类
输出迭代器
ostream_iterator 输出流迭代器打印容器元素
- 可绑定对应的 输出流缓冲区 cout(标准输出) ostream类(对应输出文件)
ofstream ofile("data.txt",ios::app|ios::out);
copy(coll.begin(),coll.end(),ostream_iterator<int>(ofile," "));
ofile.close();
- 字符串流也可
istream_iterator 输入流迭代器
插入型迭代器
back_insert_iterator ->封装函数为back_inserter --》后插 封装了push_back—》其赋值运算符重载包含了push_back (特化)
front_insert_iterator ->封装函数为front_inseter—》前插
insert_iterator ->封装函数为inseter–》按位置插------》其赋值运算符重载包含了insert,提供两个参数(泛化)
迭代器类型萃取
迭代器的设计原则
- value_type 迭代器所指对象的类型,Iterator_traits能根据容器的迭代器,萃取出元素类型value type
- difference_type 表示两个迭代器之间的距离,为什么不直接使用unsigned int 大小呢?2^32,如果其容量大于其范围呢
- iterator_category 获取迭代器的类型,如随机访问迭代器
- reference (标准库暂时还未使用)
- pointer (标准库暂时还未使用)
根据迭代器可获取到容器元素的类型等信息
- 萃取机是为了处理non-class iterator而引入的(中间层)
iterator_traits 萃取机使用模板特化来区分迭代器和指针
迭代器失效
- it=erase(it) vector和list顺序容器,删除指定it位置的元素,其删除元素位置后元素相当于左搬移,迭代器it指向当前位置,保证空间相对连续
算法
所有的算法,其内最终涉及元素本身(查询、排序等)的操作,无法就是在比较大小—》从元素本身来看,是否重载了特殊的比较方式
所有算法都用来处理一个或多个区间内的元素—》从接口来看,迭代器和仿函数是算法的养料,算法函数内也是对迭代器的操作来处理元素
迭代器也可能是一个指针
算法的形式
//2 范围内满足Predicate条件进行处理
template<typename Iterator>
Algorithm_if(Iterator itr1,Iterator itr2,Predicate pre,...) //提供一个Predicate条件
//3 将范围内满足的放置到新的区域,不改变原来的区域
template<typename Iterator>
Algorithm_copy(Iterator itr1,Iterator itr2,OutputIterator result,...) //提供一个OutputIterator指向新的区域
算法的模板参数列表接口
- 由于模板参数接收任何类型,而算法其处理的能力需要不同迭代器的分类,那么从语法层次的接口上我们无法进行限制,那么我们根据从模板参数的接口命名来使得开发者认识该算法到应该传入什么迭代器。
区间
- 所有算法都用来处理一个或多个区间内的元素。这样的区间可以(但非强行要求)涵盖容器内的全部元素。因此,为了得以操作容器元素的某个子集,我们必须将区间首尾当做两个参数传给算法,而不是一口气把整个容器传递进去。
- 用户必须确保两参数定义出来的区间是有效的,两个迭代器隶属同一个容器,而且前后放置正确(迭代器就像一般指针一样危险)
- 所有的算法处理都是半开区间 [ begin,end) ----- 包括起始元素位置但不包括末尾元素位置。其可避免对空集的另做处理。空集时begin=end
处理多个区间
有数个算法可以(或说需要)同时处理多个区间。
通常你必须设定第一个区间的起点和终点,至于其它区间,你只需设定起点即可,终点通常可由第一区间的元素数量推导出来。
排序
- 完全排序
- 局部(部分)排序 partial_sort
- stable_sort(稳定排序)
以上适用于顺序容器,但list是例外,list没有随机访问迭代器,而是双向迭代器,其自身提供了成员函数sort - 关联式容器自动排序(对全体元素进行一次性排序,通常比始终维护它们保持已序状态来得高效一些
查找
- 顺序容器使用算法查找,又分为顺序和二分查找
- 关联容器使用成员函数查找
更易性算法
remove
- 其删除元素利用依次移动元素进行覆盖,其没有改变容器大小等,没有做真正的删除,而将删除交给erase函数
仿函数
- 传入函数名,对象,lambda对象
- 作为一个C/C++程序员来说 回调应该属于深入人心的机制了。其他工具包使用回调来实现这种通信。回调函数是一个指向函数的指针,所以如果你想要一个处理函数通知你一些事件,你可以将一个指向另一个函数(回调函数)的指针传递给处理函数。处理函数然后在适当的时候调用回调函数。但回调可能不太直观,而且在确保回调参数的类型正确性方面可能会遇到问题。
仿函数的模板参数列表暗示
- 谓词Predicate 返回值为bool的判断式
UnaryPredicate 一元谓词 - Function 一个回调函数
UnaryFunction - Compare 二元比较函数,其实也算是二元谓词
接口仿函数进行包装
仿函数的适配
- 其仿函数支持函数适配器(binder2nd等)处理进行改造
其他补充
右值
STL中一般很少有人提及右值,但右值引用其是STL标准库效率提升的利器,如下
https://editor.csdn.net/md?articleId=121025037
分配器
- GUN和VC的分配器没有被统一,但普通开发者不需要对其进行扩展
- 对于数据需求少量new,应该使用new、delete相关操作,大量内存需求时使用容器
适配器
bind2nd 源码分析
补充
noexcept关键字
- vector容器的元素类的移动拷贝和移动赋值需要noexcept修饰
扩容策略
-
vector开始以2倍快速扩容,若达到一个阈值则替换当前扩容策略,防止vector后期申请大量空间造成浪费,此时特别容易抛出异常bad_alloc(其实是重新申请一块大内存,然后重新拷贝元素过去,如果是右值则使用移动拷贝)
-
deque 每次申请一个buffer块扩充
-
list 每次申请一个节点内存扩充
萃取
- 迭代器萃取 iterator_traits
- 字符串萃取 char_traits
- 类型萃取 type_traits
- allocator traits
- pointer traits
- array traits