【绝版C++书籍】《Effective STL》读书笔记

《Effective STL》读书笔记

写在前面

《Effective STL》是一本经典老书, 但它有一个问题就是太老了, 市面上已经没有出版社还在出新书, 对于这本书, 我也是在学校的图书馆无意间看到这本书, 并且是当年比较流行的版本(封面和《Effective C++》一样的版本).
由于它太老了,很多内容其实已经过时.但考虑到其经典性,以及底层原理是永不过时的,还是在反复挣扎中决定快速看一看(206页,也不是很厚).毕竟绝版书,再不看,以后就没有机会看到了.在一开始,就先从知乎上摘了一些可能过时的说明,贴到文章开头,方便查阅比较,少在老旧的细节中走弯路

0《Effective STL》中可能过时的内容

  • 条款2:迭代器可以直接用C++11的auto获取类型
  • 条款5:所有insert函数现在都有了对应的emplace函数
  • 条款6:现在可以用大括号初始化来避免C++'s most vexing parse
  • 条款7:C++11已经引入了std::shared_ptr,不需要用原始指针了
  • 条款8:std::auto_ptr已经移除了,现在的替代品是std::unique_ptr
  • 条款12:C++11自带std::mutex
  • 条款17:C++11有了std::shrink_to_fit
  • 条款18:C++11有了std::bitset
  • 条款21:C++17已经移除了std::binary_function
  • 条款22:C++17可以直接用extract修改关联容器的key
  • 条款24:用emplace代替insert,emplace可以直接传参而不用构造一个对象再传入
  • 条款25:C++11引入了std::unordered_set和std::unordered_map
  • 条款26-29:全部过时,会用auto、std::cbegin、std::cend就行 条款36:C++11有了std::copy_if
  • 条款38-42:全部过时,用C++11的lambda替代仿函数
  • 条款46:用lambda替代仿函数
  • 条款50:https://en.cppreference.com/w/

可能过时部分的来源https://www.zhihu.com/question/50997867

1 容器

STL包含迭代器、算法、函数对象等,但最值得注意的是容器。这一章讲的是内容适用于所有的STL容器的准则。

第1条:慎重选择容器类型。

不同的容器有不同有应用场景,要根据容器的特性和实际需要来选择容器。

第2条:不要试图编写独立于容器类型的代码。

不要把容器类型抽象出来,因为不同的容器类型有不同的特性,它们的函数操作等也都有很大的不同。
如果想要后期更方便地更改容器类型,可以用typedef,比如:

class Widget{...}
template vector<Widget> WidgetContainer;
WidgetContainer cw;

第3条:确保容器中的对象拷贝正确而高效。

容器内保存的是对象的拷贝,而不是其本身,因此要求对象有拷贝构造函数。
如果想要提高其效率,可以考虑把对象的指针放进容器(而不是对象本身),指针的拷贝是非常高效的。

第4条:调用empty而不是检查size()是否为0。

有些容器调用size()的时候会遍历整个容器(比如list),其时间复杂度是O(n),而empty()的调用通常都是O(1)的时间复杂度。

第5条:区间成员函数优先于与之对应的单元素成员函数。

善用区间成员函数(容器算法作用于范围的那些,比如assign()),可以使得更少的代码量(不用写循环)和更高的效率
常用的有

  • 区间创建(container(iterator begin, iterator end)))
  • 区间插入(.insert(begin, end))
  • 区间删除(.erase(begin, end))
  • 区间赋值(.assign(begin, end))

第6条:当心C++编译器最烦人的分析机制。

有些编译器,会把带()的构造,理解为声明了一个函数,比如Widget w()会被理解为声明了一个w()函数,如果在参数列表中,可能会被理解为一个函数名/指针被作为参数

第7条:如果入容器中包含了通过new操作创建的指针,切记在容器对象析构前将delete掉。

如果容器的元素是对象的指针,则用户需要在析构容器之前,把对象指针析构掉,或者使用share_ptr等引用计数智能指针。

第8条:切勿创建包含auto_ptr的容器对象。

由于auto_ptr的机制,使用一些容器中的算法时(比如sort()),会出错,比如把auto_ptr的置为NULL了。
ps: C++11以后已经不使用auto_ptr此条仅供思考

第9条:慎重选择删除元素的方法。

不同的容器适用于不同的删除方法,使用错误的删除方法可能会使得稍后需要用到的指针失效,从而造成错误。
因此正确的做法如下:

  • 删除容器中有特定值的所有对象:
    • 容器是vector、string或deque,则使用erase-remove习惯用法
    • 容器是list,则使用list::remove
    • 容器是一个标准关联容器,则使用它的erase成员函数
  • 删除容器中容易满足特定判定式(条件)的所有对象:
    • 容器是vector、string或deque,则使用erase-remove_if的习惯用法
    • 容器是list,则使用list::remove_if
    • 容器是一个标准关联容器,则使用remove_if和swap,或者写一个循环遍历容器中的元素,记住当把迭代器传给earse时,要对它进行后缀递增
  • 在循环内部做某些(除了删除对象之外的)操作:
    • 容器是一个标准序列容器,则写一个循环来遍历容器中的元素,记得每次调用erase时,要用它的返回值更新迭代器
    • 容器是一个标准关联容器,则写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对迭代器做后缀递增

