0%

Effective STL 读书笔记(一)

这是个人阅读Effective STL所做的读书笔记第一部分,包含《Effective STL》条款1~条款10的笔记内容。

导读

只有你足够了解你正写的软件,它运行的环境,以及建立它的场景,才能确定违反该书提出的方针是否合理。多数时候,不是,而且伴随每个条款的讨论解释了为什么。在一些情况里,它是。对指南奴隶般的盲从是不对的,但骑士般的漠视也不对。在一个人冒险之前,你应该保证你有一个好原因。

条款1:仔细选择你的容器

标准STL序列容器:vector、string、deque和list
标准STL关联容器:set、multiset、map和multimap
非标准序列容器slist和rope
非标准关联容器hash_set、hash_multiset、hash_map和hash_multimap

条款2:小心对"容器无关代码"的幻想

由于各种容器在设计之初就没有设计通用的接口,关联容器的insert和erase操作会使指针和引用失效,等问题的存在,在模板中写出“与容器无关代码”是不现实的。
当使用的容器可能在未来发生改变时,使用typedef通常会减少改变容器带来的代码修改;使用typedef也是提高封装的一种手段;

条款3:使容器里对象的拷贝操作轻量而正确

容器内增加对象都会发生对象的拷贝操作,如果对象的拷贝操作需要很大的代价,这样的容器会成为程序的瓶颈;可以考虑使用对象指针的容器或者使对象智能指针的容器;

条款4:用empty来代替检查size()是否为0

简单的理由是对于所有标准容器,empty是一个常数时间的操作,对于一些list实现,size将花费线性时间;
list为链表实现,由于list要求在常数时间内接合另一个list的节点,而计算一个链表的所有元素大小必然需要花费一个遍历的线性时间,所以list的一些实现里,调用size会去遍历求得容器大小;

条款5:尽量使用区间成员函数代替它们的单元素兄弟

区间成员函数更容易书写,拥有更好的可读性,并拥有更好的性能。当要完成一个批量操作时,由于单元素成员函数有多次构造、元素多次移动的问题,区间成员函数较其单元素兄弟拥有更好的性能。常见的区间成员函数:
区间构造:
container::container(inputiterator begin, inputiterator end)
区间插入:
序列容器 void container::insert(iterator position,inputiterator begin,inputiterator end)
关联容器 void container::insert(inputiterator begin,inputiterator end)
区间删除:
序列容器 iterator container::erase(iterator begin,iterator end)
关联容器 void container::erase(iterator begin,iterator end)
区间赋值:
void container:assign(inputiterator begin,inputiterator end)

条款6:警惕C++最令人恼怒的解析

这里提到了一个C++的通用规则,“几乎所有东西都可能被分析成函数声明”;
即在声明变量时应注意声明变量可能被编译器理解为函数声明,这里书上举了个例子

1
list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>());

这里编译器会将这个意图声明list容器的声明理解为:
函数data,返回list<int>;
第一个参数为名为dataFile的istream_iterator<int>;
第二个参数为无名的无参数返回istream_iterator<int>的函数指针。
这里利用“用括号包围一个实参的声明是不合法的,但括号包围一个函数调用是合法的“,将上述改为

1
list<int> data((istream_iterator<int>(dataFile)),istream_iterator<int>());    //并非所有编译器都支持

或者改用更易阅读也不容易出错的版本

1
2
3
4
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

条款7:当使用new得到指针的容器时,记得在销毁容器前delete那些指针

很简单的原理,形如vector<MyClass*>的容器在析构时并不会去调用delete删除所有指针所指内容,所以需要人为去删除这些堆上的数据,否则将出现内存泄露。
这里书上写了个一个用于删除的functor示例:

1
2
3
4
5
6
7
struct DeleteObject {
template<typename T>
void operator()(const T* ptr) const
{
delete ptr;
}
}

利用template operator()实现在删除时调用for_each使用临时DeleteObject对象的operator()作为functor来做删除

1
for_each(dssp.begin(),dssp.end(),DeleteObject());

但是以上这些方式是在new之后操作完成之后进行的删除操作,如果在new所有指针的过程中出现异常,将导致之前所new的指针无法释放,从而导致内存泄露;
所以最高效的方式还是使用智能指针shared_ptr来管理这些指针,在声明指针容器时形如

1
2
typedef boost::shared_ptr<Widget> SPW;
vector<SPW> vwp;

条款8:永不建立auto_ptr的容器

这里书上提到了其中一个理由,当使用sort对auto_ptr的容器进行排序时,由于排序算法将频繁的使用拷贝构造构造临时value_type,而形如auto_ptr<typename>的auto_ptr在拷贝构造时将把typename*指针所属交由构造后的auto_ptr<typename>将自身置为null,对于其他容器操作牵扯到拷贝构造或者赋值操作的地方都会出现该问题,固建立auto_ptr的容器不是一个明智的选择;

条款9:在删除选项中仔细选择

对于连续内存容器(vector、deque或string),最好的删除指定元素的方式是erase-remove,如:

1
2
Container<int> c;
c.erase(remove(c.begin(),c.end(),1963),c.end());

标准关联容器(set、multiset、map、multimap)删除指定值

1
c.erase(1963);

序列容器(vector、string、deque和list)删除指定判断式元素

1
2
bool badValue(int x);
c.erase(remove_if(c.begin(),c.end(),badValue),c.end());
1
c.remove_if(badValue);        // 当c是list时这是去掉badValue返回真的对象的最佳方法

标准关联容器删除指定判断式元素

1
2
3
4
5
6
7
AssocContainer c;                     // c现在是一种标准关联容器 
AssocContainer goodValues; // 用于容纳不删除的值的临时容器
remove_copy_if(c.begin(), c.end(), // 从c拷贝不删除的值到goodValues
inserter(goodValues,
goodValues.end()),
badValue);
c.swap(goodValues); // 交换c和goodValues

上述代码将有复制的时间空间代价;或者直接改由

1
2
3
4
5
6
AssocContainer<int> c;
for(AssocContainer<int>::iterator i = c.begin(); i != c.end();)
{
if(badValue(*i)) c.erase(i++);
else ++i;
}

对于需要在循环里进行其他操作并进行删除时,对于vector、string和deque需要将erase返回的下一个迭代器赋值给当前循环用迭代器

1
2
3
4
5
6
7
8
9
10
11
for(SeqContainer<int>::iterator i = c.begin();
i != c.end();)
{
if (badValue(*i))
{
logFile << "Erasing " << *i << '\n'; //log
i = c.erase(i);
}
else
++i;
}

对于list,对待迭代和删除像vector/string/deque一样或像关联容器一样,两种方式都可以工作于list;

  • 去除一个容器中有特定值的所有对象:
    如果容器是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时记得后置递增它。

条款10:注意分配器的协定和约束

如果你想要写自定义分配器,需要牢记以下几点:

  • 把你的分配器做成一个模板,带有模板参数T,代表你要分配内存的对象类型;
  • 提供pointer和reference的typedef,但是总是让pointer是T*,reference是T&;
  • 决不要给你的分配器设置对象状态,通常,分配器不能有非静态的数据成员;
  • 记得应该传给分配的allocalte成员函数的参数是需要分配的对象个数而不是字节数,也应该记得这些函数返回的是T*指针(通过pointer typedef),记得这些指针所指向的对象未被构造;
  • 一定要提供标准容器依赖的内嵌rebind模板;

这里提到了另外一个问题,对于一些使用链表作为实现的容器来说例如list,list本身并未使用他的allocator为其分配内存,使用的是allocator::rebind<ListNode>::other为其节点分配内存;