Effective STL 读书笔记

2023-03-03,,

Effective STL 读书笔记

标签(空格分隔): 未分类


    慎重选择容器类型

标准STL序列容器: vector、string、deque和list(双向列表)。 标准STL管理容器:

set、multiset、map和multimap。 非标准STL序列容器: slist(单向列表)和rope(重型字符串?)。

非标准STL关联容器:

hash_set、hash_multiset、hash_map和hash_multimap。(c++11引入了unordered_set、unordered_multiset、unordered_map和unordered_multimap,其亦基于hash表,但属于最新的标准关联容器,所以相对hash_*拥有更高的效率和更好的安全性)

标准非STL容器:

数组(c++11中有新的array标准)、bitset、valarray(用于数值计算,但是一般很少使用)、stack、queue和priority_queue(常用于模拟最大堆和最小堆)

其他: vector在某些情况下可以替换string,

vector在某些情况下可以替换标准关联容器。

如何选择容器?需要考虑一下几点:是否要求内存连续(随机访问)?是否有频繁的插入/删除?是否要求容器内元素有序?是否有查找速度的要求?本书作者认为没有一个默认容器

    不要试图编写独立于代码的容器

STL是建立在泛型的基础上,但由于不同容器的特性不同(尤其是迭代器、指针和引用的类型与失效规则不同),支持所有容器的相同接口是不存在的。

同样,typedef可以用来简化一个经常被运用到的容器的定义,并减少维护的成本。但是typedef只是其他类型的同义字,

如果不想暴露所使用的容器类型,可以将所用的容器封装到一个class中。可以在这个class实现额外的功能,并对原始容器的操作进行封装。

    确保容器中的对象拷贝正确而高效

当向容器中加入对象时,存入容器的是你所指定的对象的拷贝;当从容器中取出一个对象时,你所取出的并不是容器中所保存的那份,而是所保存对象的拷贝。拷贝对象是STL的工作方式。

剥离的产生:如果你常见了一个存放基类对象的容器,却向其中插入派生类的对象,那么在派生类对象被拷贝进容器时,派生类的信息将会丢失。

防止剥离问题发生的一个简单方法是存放指针。智能指针是一个诱人的选择。关于指针问题,还可以参考条目7.

    调用empty而不是检查size()返回值是否是0

    如果你香知道容器中是否含有0个元素,那么请使用empty。

    empty对所有的容器都是时间常数操作;而size 对某些list实现,耗费线性时间。

    使用区间成员函数优先于对应的单元素成员函数

    使用区间成员函数可以提高编程和执行的效率。

    区间创建: 所有标准容器都提供了区间创建构造函数

    区间插入: insert 区间版本

    区间删除: erase 区间版本

    区间赋值: assign

    当心c++编译器最烦人的分析机制

    当使用new得指针的容器时,记得在销毁容器钱delete那些指针

    切勿创建包含auto_ptr的容器对象

    慎重选择删除元素的方法

    总结:

    ● 去除一个容器中有特定值的所有对象:

    如果容器是vector、string或deque,使用erase-remove惯用法。

    如果容器是list,使用list::remove。

    如果容器是标准关联容器,使用它的erase成员函数。

    ● 去除一个容器中满足一个特定判定式的所有对象:

    如果容器是vector、string或deque,使用erase-remove_if惯用法。

    如果容器是list,使用list::remove_if。

    如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

    ● 在循环内做某些事情(除了删除对象之外):

    如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你

    的迭代器。

    如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

    了解分配子(allocator)的约定和限制

    理解自定义分配子的合理用法

    切勿对STL容器的线程安全有不切实际的依赖

    vector和string优先于动态分配的数组

    使用reserve来避免不必要的重新分配

