Effective STL 笔记

178 阅读11分钟

#Head BLog

本人掘金的专栏文章链接,欢迎阅读!

  1. MIT-6.S081 xv6-labs-2020
  2. MIT-6.S081 xv6 book
  3. 启智好文
  4. C++
  5. Linux

这是本人的 CSDN 博客之分类专栏链接,欢迎点击阅读!

  1. MIT-6.S081 xv6-labs-2020
  2. MIT-6.S081 xv6 book
  3. 启智好文
  4. C++
  5. Linux

#Effective STL

#1 - 容器

条款01 - 慎重选择容器类型

条款02 - 不要试图编写独立于容器类型的代码

试图编写对序列容器和关联容器都适用的代码几乎是毫无意义的,因为序列容器和关联容器有本质上的区别,所以不要尝试大一统

使用封装技术,最简单的方式是通过对容器类型和其迭代器类型使用类型定义( typedef ),

typedef vector<widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;

条款03 - 确保容器中的对象拷贝正确而高效

当要将一个对象存储到容器中时,STL 会将其拷贝一份放入容器;同样取出也是容器中所保存的对象的拷贝(取出难道不是引用嘛?

STL 的工作方式:进去的是拷贝,出来的也是拷贝( copy in, copy out

剥离现象,

拷贝动作会导致剥离,如果向一个存放基类对象的容器中插入派生类对象,那么派生类对象(通过基类的拷贝构造函数)被拷贝进容器时,它所特有的部分(即派生类中的信息)将会丢失

最简单的方式是使用 Widget* 容器,而不是 Widget 容器。拷贝指针的速度非常快,而且不会有剥离现象的发生

条款04 - 调用 empty 而不是检查size()是否为0

empty() 对所有标准容器都是常数时间操作,而对一些 list 的实现, size() 耗费线性时间。因为 list 需要遍历整个数据结构来确认自己含有多少个元素

条款05 - 区间成员函数优先于与之对应的单元素成员函数

对于 vector 而言,在使用 insert() 时,如果是单元素插入,那么会多次导致内存不足进而重新分配内存;反之若采用区间插入,那么就可以一次性申请足够的内存,也就不存在需要多次分配内存的窘况了

请记住,vector 每次的内存重新分配都很费时费力。因为要先申请一块内存,然后把旧的拷贝到新的去,再把旧的对象和内存释放掉!(插入 N 个新元素最多可导致 logN 次新的内存分配)。另外赋值时可以使用区间函数 assign() ,可以少写一些代码并且简洁明了

对于 list 而言,在使用 insert() 时,如果是单元素插入,那么会导致指针的 N 次赋值(连了又要断,断了又要连下一个);反之若采用区间插入,那么就只需一次赋值就能将每个节点串联起来,可以避免不必要的指针赋值

最后,关联容器的区间操作比其对应的单元素操作效率更高

条款06 - 当心 C++ 编译器最烦人的分析机制

尽量不要有连招,这样会使代码变得有二义性,

// 错误示范,小心!结果不会是你所想象的那样
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());

// 正确示范
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

使用命名的迭代器对象与通常的 STL 程序风格相违背,但为了使代码对所有编译器都没有二义性,并且为了代码的可读性,这样做是值得的!

条款07 - 如果容器中包含了通过 new 操作创建的指针,切记在容器对象析构前将指针 delete 掉

// 错误示范01
void f()
{
   vector<Widget*> vwp; 
   for(int i=0; i<N; i++) 
      vwp.push_back(new Widget);
   ...
   // 可能会在这里发生Widget的泄露 异常抛出
   ...
   // 导致无法delete
   for(auto i=vwp.begin(); i!=vwp.end(); ++i)
      delete *i;
}

这仍然不是异常安全的,如果在vector已经被创建而 for_each() 还没有被调用之间有异常抛出,则会有资源泄露发生,

// 错误示范02
template<typename T>
struct DeleteObject : public unary_function<const T*, void> {
   void operator() (const T* ptr) const 
   {
       delete ptr;
   }
}

void f()
{
   ...        //同上
   for_each(vwp.begin(), vwp.end(), DeleteObject<Widget>());
}

正确的做法是借用 shared_ptr 封装对象指针,

void f()
{
   //指向Widget的shared_ptr
   typedef boost::shared_ptr<Widget> SPW;
   
   vector<SPW> vwp;
   for(int i=0; i<N; i++) 
      vwp.push_back(SPW(new Widget));
   ...
   //这里不会有Widget泄露,即使在上面的代码中有异常抛出
}

条款08 - 切勿创建包含 auto_ptr 的容器对象

就像这样,

vector<auto_ptr<Widget>> widgets;

这样会带来很多问题(排序算法中的拷贝问题),因为 auto_ptr 一次只能指向一个对象,所以它会导致上一元素失效(置空),

auto_ptr<Widget> pw1(new Widget);  //pw1指向一个Widget
auto_ptr<Widget> pw2(pw1);  //pw2指向pw1的Widget; pw1被置为NULL

pw1 = pw2;  //现在pw1又指向Widget了; pw2被置为NULL

条款09 - 慎重选择删除元素的方法

erase-remove 用法,

//当c是vector, string, deque时,erase-remove是删除特定值元素的最好办法
c.erase(remove(c.begin(), c.end(), 1963));

remove() 移除(但不删除),移除 [first, last) 之中所有与 value 相等的元素。这一算法并不真正从容器中删除那些元素,而是将每个不与 value 相等的元素轮番赋值给 first 之后的空间。返回值 ForwardIterator 标示出重新整理后的最后一个元素的下一位置(把值等于 value 的元素往 vector 的尾巴处扔,便于 vector 调用 erase 真正删除)。见《STL源码剖析》p356

当 c 是 list 时,成员函数 remove() 是删除特定值元素的最好办法!

标准关联容器( set multiset map multimap ),只能使用成员函数 erase() ,但不能直接使用 erase() 删除,需要递增迭代器。因为当容器(除了 list )中的一个元素被删除时,指向该元素的所有迭代器将变得无效,所以一旦 c.erase(i) 返回,i 就成为无效值!

// 错误示范
AssocContainer<int> c;
...
for(auto i=c.begin(); i!=c.end(); ++i)
   if(badValue(*i)) c.erase(i);

// 正确示范
AssocContainer<int> c;
...
for(auto i=c.begin(); i!=c.end(); )
   if(badValue(*i)) c.erase(i++);
   else ++i;
//对坏值,把当前的i传给erase,递增i是副作用;
//对好值,则简单地递增i

对于标准关联容器,erase() 的返回类型是 void ,所以需要在删除后自行递增。但标准序列容器,

for(auto i=c.begin(); i!=c.end(); )
   if(badValue(*i)) i=c.erase(i);
   else ++i;
//把erase的返回值赋给i,使i的值保持有效

条款10 - 了解分配子( allocator )的约定和限制

条款11 - 理解自定义分配子的合理用法

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

STL 对多线程的支持非常有限,只能保证多个线程读是安全的以及多个线程对不同容器写是安全的。所以为了多线程安全,还是需要通过加锁手段来克服。建议多使用 RAII 思想,例如 std::lock_guard() ,在构造函数时进行加锁,在析构函数中解锁,这样即使遭遇异常,依旧可以保证锁被释放

C++保证,如果有异常抛出,局部对象会被析构

#2 - vector 和 string

条款13 - vector 和string 优先于动态分配的数组

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

每当需要更多空间时,就会调用类似 realloc() 的分配算法

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

需要警示的是,当 vector 和 string 重新分配内存时,容器中的所有迭代器指针引用将全部失效!

条款15 - 注意string实现的多样性

条款16 - 了解如何把vector和string数据传给旧的API

条款17 - 使用 "swap技巧" 除去多余的容量

压缩容量至适当大小,

vector<Contestant>(contestants).swap(contestants);
// 表达式vector<Contestant>(contestants)创建一个临时变量,它是contestants的拷贝:这由vector的拷贝构造函数来完成。
// vector的拷贝构造函数只为所拷贝的元素分配内存,所以这个临时变量没有多余的容量。

在C++11后 vector 可以通过 shrink_to_fit() 成员函数来压缩容量

条款18 - 避免使用 vector<bool>

vector<bool> 不完全满足 STL 容器的要求,所以最好不要使用它,可以使用 deque<bool> 或 bitset 来替代它

#3 - 关联容器

条款19 - 理解相等( equality )和等价( equivalence )的区别

条款20 - 为包含指针的关联容器指定比较类型

集合 s 中包含的是指针,所以 i 不是一个string,而是一个指向 string 的指针,

set<string*> s;
...
for(auto i=s.begin(); i!=s.end(); ++i)
   cout << *i << endl;

比较的只是指针地址的大小而已并不是字符串的内容,

set<string*, less<string*> > s;

如果容器中的对象类型是指针,那么请重写该指针对象的 functor ,

struct StringPtrLess :
   public binary_function<const string*, const string*, bool> {
       bool operator() (const string *ps1, const string *ps2) const {
           return *ps1 < *ps2;
       }
};

typedef set<string*, StringPtrLess> StringPtrSet;
StringPtrSet s;
...

可能会纳闷为什么要创建一个函数子类,而不是简单地为集合写一个比较函数,

bool stringPtrLess(const string* ps1, const string* ps2)
{
   return *ps1 < *ps2;
}
set<string, stringPtrLess> s;

因为 set 模板的三个参数每个都是一个类型而不是函数,所以比较函数不能通过编译

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

见条款20 struct StringPtrLess

条款22 - 切勿直接修改 set 或 multiset 中的键

map<int, string> m;
...
//错!map的键不能修改
m.begin()->first = 10;

multimap<int, string> mm;
...
//错!multimap的键同样不能修改
m.begin()->first = 10;

下面这种情况,雇员 ID 号是 set 中元素的键( key ),其他数据只不过跟这个键绑在一起罢了!!!修改雇员的头衔,没有问题。但不能修改 ID 号!!!因为 ID 号是以此排序的关键,如果修改了就会影响集合的排序方式,进而破坏整个集合,

class Employee {
public:
   //设置雇员的头衔
   void setTitle(const string& title);
   //获取雇员的ID号
   int getID() const;

private:
   //雇员ID
   int m_id;
   //雇员头衔
   string m_title;
};

struct IDLess :
   public binary_function<Employee, Employee, bool> {
      bool operator() (const Employee& lhs, const Employee& rhs) const {
         return lhs.getID() < rhs.getID();
      }
};

typedef set<Employee, IDLess> EmployeeSet;
EmployeeSet s;

如若想修改,正确做法是对副本进行修改,具体为

  1. 定位待修改的元素
  2. 拷贝该元素,称其为副本
  3. 修改副本的值
  4. 删除原数据
  5. 向容器中插入副本

条款23 - 考虑用排序的 vector 替代关联容器

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

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

operator[] 返回一个引用,它指向与 key 相关联的值对象,然后 value 被赋给该引用所指向的对象

如果 key 已经有了相关联的值,则该值被更新,这很直截了当,因为 operator[] 可以返回一个指向该已有的值对象的引用;但如果 key 还没有在映射表中,那就没有 operator[] 可以指向的值对象了

在这种情况下,它使用值类型的默认构造函数创建一个新的对象,然后 operator[] 就能返回一个指向该新对象的引用了,

map<int, Widget> m;
m[1] = 1.50;

因为一开始 m 中什么都没有,所以键 1 没有对应的值对象。调用 operator[] 默认构造了一个 Widget ,作为与键 1 相关联的值,然后返回一个指向该 Widget 的引用,置 key 为 1.50

采用 insert() 同样也可以完成此操作,且效率更高,

typedef map<int, Widget> IntWidgetMap;
pair<IntWidgetMap::iterator, bool> node = m.insert(IntWidgetMap::value_type(1, Widget()));
node.first->second= 1.50;

因为 insert() 就是直接插入操作,无需定位到 key 在映射表中的位置,而 operator[] 需要提前定位到待插入的位置,所以更耗时

条款25 - 熟悉非标准的哈希容器

#4 - 迭代器

条款26 - iterator 优先于 const_iterator 、reverse_iterator 以及 const_reverse_iterator

尽量使用 iterator 而不是 const 或 reverse 型的迭代器,可以使容器的使用更为简单有效,并且可以避免潜在的问题

因为 STL 中很多的 API 只接受 iterator,而不接受 const 和 reverse 版本;而 const 和 reverse 版本能接受 iterator 版本,因为 iterator 可以隐式转换为const、reverse 版本

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

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

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

#5 - 算法

条款30 - 确保目标区间足够大

条款31 - 了解各种与排序有关的选择

  • nth_element 只选出前 n 个最小元素;而 partial_sort 不仅选出前 n 个最小元素,而且还对前 n 个最小元素进行排序操作
  • partial_sortnth_elementsort 都属于非稳定的排序算法
  • 如果在排序时需要保证稳定性,那么可以使用 stable_sort
  • sortstable_sortpartial_sortnth_element 算法都要求随机访问迭代器,所以这些算法只能被应用于 vector 、string 、deque 和数组
  • 如果要对 list 中的对象使用 partial_sortnth_element 算法的话,那么只能通过间接途径来完成:将 list 中的元素拷贝到一个提供随机访问迭代器的容器中,然后对该容器进行排序

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

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

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

条款35 - 通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较

条款36 - 理解copy_if算法的正确实现

条款37 - 使用accumulate或for_each进行区间统计