Effective STL 读书笔记

第二章 vector和string

第13条:vector和string优先于动态分配的数组

使用new来动态分配内存,需要承担以下责任

1.确保有配套的delete调用

2.确保delete调用形式正确,单个对象使用delete,数组使用delete[]

3.delete只调用一次

每当需要动态分配一个数组时,都应该考虑使用vector和string来替代,原因是

它们自己管理内存

它们可以使用全部stl算法

它们和旧代码可以相互转化

只有一种情况使用动态的数组取代string,关于string的引用计数技术

·多线程中使用引用计数的string,在避免内存分配和字符拷贝所节省下的时间比不过在背后同步控制上的时间

规避方法:

使用不使用引用计数的string
考虑使用vector<char>而不是string

第14条:使用reserve来避免不必要的重新分配

stl容器的自动增长机制

当需要更多空间时,就调用realloc类似的操作

1.分配 一块当前容量的某个倍数的新内存。一般是2倍
把容器的所有元素从旧的内存拷贝到新的内存
析构旧内存中的对象
释放旧内存

这些操作十分费时,这些步骤发生时,容器所有的指针、迭代器和引用都无效

4个函数

size(),告诉你容器有多少元素,不会告诉你该容器为自己所包含的元素分配多少内存

capacity(),告诉你该容器已经分配的内存可以容纳多少个元素,是容器可以容纳的元素最大总数,不是还能容纳的多少元素,还能容纳的多少元素,capacity-size

resize(n),强迫容器改变到包含n个元素的状态,n如果比当前size要小,尾部多余元素会析构,如果n比当前size要大,通过默认构造函数创建的新元素将会被添加到容器的末尾,如果n比当前的capacity要大,在添加元素之前,将先重新分配内存

reserve(n),强迫容器把它容量变为至少是n,前提是n不小于当前大小,这通常会导致重新分配,

避免重新分配的关键是,尽早的使用reserve把容器的容器设为足够大的值

第15条:注意String实现的多样性

string大小和实现不同而不同

size信息

capacity容量

value值

可能包括:分配子的拷贝

建立在引用计数之上的string:对值的引用计数

string类型的区别

string的值可能会被引用计数

大小的范围值时一个char*指针大小的1-7倍

创建新的字符串子可能需要零次、一次或者两次动态分配

string类型可能共享、也可能不共享其大小和容量信息

string 可能支持,也可能不支持针对单个对象的分配子

不同的实现对字符内存的最小分配单位有不同的策略

第16条:了解如何把 vector 和 string 数据传给旧的 API。

vector v表示数组的指针 &v[0]

string s.ctr() 表示char*

事实上,先让C API把数据写到一个vector中,再把数据拷贝到STL容器中,这思想总是可行的

可以先把其他容器转换为vector再使用

第17条:使用swap技巧除去多余的容量

shrink to fit思想(压缩至适当大小)

vector<contestant>(contestants).swap(contestants);

vector<contestant>(contestants)创建一个临时的vector,是contestants的拷贝,只为所拷贝的元素分配了所需要的内存,swap后临时变量的容量是原先contestants的容量,临时变量执行完后被析构,而contestants拥有合适的内存值

vector<contestant>().swap(v);// 清除v并把它的容量变为最小

swap不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换

第18条:避免使用vector<bool>

它不是一个STL容器

STL容器的一个条件:

T *p = &c[0] 如果operator[取得了Container<t>中的一个T对象,那么你可以通过取它的地址得到一个指向该对象的指针。

它不存储bool

它存储的是bool的紧凑表示,而非真正的bool,使用了bitfield思想

使用deque<bool> 或bitset代替

第三章 关联容器

equivalence(等价)而不是equality(相等)来对待自己的内容

第19条:理解相等(equality)和等价(equivalence)的概念

相等:以operator==为基础 :find

等价:以operator< insert>

相等不一定所有数据成员都有相同值

等价关系:"在已排序的区间中对象值的相对顺序",针对oprator <

!(w1 < w2) && !(w2 < w1)这两个值就是等价的

用户判别式(predicate):比较函数

标注关联容器通过key_comp成员函数可被外部使用
!c.key_comp()(x,y) && !c.key_comp()(y,x) // 在c的排列顺序中,x不在y前,y也不在x之前

例子:自定义set<string> 不区分大小写

使用set的find成员函数,查找含义仅大小写不同的string的set里面,可以成功查找(基于等价),使用非成员的find算法就不会成功(基于相等)

标注关联容器使用等价的原因

容器总是保持排列顺序的,那么必须要实现比较相对大小,如果使用相等,需要多定义一个操作符

第20条:为包含指针的关联容器制定比较类型

set<string*> ssp;

是下面代码的缩写:set<string*,less<string*>> spp;

最精确的:set<string*,less<string*>,allocator<string*>> spp;

定义比较函数子类

set 比较不需要函数,而是需要一个类型,在内部通过它创建一个函数

其他容器包含的对象与指针的行为类似:比如智能指针和迭代器

第21条:总是让比较函数在等值情况下返回false

比较函数的返回值表明的按照该函数定义的排列顺序,相等的值从来不会有前后顺序关系,比较函数应当始终返回false

第22条:切勿直接修改set或multiset中的键

为什么set或者multiset中的元素不能是const的

针对对象,如果用对象中某个参数表示key,其他参数表示value,如果设置const,则无法改变value

更改键部分(key part):这部分信息会影响容器的排序性,可能破坏容器

强制类型转换是危险的,只要您能避免使用它就应用避免使用

第23条:考虑用排序的vector替代关联容器

需要可提供快速查找功能的数据结构时,可以选择关联容器

考虑查找速度,非标准的哈希容器是值得的

标准关联容器比vector效率还低的情况并不少见

标准关联容器通常被实现为平衡二叉查找树

平衡二叉树对插入、删除和查找的混合操作做了优化,总的来说就是没办法预测针对这棵树的下个操作是什么

常见的数据结构过程

设置阶段:几乎所有操作都是插入和删除

查找阶段:几乎所有操作都是查找

重组阶段:改变改数据结构,再插入新的的数,和第1阶段类似

这样的情况下,vector可能比关联容器提供了更好的性能,必须是排序的vector

排序的vector性能强的原因

大小问题:关联容器中一个widget伴随的空间至少是3个指针,更非内存

但是需要对每个元素都需要排序

第24条:当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择

map的operator[]与众不同,它的设计目的是提高"添加和更新"

如果键k没有的map中,需要先初始化一个对象给其拷贝赋值,效率低,应该使用insert

第25条:熟悉非标注的哈希容器

目前应该有std::map等实现

第四章 迭代器

第26条:Iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator

四个迭代器

iterator 相当于T*,

const_iterator相当于const T*

iterator、const_iterator 递增效果:头部到尾部

reverse_iterator、const_reverse_iterator相当于T、const T,递增效果:尾部到头部

类似的参数基本参数类型为:Iterator

不同迭代器的转换:base()转换

const 转普通的Iterator不能直接得到

第27条:使用distance和advance将容器的const_iterator转换成iterator

const_iterator无法强制转换为Iterator

第28条:正确理解由reverse_iterator的base()成员函数所产生的Iterator的用法

例子

vector<int> v = {1,2,3,4,5};
​vector<int>::reverse_iterator ri =
find(v.rbegin(),v.rend(),3); vector<int>::iterator i (ri.base()

图例

对于插入来说:直接使用ri和ri.base()是等价的

对于删除来说:如果需要删除ri所指的元素,必须删除i前面的元素:v.erase(--ri.base())

对于vector和string的很多实现来说,Iterator和const_iterator是以内置指针的方式来实现的,这样--ri.base()的表达式就无法通过编译。必须使用 v.erase((++ri).base())

第29条:对于逐个字符的输入请考虑使用istreambuf_iterator

istream_iterator<char>对象使用oprator>>从输入流中读取单个字符:每调用一次operator>>操作符,都需要执行许多附近的操作

istreambuf_iterator<char> 直接从流的缓冲区中读取下一个字符

第五章 算法

第30条:确保目标区间足够大

在vector尾部添加对象

失败案例

成功案例

如果插入的目标容器是vector和string,预先调用reserve,可以提高插入操作的性能。

reserve和insert建议同时调用

需要牢记的是:如果使用的算法需要一个目标区间,那么必须确保目标区间足够大,或者确保它会随着算法的运行而增大。

要在算法执行过程中增大目标区间,使用插入型迭代器

第31条:了解各种与排序有关的选择

nth_element算法:

排序一个区间,使得位置n上的元素正好是全排列情况下的第n个元素,当nth_element返回时,所有按全排列(sort的结果)排在位置n之前的元素也都排在位置n之前,而所有按照全排序规则排在位置n之后的元素则都排在位置n之后。

例子:将最好的20个元素放在容器前列,而不关心他们的具体排序

nth_element,没有对位置1-20中的元素排序

用途:找到一个区间的中间值,或者特定百分比上的值

排序算法的稳定性:排序后,等价的值,先后次序稳定

sort、stable_sort和partial_sort

partition算法

sort、stable_sort、partial_sort和nth_element算法都要求随机访问迭代器

list是唯一一个需要排序却无法使用这些排序算法的容器,只能sort完全排序

对于标准关联容器的元素进行排序无意义

partion和stable_partion只要求双向迭代器就能完成工作

总结

完全排序使用sort

等价性前n个元素排序,使用partial_sort

需要找到前n个元素但又不进行排序,nth_element

标准容器需要按照满足某个特定的条件区分开,使用partion和stable_partion

list中只能实现list::sort,使用其他算法需要别的转化

第32条:如果确实需要删除元素,则需要在remove这一类算法之后调用erase

用remove删除容器元素,容器中的元素数目不会因此减少

remove不是真正意义上的删除,因为它做不到

例子

调用remove之前:

调用之后

remove只是移动了区间中的元素,把不用删除的元素移到了区间的前部

真正删除值

list.remove() 唯一一个命名为remove而确实删除了容器元素的函数

类似的函数:remove_if和unique,同时unique和list::unique和remove的关系一致

第33条:对包含指针的容器使用remove这一类算法时要特别小心

例子:

对于vector<widget*> v

删除操作有问题,在remove_if中就出现

调用之前
remove_if调用之后
erase执行完

容器中存放的是指向动态分配对象指针的时候

避免使用remove和类似算法,使用partition算法时不错的选择

如果是智能指针,问题就不存在

第34条:了解哪些算法要求使用排序的区间作为参数

有些算法需要排序的区间:违反这一规则并不会导致编译器错误,而会导致运行时错误

有些算法在排序的区间上,算法会更加有效

要求排序区间的算法

lower_bound/upper_bound

equal_range

set_union/set_intersection

set_difference/set_symmetric_difference

merge/inplace_merge

includes

不要求区间排序,但一般和排序区域一起使用的

unique/unique_copy

需要用二分法查找数据

binary_search、lower_bound/upper_bound、equal_range

如果区域时排序好的,承诺对数时间的查找效率

只有接受随机迭代器的时候,才能保证此效率
如果不支持随机迭代器,只能保证线性时间

提供线性时间效率的集合操作

set_union/set_intersection、set_difference/set_symmetric_difference

不使用排序区间,无法保证在线性时间完成

提供线性时间的合并和排序联合操作

merge/inplace_merge

读入两个排序的区间、然后合并为一个新的排序区间

不使用排序算法,变得很慢

includes

对于未排序区间有很好行为

unique和unique_copy

unique如果想要删除区间的重复元素,必须保证所有相等的元素都是连续存放的

这样需要

一个区间被排序的含义

区间可能有不同的排序比较函数,算法也有比较函数,需要保证两者有一致的行为

正确用法

第35条:通过mismatch或lexicograhical_compare实现简单的忽略大小写的字符串比较

该条例实现简单的英语字符串的比较

忽略大小写的字符串比较功能

实现类似strcmp接口

实现与operator<类似接口>

strcmp类似接口

第一种实现

比较函数

c1、c2的char强制转换为unsigned char

接口函数
接口实现
mismatch调用前提:必须把短字符串作为第一个区间传入,真实实现是:ciStringCompareImpl
not2(ptr_fun( ciCharCompare)):两个字符匹配时返回true

第二种实现

比较函数同上
接口函数
lexicographicalcompare()是strcmp的泛化版本,可以接收一个判别式

第36条:理解copy_if算法的正确实现

名字包含copy的算法

没有包含copy_if的算法

合理的copy_if算法(不是有效)

正确的实现

第37条:使用accumulate或者for_each进行区间统计

count/count_if

min_element/max_element

accumulate的第一种形式

使用形式

0.0表示保存值的类型

输入职要求为输入迭代器

accumulate的第二种形式

起始的总和值和如何更新值

更新函数

string::size_type 表示容器中用于计数的类型
返回值的类型与函数的一个参数的类型相同

使用形式(使用函数)

使用形式(区间的数值的乘积)

for_each的使用

使用形式

允许有副作用

accumulate直接返回统计结果,for_each返回一个函数对象

第六章 函数子、函数子类、函数及其他

基础

函数子(functor):类似函数的对象

not1和bind2nd的配接器则可以动态生成函数子

第38条:遵循按值传递的原则来设计函数子类

函数指针是按值传递的

按值传递的行为虽然是std默认却可以改变的,但是使用者几乎不会使用

函数对象特点

函数对象需要尽可能小,否则拷贝开销大

函数对象必须是单态的,避免剥离问题(slicing problem)

设计想法

即允许函数对象可能很大并且/或者保留多态性

又需要与STL所采用的按值传递函数子保持一致

实现思路:

将所需的数据和虚函数从函数子类中分离出来,放在新类中

在函数子类中,包含该新类的对象

例子:创建一个包含大量数据并使用了多态性的函数子类

原来(成员包含大量数据,且存在虚函数)

修改后(包含指针,指向另一个实现类,将所有数据和虚函数放在实现类中)

PIMPL技术
注意项
必须以合理的方式支持拷贝动作(必须确保BPFC的拷贝函数正确处理它所指向的BPFCImpl对象)

第39条:确保判别式是"纯函数"

判别式(predicate):返回值为bool类型的函数

纯函数(pure function):返回值仅依赖于其传入参数的函数

纯函数能访问的数据应该仅局限于参数以及常量

判别式类(predicate class):是一个函数子类,它的operator()函数是一个判别式,凡是STL能接受判别式的地方,也可以接受判别式的对象

为什么

接受函数子的STL算法可能先创建函数子的拷贝

违反原则的结果

函数拷贝状态量状态不保持

避免违反原则的简单方法

在判别式类中,将operator()函数的声明为const

上述操作不能完全规避问题,比如mutable数据成员,非const的局部static对象,依然无法限制

最严格的限制是"纯函数",纯函数没有状态

第40条:若一个类是函数子,这应使他可配接

使用函数判别容器成员满足条件容易实现,判断不满足条件可能无法通过正向判别取反来实现

错误做法:

正确做法:

ptr_fun作用:完成类型定义工作

4个标准的函数配接器(not1、not2、bind1st和bind2st)要求特殊的类型定义,提供必要的类型定义的函数对象称为可配接的(adaptable)函数对象,应该尽可能使你的编写函数对象可以配接

不需要知道配接器类型细节,让函数子从特定基类继承,stl提供模板,不能直接继承,需要继承类似实参。

例子:

结构定义:参数类型和返回值类型

如果一个函数子的所有对象都是公开的,一般被定义为struct,否则是class

oprator()参数与结构参数

如果是非指针对象,省略const修饰

如果是指针对象,不能省略修饰

第41条:理解ptr_fun、mem_fun和mem_fun_ref的由来

目的:为了掩饰C++语言中语法不一致问题

函数调用基本语法

例子:

void test(widget& w)

在存放Widget对象的容器中vector<widget> vw; 支持for_each(vw.begin(),vw.end(),test)// 支持调用
如果test为Widget成员函数:for_each(vw.begin(),vw.end(),&idget::test)// 不能通过编译
对于list<widget*> lpw; for_each(lpw.begin(),lpw.end(),&idget::test)// 不能通过编译

STL惯例:函数或者函数对象在被调用的时候,总是使用非成员函数的语法形式

ptr_fun、mem_fun和mem_fun_ref用了调整成员函数,使之能通过语法1被调用

ptr_fun将语法1转换为语法1

mem_fun 将语法3转换为语法1

mem_fun_ref把语法2转换为语法1

第42条:确保less<t>与operator<具有相同的语义>

针对std::less进行特化是常见的,但是针对Widget而特化未必是个合理选择

less和operator < 最好保持一致

第七章 在程序中使用STL

第43条:算法调用优先于手写的循环

算法的内部都是循环:算法基本都需要一个

本该编写循环来完成的任务可以用STL算法来完成

三个理由:

效率:算法通常效率更高

正确性:算法更不容易出错

可维护性:算法更加简洁明了

效率更高:底层有优化

正确性:不需要担心迭代器的正确性问题

例子:给一个数组(C Style)每个元素加41,插入到一个deque的前部。

错误1
错误2:insert调用后使queue的所有迭代器无效
正确1:使用循环:花了太多功夫实现简单的功能
正确2:使用transfrom算法
关键点bind2nd的使用
正确的区间的起点和终点
inserter的使用

可维护性:

算法名称比普遍循环更有意义

例子:

结论

如果功能和一个算法很相似,应该使用算法实现

如果循环简单,使用循环实现

如果循环做的事很多,又复制最好使用算法调用

第44条:容器成员函数优先于同名算法

成员函数速度快

成员函数期望行为一致,而算法不一定

例子:set::find 与find算法

set::find 只需要对数时间,find算法线性时间
std的关联容器的底层使用红黑树而非完全平衡树
算法使用相等性和容器使用等价性来判别

第45条:正确区分count、find、binary_search、lower_bound、upper_bound和equal_range

容器查找工作,如何选择函数

区间是否排序:binary_search、lower_bound、upper_bound和equal_range更快

区间未排序:count和find

count回答的问题:区间是否存在某个特定值,如果存在,有多少拷贝
find回答的问题:区间有这样的值吗?如果有,它在哪里?
find找到第一个匹配就马上返回,count必须到区间尾部

等价性与相等性

count和find使用相等性
binary_search、lower_bound、upper_bound和equal_range使用等价性

排序区间

这个值在区间中吗,如果在,那它在哪?:equal_range
这个值的区间中吗:如果在,它的第一份拷贝在哪,如果不在它该往什么地方插入:lower_bound
错误:lower_bound 使用等价性来搜索

速查表

第46条:考虑使用函数对象而不是函数作为STL算法的参数

将函数对象传递给算法,比传递实际的函数更加高效

函数内联

传递函数的实质是给函数指针

函数指针参数抑制了内联机制

有助于避免一些微妙的、语音本身的缺陷

第47条:避免产生"直写型"(write-only)代码

代码被阅读的次数远远大于它被编写的次数

直写型代码:容易编写、难以阅读与理解

第48条:总是包含(#include)正确的头文件

大部分STL标注容器声明在同名的头文件中

<multiset><multimap>例外,都放在非mulit声明中

大部分算法被声明在<algorithm>

<accumulate><inner_product><adjacent_difference><partial_sum>被声明在<numeric>

迭代器<iterator>

标准函数子和函数子配接器被声明在<functional>

第49条:学会分析与STL相关的编译器诊断信息

string不是一个类是typedef,所有与string类似的类型实际都是basic_string模板的实例

字符串被泛化为一组具有任意字符特征的任意字符类型的序列,它的存储空间由任意分配子来分配

简化诊断消息:用全程替换的方法将basic_string长的类型名替换为文本string,

一个被声明为const的成员函数内部,该类的所有非静态数据成员被自动转换为相应的const类型

vector和string的迭代器通常就是指针

如果诊断信息中出现了back_insert_iterator、front_insert_iterator或者insert_iterator,几乎意味着你错误调用了插入器类型对象

如果你正在使用一个常见的STL组件,但是从错误消息来看,编译器对此一无所知,那么可能是你没有包含正确的头文件

第50条:熟悉与STL相关的Web网站

posted @ 2022-05-10 20:34  nino4love  阅读(44)  评论(0编辑  收藏  举报