> 以下四个成员函数经常被混淆,而且只有vector和string提供了所有这四个函数。
> size():返回该容器中有多少个元素
> capacity():返回该容器利用已分配的内存可以容纳多少个元素,可以用“capacity()-size()”得知容器在不重新分配内存的情况下还能插入多少个元素。
> resize(container::size_type n):强迫改变容器改变到包含n个元素的状态。如果n小于当前size,则容器尾部元素北析构;如果n大于当前size,则通过默认工造函数将新元素添加到容器的尾部;;如果n大于当前capacity,则先重新分配内存。
> reserve(container::size_type n):强制容器吧他的容量变成至少是n,前提是n不小于当前的大小。如果n小于当前容量,vector什么都不做;而string可能把自己的容量见效为size()和n中的最大值。
> 为了避免内存的重新分配和元素拷贝,可以使用reserve成员函数。第一种方式是,若能确切知道大致预计容器中最终有多少元素,可以在容器声明后,立即使用reserve(),预留出适当大小的空间。第二种方式是,先预留足够大的空间,当把所有元素都加入容器后,去除多余的容器。(使用resize()或参考条款17)

    注意string实现的多样性

    了解如何把vector和string传给旧的API

    使用swap技巧除去多余的容量

    vectorv;

    vector(v).swap(v);或者

    string s;

    string(s).swap(s);

    避免使用vector

    可以用deque或者bitset来代替它

    理解相等和等价的区别

    相等是指两个值可以通过==判断;而等价是以“在已排序的区间中对象值的相对顺序”为基础的。

    为包含指针的关联容器指定比较类型

    最好为比较函数准备一个模板类型,像这样:

    struct myless

    {

    template

    bool operator()(T *a1,T *a2)const

    {

    return a1->name < a2->name;

    }

    };

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

    比较函数返回的值表明的是按照该函数定义的排列顺序,一个值是否在另一个之前。相等的值从来没有前后顺序关系,所以,对于相等的值,比较函数应该返回false

    切勿直接修改set或者multisete中的键

    举个例子,如果set或者multiset中的元素是employee类型,直接修改元素的值。某些实现,如gcc编译器就会报错。但据说vs2005下可以通过。

    如果你要总是可以工作而且总是安全地改变set、multiset、

    map或multimap里的元素,按五个简单的步骤去做:

    1. 定位你想要改变的容器元素。如果你不确定最好的方法,条款45提供了关于怎样进行适当搜寻的指

    导。

    2. 拷贝一份要被修改的元素。对map或multimap而言,确定不要把副本的第一个元素声明为const。毕

    竟,你想要改变它!

    3. 修改副本,使它有你想要在容器里的值。

    4. 从容器里删除元素,通常通过调用erase(参见条款9)。

    5. 把新值插入容器。如果新元素在容器的排序顺序中的位置正好相同或相邻于删除的元素,使用insert

    的“提示”形式把插入的效率从对数时间改进到分摊的常数时间。使用你从第一步获得的迭代器作为提示信息。

下边是安全的,可移植的方式编写的:
EmpSet::iterator it = se.find(selectId);
if (it != se.end())
{
Employee e(*it);//第1、2步
e.setName("hahah");//第3步
se.erase(it++);//第4步,可以参加第9条关于非顺序容器的erase
se.insert(it, e);//第5步
}