第10条:了解分配子(allocator)的约定和限制。

  • 你的分配子是一个模板,模板参数T代表你为它分配内存的对象的类型
  • 提供类型定义pointer和reference,但是始终让pointer为T*,reference为T&
  • 千万别让你的分配子拥有随对象而不同的状态(per-object state).通常,分配子不应该有非经费的数据成员
  • 记住,传给分配子的allocate成员函数的是那些要求内存的对象个数,而不是所需的字节数.同时要 记住,这些函数返回T*指针(通过pointer类型定义),即使尚未有T对象被构造出来
  • 一定要提供嵌套的rebind模板, 因为标准容器依赖该模板

第11条:理解自定义分配子的合理用法。

“只要你遵守了同一类型的分配子必须是等价的这一限制要求,那么,当你使用自定义的分配子来控制通用的内存管理策略的时候,或者在聚集成员关系的时候,或者在使用共享内存和其他特殊堆的时候,就不会陷入麻烦”

第10、11条,我不甚了解,总结是引用的书本内容,暂时跳过,mark一下

第12条:切勿对STL容器的线程安全性有不切实际的依赖。

STL并不是线程安全的,我们只能“期望”它,但不能“依赖”它。其部分能够做到:

  • 多个线程是安全的
  • 多个线程对不同的容器做入操作是安全的

如果想要获得异常安全,我们应该在处理之前和处理之后进行加锁。同时,结合现代面向对象的特性,以及避免在释放锁之前的代码抛出异常造成死锁,我们应该把“加锁”和“释放锁”分别放进一个锁类的构造函数和析构函数

2 vector和string

vector和string的使用频率是最高的,这章将介绍什么时候应该使用vector和string,如何提高vector和string的途径,不同string实现的重要区别,研究如何把vector和string的数据传递给只能理解C的API,学会怎样避免不必要的内存分配。并且研究一个不完全的vector——vector

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

如果需要使用new来动态分配数组等,最好用vector或者string来替代,而避免因为使用new带来的一系列注意事项。
vector和string的内存是动态增长的,而且当vector和string被析构的时候,会自动调用元素的析构函数并释放包含这些元素的内存。
vector和string也能很好地兼容老旧的API

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

首先需要注意4个函数:

  • size(),容器中有多少个元素
  • capacity,容器(已分配的内存)可以容纳多少个元素
  • resize(size_type n),强迫容器改变到包含n个元素的状态(可能会引起裁切析构)
  • reserve(size_type n), 强迫容器把它的容量变为>=n,n不小于当前的大小。
    如果容器重新分配大小,原来的迭代器、指针和引用会失效。
    因此,最好在容器刚被构造出来的时候就使用reserve()来重新分配容器的大小,如:
vector<int> v;
v.reserve(1000);			//避免约10次的重新分配(2^10=1024),每次重新分配是前一次大小的2倍
for(int i = 1; i <= 1000; i++)
	v.push_back(i);

通常为了避免不必要的重新分配,方法1是预留适当大小的空间;方法2是先留足够大的空间,再去除多余的空间,其中一个去除多余空间的小窍门见第17条

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

string的实现是多种多样的,不同的实现有不同的特点。大致的区别如下:

  • string的值可能会被引用技术,也可能不会
  • string对象大小的范围可以是一个char*指针的大小的1倍到7倍
  • 创建一个新的字符串值可能需要零次、一次或两次动态分配内存
  • string对象可能共享,也可能不共享其大小和容量信息
  • string可能支持,也可能不支持对单个对象的分配子
  • 不同的实现对字符内存的最小分配单位有不同的策略

虽然string有多种实现,但还是鼓励用户多使用string。只是如果对性能和内存比较敏感的话,可能需要更加详细的了解string的底层机制。

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

string可以使用&v[0]或者.c_str()来获取字符串类型char*,特别说明不要使用v.begin(),而应该使用&*v.begin(),这跟&v[0]产生相同结果。
vector更加灵活,但被调用但历程不能试图改变vector中元素的个数。
无论是vector还是其他的容器,都推荐先把数据复制到vector中,再传给老旧的C API,使用方法如下:

//从C API到容器
//向数组中写入数据。返回被写入的double的个数。
size_t fillArray(double* aArray, size_t arraySize);
vector<double> vd(maxNumDouble);//创建大小为maxNumDoubles的vector
vd.resize(fillArray(&vd[0],vd.size()));
  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值