对于set和multiset,如果直接对容器中的元素做了修改,那么要保证该容器仍然是排序的。

    考虑用排序的vector带代替关联容器

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

    如果要更新一个已有的映射表元素,选择operator[];如果要添加一个新的元素,选择insert。

    熟悉非标准的hash容器

    iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator

    减少混用不同类型的迭代器的机会,尽量用iterator代替const_iterator。从const正确性的角度来看,仅仅为了避免一些可能存在的 STL实现缺陷而放弃使用const_iteraor显得有欠公允。但考虑到在容器类的某些成员函数中指定使用iterator的现状,得出 iterator较之const_iterator更为实用的结论也就不足为奇了。更何况,从实践的角度来看,并不总是值得卷入 const_iterator的麻烦中。

    使用distance和advance将容器的const_iterator转换成iterator

    正确理解由reverse_iterator的base()成员函数所产生的iterator的用法

    对于逐个字符的输入请考虑使用istreambuf_iterator

    确保目标区间足够大

    无论何时,如果所使用的算法需要指定一个目标区间,那么必须确保目标区间足够大,或者确保它会随着算法的运行而增大。要在算法执行过程中增大目标区间,请使用插入型迭代器,比如ostream_iterator,或者由back_inserter,front_insert 和inserter返回的迭代器。

    了解各种与排序有关的选择

    如果需要对vector、string、deque或者数组中的元素执行一次完全排序,那么可以使用sort或者stable_sort.

    如果有一个vector、string、deque或者数组,并且只需要对等价性最前面的n个元素进行排序,那么可以使用partial_sort

    如果有一个vector、string、deque或者数组,并且需要找到第N个位置上的元素,或者需要找到等价性最前面的n个元素但又不必对这n个元素进行排序,那么nth_element正是你所需要的函数

    如果需要讲一个标准序列容器中的元素按照是否满足某个特定的条件区分开来,那么,partition 和stable_partition可能正是你所需要的。

    如果你的数据在一个list中,那么你仍然可以直接调用partiton和stable_partition算法,你可以用list::sort来替代sort和stable_sort算法。但是,如果你需要获得partial_sort货nth_element算法的效果。那么,可以使用一些间接的途径来完成这项任务。

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

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

    解决该问题,可以通过使用引用计数的智能指针,或者在调用remove类算法之前先手工删除指针并将它们置为空

    了解哪些算法需要排序的空间作为参数

    意思就是说要了解哪些算法的调用前提是输入的是已经有序的空间。

    这些算法有:

    binary_search

    lower_bound

    upper_bound

    equal_range

    set_union

    set_intersection

    set_difference

    set_symmetric_difference

    merge inplace_merge

    includes

    下面的算法并不一定需要排序的区间,但通常情况下会与排序区间一起使用:

    unique

    unique_copy

    示例:

    vector<int> intv;
vector<int>::iterator intit;
intv.push_back(1);
intv.push_back(1);
intv.push_back(2);
intv.push_back(3);
intv.push_back(3);
sort(intv.begin(),intv.end());
intit = unique(intv.begin(),intv.end());
intv.erase(intit,intv.end());
cout<<intv.size()<<endl;
for(int k = 0; k<intv.size(); k++)
cout<<intv[k]<<endl;
    通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较
    理解copy_if算法的正确实现
    使用accumulate或者for_each进行区间统计。
    遵循按值传递的原则来设计函数子类
    确保判别式是“纯函数”
>  一个判别式(predicate)是一个返回值为bool类型的函数。一个纯函数(pure
> function)是指返回值仅仅依赖于其参数的函数。
>
> 因为接受函数子的STL算法可能会先创建函数子对象的拷贝,然后使用这个拷贝,因此这一特性的直接反映就是判别式函数必须是纯函数。
    若一个类是函数子,则应使它可配接
    理解ptr_fun、mem_fun和mem_fun_ref的来由
    确保less与operator<具有相同的含义
    算法调用优于手写的循环
>     有三个理由:
>
> 效率:算法通常比程序员自己写的循环效率更高。
>
> STL实现者可以针对具体的容器对算法进行优化;几乎所有的STL算法都使用了复杂的计算机科学算法,有些科学算法非常复杂,并非一般的C++程序员所能够到达。
>
> 正确性:自己写的循环比使用算法容易出错。
>
> 比如迭代器可能会在插入元素后失效。
>
> 可维护性:使用算法的代码通常比手写循环的代码更加简介明了。
>
> 算法的名称表明了它的功能,而for、while和do却不能,每一位专业的C++程序员都应该知道每一个算法所做的事情,看到一个算法就可以知道这段代码的功能,而对于循环只能继续往下看具体的代码才能懂代码的意图

    容器的成员函数优先于同名的算法。

    第一:成员函数往往速度快;

    第二,成员函数通常与容器(特别是关联容器)结合得更紧密(相等和等价的差别,比如对于关联容器,count只能使用相等测试)。

    正确区分count、find、binary_search、lower_bound、upper_bound和equal_range。

    考虑使用函数对象而不是函数指针作为STL算法的参数

> 函数指针抑制了内联机制,而函数对象可以被编译器优化为内联
    避免产生“直写型”(write-only)的代码。
    总是包含(#include)正确的头文件
    学会分析与STL相关的编译器诊断信息
    熟悉STL相关的Web站点

Effective STL 读书笔记的相关教程结束。

《Effective STL 读书笔记.doc》

下载本文的Word格式文档,以方便收藏与打印。