《Effective STL》阅读笔记

一 容器

1 慎重选择容器

  STL提供了多种容器,不同容器的应用场景不同,需要充分理解容器的使用场景,并针对性的选择合适的容器。C++中的容器类型包括:

  • 标准STL序列容器:vector、string、deque、list;
  • 标准STL关联容器:set、multiset、map、multimap;
  • 非标准序列容器:slist、rope;
  • 非标准关联容器:unordered_set、unordered_multiset、unordered_map、unordered_multimap;
  • 标准的非STL容器:array、bitset、valarray、stack、queue、priority_queue。

  上面提到的多种容器一般分类为连续内存容器(vector、string和dequeue)和基于节点的容器(set、list等)。在进行容器选择时可以按照下面的准则进行判断:

  1. 若需要在容器的任意位置插入新元素,选择序列容器;关联容器是不行的;
  2. 若关心容器中的元素是排序的,如果不关心,则散列容器是一个可行的选择;否则要避免散列容器;
  3. 若选择的容器必须是标准C++的一部分,就排除了散列容器、slist和rope;
  4. 若需要随机访问迭代器,则对容器的选择就被限定为vector、deque和string,也可以考虑rope;若需要双向迭代器,那么必须避免slist以及散列容器的一个常见实现;
  5. 发生元素的插入或删除操作时,若需要避免移动容器中原来的元素,则要避免连续内存的容器;
  6. 若容器中数据的布局需要和C兼容,则只能选择vector;
  7. 若元素的查找速度是关键的考虑因素,则要考虑散列容器、排序的vector和标准关联容器;
  8. 若介意容器内部使用了引用计数技术(reference counting),则要避免使用string和rope。当然,若需要某种表示字符串的方法,这是可以考虑vector;
  9. 对于插入和删除操作,若需要事务语义(transactional semantics)即在插入和删除操作失败时,若需要回滚的能力,则使用基于节点的容器;
  10. 若需要使迭代器、指针和引用变为无效的次数最少,则要使用基于节点的容器,因为对这类容器的插入和删除操作从来不会使迭代器、指针和引用变为无效(除非它们指向了一个你正在删除的元素),而针对连续内存容器的插入和删除操作一般会使指向该容器的迭代器、指针和引用变为无效;
  11. 当插入操作仅在容器末尾发生时,deque的迭代器有可能会变为无效。deque是唯一的、迭代器可能会变为无效而指针和引用不会变为无效的STL标准容器。

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

  由于不同类型的容器的具体实现差距过大,即便是相同操作往往也可能有不同的执行结果。比如map默认是有序的,vector默认是无序的,对二者进行insert或者erase操作对容器的结构影响不同,且迭代器、指针和引用等失效的策略不同。因此尝试写独立于容器的代码往往得不偿失。
  但是有时为了隐藏实现,不可避免希望将容器独立于实现代码中可以使用typedef将具体的容器类型封装隐藏,或者将容器隐藏在一个类中将对容器的操作进行抽象,即创建一个适配器满足当前的应用场景。但是这么做的时候也需要仔细考虑,有没有可能过度设计,过度封装。

//使用typedef封装隐藏实现
typedef vector<Object> objVector;
typedef objVector::iterator objVecIterator; 

//将容器隐藏在类中
class ObjVector{
private:
    typedef vector<Object> objVector;
    typedef objVector::iterator objVecIterator; 
public:
    objVecIterator insert(Object obj);
};

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

  STL虽然针对传入的对象类型进行了左右值得优化,但是有些情况下依然可能调用对象的拷贝构造函数。因此在使用STL插入对象时需要明白对象的拷贝的开销,如果开销过大可尝试使用指针进行管理。

4 使用empty而不是size() == 0

  虽然size() == 0empty()语义结果相同,但是二者的语义不同。STL容器基本能够保证empty()的耗时是常数,但是有些容器size()无法保证常数的实现,因此为了保证性能应该 只使用empty()判空。

5 区间成员函数优先于对应的氮元素成员函数

  区间成员函数是指使用迭代器的区间操作容器,而不是使用循环多次调用针对某单个元素的插入或者删除工作。使用区间成员函数能够触发某些优化,比如对于内置类型STL可能直接使用memmove移动内存,防止在插入或者删除时不断改变元素的位置。同时使用区间成员函数代码的可维护性和可读性相比比较好。
  一般设计的区间操作包含:区间创建,区间插入,区间删除,区间赋值。

6 当心C++编译期烦人的分析机制。

  C++编译器在分析声明时尽量将语句解析为函数声明,如果无法解析为函数声明再尝试其他解析方式。如果某一个变量的声明符合函数声明的语法则会被解释为函数声明,导致期望外的语义,比如:

list<char> data(istream_iterator<char>(is), istream_iterator<char>());

  C++中允许省略参数名和在参数名中添加括号,因此上面的声明会被解析为返回值为list<char>,第一个参数为is,第二个参数省略参数名的函数声明。为了避免该种情况可显示声明istream_iteraor<char>两个对象而不是在函数调用中创建,或者将istream_iterator<char>(is)使用括号包含。

list<char> data((istream_iterator<char>(is)), istream_iterator<char>());

istream_iterator<char> is, dataFile;
list<char> data(is, dataFile);

7 如果容器中包含了使用new创建的指针,需要在容器析构前释放其中的内存

  容器只负责维护插入的对象,对象的销毁工作需要用户自己负责。对于栈上的对象因为时自动析构的,用户不用在意,但是对于堆上的对象容器只会销毁对应的指针对象,而指针对象指向的内存容器是不会管理的。因此如果容器中插入了堆指针,则需要在容器销毁前释放对应的内存。

8切勿包含auto_ptr容器对象

  auto_ptr已经被开除智能指针的行列了,目前应该不存在这个问题。

9 慎重选择删除对象的方法

  虽然直接调用对应的函数能够直接删除容器中的对象,但是如果使用更加合理的操作函数能够提高效率。

  • 删除容器中特定值的所有对象:
    • 如果为vector``````string或者deque,则使用erase(remove)方法;
    • 如果是list,使用remove
    • 如果是标准关联容器使用erase
  • 删除容器中满足某个判别式的所有对象:
    • 如果为vector``````string或者deque,则使用erase(remove_if)方法;
    • 如果是list,使用remove_if
    • 如果是标准关联容器使用remove_copy_if或者```swap,或者自己写一个循环;
  • 要在循环内部做某些外的操作:
    • 如果为标准序列容器,则在遍历时进行操作,记得更新迭代器;
    • 如果为标准关联容器,则在遍历时进行操作,更新迭代器后记得自增。

10 了解allocator的约定和限制

  allocator存在一些问题:

  • allocate得到的内存虽然类型为T*但是并不是真正的对象的内存,因为对象还未构造;
  • 为了保证allocator相同类型的等价性,allocator不能包含非静态数据成员,因为该成员会破坏对象的状态;
  • 对于基于节点管理的容器allocator并不真正的负责节点的内存申请,STL实现中而是使用一个other负责节点内存构造。
template<class _Other>
    struct _CXX17_DEPRECATE_OLD_ALLOCATOR_MEMBERS rebind{	
        // convert this type to allocator<_Other>
        using other = allocator<_Other>;
    };

如果希望编写自定义的分配子,需要记住以下内容:

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

11 理解自定义分配子的合理方法

  针对不同的场景需要不同的使用方式。

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

  STL不能完全保证线程安全性,因此在实现线程安全地代码时需要自行针对代码的线程安全性进行自我检查,如果无法保证代码的可重入性则使用锁保证。

二 vector和string

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

  当决定用new来动态分配内存时,这意味着将承担以下责任:

  • 必须确保以后用delete来删除所分配的内存,否则会导致资源泄漏
  • 必须确保使用了正确的delete形式,如果分配了单个对象,则必须使用"delete";如果分配了数组,则需要使用"delete[]"。如果使用了不正确的delete形式,那么结果将是不确定的。在有些平台上,程序会在运行时崩溃;在其他平台上,它会妨碍进一步运行,有时会泄漏资源和破坏内存。
  • 必须确保只delete了一次。如果一次分配被多次delete,结果同样是不确定的。

  如果正在动态地分配数组,那么可能要做更多的工作。为了减轻自己的负担,使用vector或string。另外有些时候string是以引用计数实现的,多线程环境下引用计数带来的性能损耗可能远超过你对所占用内存的容忍度,那么需要尝试改变string,禁用引用计数或者自己实现一个,或者使用vector<char>

14 使用reverse避免不必要的内存分配

  如果场景中需要频繁插入数据,可能预先使用reverse分配对应大小的内存能够减小可能发生的不断申请销毁内存的消耗。
  STL容器中如果内存不足会申请新的内存,将对象拷贝到新内存,然后销毁就内存。

15 注意string的多样性

  1. string的值可能会被引用计数,也可能不会。很多实现在默认情况下会使用引用计数,但它们通常提供了关闭默认选择的方法,往往是通过预处理宏来做到这一点。
  2. string对象大小的范围可以是一个char*指针大小的1倍到7倍。
  3. 创建一个新的字符串值可能需要零次、一次或两次动态分配内存。
  4. string对象可能共享,也可能不共享其大小和容量信息。
  5. string可能支持,也可能不支持针对单个对象的分配子。
  6. 不同的实现对字符内存的最小分配单位有不同的策略。

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

  在使用容器时可能需要将容器数据传递给某些C接口,对于vector可直接将首个元素的地址传递给需要兼容的接口即可,但是需要注意可能容器中没有元素获取第一个元素的地址是非法的。虽然对于某些内置类型迭代器就是指针,但是不建议获取地带器的指针作为数据地址的起始位置,如果需要使用可以使用&*vec.begin()

vector<int> vec;
if(!vec.empty()){
	doSomething(&vec[0]);
}

  对于stinrg内部实现不一定是连续内存,因此不能使用和vector同样的方式获取地址,可以使用STL提供的APIc_str(),该函数返回一个const char*
  另外需要注意的是,在使用vector时如果获取了对应数据的地址,如果以后需要继续使用对应的vector就不应该修改其中元素的数量,修改元素的值是可以的。
  同时,如果希望通过C API初始化容器可以先将对应的数据拷贝到vector中,然后根据vector构建对应的容器总是可行的。

17 使用swap除去多余的容量

  使用swap可以完成一些特定的任务:

  • 原本容器尺寸比较大,现在希望缩减容器的尺寸,vec.swap(ret),该操作并不保证总能去除所有多余的空间,但是总是能够保证缩减尺寸;
  • 清空容器并将容器缩小为最小容量,string().swap(str)

18 避免使用vector<bool>

  由于vector<bool>的实现是利用bit紧促实现的,导致我们无法像正常操作vector那样获取单个元素的地址(因为bit不能取地址),因此在使用vector<bool>时需要注意这些。如果希望像使用普通容器那样使用vector<bool>可以使用deque<bool>或者使用bitset

三 关联容器

19 理解等价和相等的区别

  STL中有两种判断两个元素的值是否相等的方法:

  • 基于相等(以``find算法为代表)。它是以operator==```为基础;
  • 基于等价(以set::insert),默认以operator<为基础(!(w1<w2) && !(w2<w1))
!c.key_comp()(x, y) && !c.key_comp()(y, x)

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

  不提供的话,容器会使用默认的比较函数比较指针的地址值,导致关联容器中元素的存储顺序是按照地址大小存储的。

21 总是让比较函数在相等的情况下返回false

  由于关联容器中比较元素是看元素是否等价,而等价是基于<的,如果比较函数在相等的情况下返回true,就会导致表达式!(w1 < w2) && !(w2 < w1)变成!(w1 <= w2) && !(w2 <= w1),原本相等的元素变成不相等。比如下面的代码会导致set的size是2(vs环境下会直接abort)。

    set<int, less_equal<int>> s;
    s.insert(1);
    s.insert(1);
    printf("size = %d\n", s.size());

  因此在自定义比较函数时,记住比较函数的返回值表明按照比较函数给定的顺序,一个元素是否在另一个元素之前。对于相等的情况总是返回false不会出问题。

22 切勿修改set或者multiset中的键值

  mapmultimap的key是const的因此如果修改是无法通过编译的,但是对于setmultiset标准委员会并未要求其key是const,因此对于不同的STL实现是有可能修改set的key的。如果你需要修改你应该明白,修改会导致代码不具备可移植性的同时破坏容器的结构。(我自己测试发现vs和gcc的set stl实现的迭代器返回的都是const)。
  由于C++提供了const_cast也就意味着你可以强制修改,如果需要修改,需要正确的修改(如下示例)。

auto it = s.find(i);
if(it != s.end()){
  const_cast<Object&>(*it)->changeValue(1);
}

  下面的代码在vs下回abort,而gcc能能够正常编译运行。

class object {
public:
    int val;
    void setValue(int v) {
        val = v;
    }

    bool operator==(const object &ob) {
        return val == ob.val;
    }

    bool operator<(const object &ob) {
        return val < ob.val;
    }
};


int main() {
    set<object> s;
    object ob;
    ob.setValue(1);
    const_cast<object&>(*s.begin()).setValue(2);
    printf("size = %d\n", s.begin()->val);
    //system("pause");
}

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

  • 场景中在查找元素的中途还要插入元素,则使用关联容器比较高效;
  • 场景中容器一旦配置完成很少修改,只需要查找时使用排序的vector更加高效。

  对于关联容器由于其实现是RBTree就导致容器存储的节点包含多个额外的指针,当元素比较多是这些额外的指针就导致比存储同样数据的vector占用更大的内存,占用更大的内存可能导致数据跨页存储,导致缺页错误。

  vector总是能够保证数据的连续性,但是关联容器的节点并不总是能够保证,导致数据存储可能不集中更加容易触发缺页错误。

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

  向关联容器中插入或者更新元素时,可以使用insert或者[],它们都会检查容器中是否包含对应的元素如果包含则更新元素,如果不包含则插入。
  对于插入容器中不存在的值,使用[]等价于,首先创建一个对象,返回对象的引用,然后更新对象的值。而直接调用insert会直接根据值构造对应的对象。

m[1] = 1;
//等价于
typedef map<int, object> objectmap;
pair<objectmap::iterator, bool> ret = m.insert(objectmap::value_type(1, object()));
ret.first->second = 1;

//insert
m.insert(objectmap::value_type(1, 1));

  对于更新容器中的值,[]操作直接更新,而insert需要构造一个pair和对应的对象,并包含临时对象的析构和pair的析构。

m[k]=v;
m.insert(objectmap::value_type(k, v));

  因此对于插入容器中不存在的元素使用insert更加高效;更新容器中存在的元素使用[]更加高效。

25 熟悉非标准的散列容器

  了解并使用hash_set,hash_multiset,hash_maphash_multimap

四 迭代器

26 iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator

  从下面的图中能够看出iterator总是能够通过一些方式转换成const_iteatorreverse_iterator或者const_reverse_iterator,而反之不一定。
  在使用迭代器时尽量使用iterator能够避免一些因为STL不良实现而导致的潜在问题。最直接解决问题的方式是避免混用不同类型的迭代器。
在这里插入图片描述

27 使用distanceadvance将容器的const_iterator转换成iterator

  条款26中提到尽量避免混用迭代器,但是有些场景中可能无法避免,此时可以使用distance或者advanceconst_iterator转换成iterator,用法是advance(i, distance(i, ci))。该方式的效率取决于使用的迭代器,如果是随机访问迭代器,则是O(1),如果是双向迭代器则是O(n)的时间复杂度。

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

  对于以下代码,base()返回的迭代器并不是像我们期望的那样,而是指向了下一个元素,因此在使用base()获取迭代器是需要记得改特性。

    vector<int> vec = { 1,2,3,4,5 };
    auto it = find(vec.rbegin(), vec.rend(), 3);
    printf("rvalue = %d, value = %d\n", *it, *it.base());   //rvalue = 3, value = 4

29 对于逐个字符输入考虑istreambuf_iterator

  istreambuf_iterator相比于istream_iterator能够处理所有字符(不忽略空字符),并且效率要高一些。

五 算法

30 确保目标区间足够大

  如果在操作容器时能够预知操作的大小,使用reverse()对容器扩容能够减少内存分配带来的性能损耗。

31 了解各种与排序相关的选择

  • 如果需要对vectorstringdeque或者数组中的元素执行一次完全排序,那么可以使用sort或者stable_sort
  • 如果有一个vectorstringdeque或数组,并且只需要对等价性最前面的n个元素进行排序,可以使用partial_sort。
  • 如果有一个vectorstringdeque或数组,并且需要找到第n个位置上的元素,或者,需要找到等价性最前面的n个元素但又不必对着n个元素进行排序,那么可以使用nth_element
  • 如果需要将一个标准序列容器中的元素按照是否满足某个特定的条件区分开来,那么可以使用partitionstable_partition
  • 如果数据在一个list中,那么仍然可以调用partitionstable_partition算法;可以利用list::sort来替代sortstable_sort算法。

  除此之外,也可以通过标准的关联容器来保证容器中的元素时钟保持特定的顺序。

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

  remove只是将不需要删除的元素前移,也就是元素虽然删除了但是并不会对容器缩容,因此如果要删除需要使用erase真正删除。类似效果的还有remove_ifunique

    vector<int> vec = { 1,4,3,4,5 };
    for (auto &&val : vec) {
        printf("%d\t", val);
    }

    printf("\n");
    //1       4       3       4       5
    //1       3       5       4       5
    remove(vec.begin(), vec.end(), 4);
    for (auto &&val : vec) {
        printf("%d\t", val);
    }
    printf("\n");

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

  remove并不会析构对象,而是使用赋值替换对象,如果有指针,可能会出现指针丢失造成内存泄漏。一个可行的方案是使用智能指针。

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

STL中那些要求排序区间的STL算法:

  • binary_search
  • lower_bound
  • upper_bound
  • equal_range
  • set_union
  • set_intersection
  • set_difference
  • set_symmetric_difference
  • merge
  • inplace_merge
  • includes
      binary_search、lower_bound、upper_boundequal_range这些算法并不都能够保证log(n)的时间复杂度,只有在接受随机访问迭代器时才能保证。
      set_union、set_intersection、set_difference和set_symmetric_difference这4个算法提供了线性时间效率的集合操作。
      mergeinplace_merge实际上实现了合并和排序的联合操作:它们读入了两个排序的区间,然后合并成一个新的排序区间。线性时间。
      includes用来判断一个区间中的所有对象是否都在另一个区间中。
      uniqueunique_if与上述讨论过的算法有所不同,它们即使对于未排序的区间也有很好的行为。

35 通过mismatchlexicographical_compare实现简单的忽略大小写的字符串比较

// 忽略大小写地比较字符c1和c2,如果c1<c2,返回-1;如果c1>c2,返回1;如果c1==c2,返回0
int ciCharCompare(char c1, char c2)
{
	int lc1 = std::tolower(static_cast<unsigned char>(c1));
	int lc2 = std::tolower(static_cast<unsigned char>(c2));
 
	if (lc1 < lc2) return -1;
	if (lc1 > lc2) return 1;
	return 0;
}
 
int ciStringCompareImpl(const std::string& s1, const std::string& s2)
{
	typedef std::pair<std::string::const_iterator, std::string::const_iterator> PSCI;
	PSCI p = std::mismatch(s1.begin(), s1.end(), s2.begin(), std::not2(std::ptr_fun(ciCharCompare)));
 
	if (p.first == s1.end()) { // 如果为true,要么s1和s2相等,或者s1比s2短
		if (p.second == s2.end()) return 0;
		else return -1;
	}
 
	return ciCharCompare(*p.first, *p.second); // 字符串之间的关系和这两个不匹配的字符之间的关系相同
}
 
int ciStringCompare(const std::string& s1, const std::string& s2)
{
	// 把短的字符串作为第一个区间传入
	if (s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2);
	else return -ciStringCompareImpl(s2, s1);
}
 
// 返回在忽略大小写的情况下,c1是否在c2之前
bool ciCharLess(char c1, char c2)
{
	return std::tolower(static_cast<unsigned char>(c1)) <
		std::tolower(static_cast<unsigned char>(c2));
}
 
bool ciStringCompare2(const std::string& s1, const std::string& s2)
{
	return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
}
 
bool ciStringCompare3(const std::string& s1, const std::string& s2)
{
	// 前提:不考虑国际化支持,也确定字符串中不会包含内嵌的空字符
	return strcmp(s1.c_str(), s2.c_str());
}
 
int test_item_35()
{
	std::string str1{ "xxz" }, str2{ "xxx" };
	fprintf(stdout, "str1:str2: %d\n", ciStringCompare(str1, str2));
	fprintf(stdout, "str1:str2: %d\n", ciStringCompare2(str1, str2));
	fprintf(stdout, "str1:str2: %d\n", ciStringCompare3(str1, str2));
 
	return 0;
}

36 理解copy_if算法的正确实现

  c++11已经支持copy_if

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

  传给accumulate尽量不要带有副作用,即不要带状态。

class printer {
public:
    void operator()(const int val)const {
        printf("%d\t", val);
    }
};
int main() {
    vector<int> vec = { 1,4,3,4,5 };
    auto ret = accumulate(vec.begin(), vec.end(), 0);
    for_each(vec.begin(), vec.end(), printer());
    printf("ret = %d\n", ret);  //17
}

六 函数对象、函数和其他

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

  由于函数对象是值传递因此在使用函数对象时需要注意:

  • 函数对象要尽可能小,避免过大的拷贝开销;
  • 函数对象不支持多态,如果需要支持可以使用组合的方式支持。

39 确保判别式是“纯函数”

  • 判别式:返回值是bool类型的函数;
  • 纯函数:返回值仅仅依赖于参数的函数;
  • 判别式类:函数子类,operator()是一个判别式。

  由于有些STL实现会创建函数对象的副本,如果判别式的函数对象实现本身带有状态会导致多次调用的行为和预期的不一致。解决该问题的最直接思路是将函数修饰为const,但是const并不能完全限制,并且可以访问mutable成员。因此在使用时要避免判别式的函数对象带有状态。

40 若一个类是函数子,则应使它可配接

  下面的例子如果isOdd并未使用ptr_func修饰则无法通过编译,因为确实STL需要的一些类型信息。也就是该函数是不可配接的,可以通过继承自unary_function或者binary_function保证函数对象是可配接的。

bool isOdd(int val) {
    return val % 2;
}

int main() {
    vector<int> vec = { 1,4,3,4,5 };
    auto ret = accumulate(vec.begin(), vec.end(), 0);
    auto it = find_if(vec.begin(), vec.end(), not1(ptr_fun(isOdd)));
    printf("ret = %d, it = %d\n", ret, *it);  //17
    //system("pause");
}

41 理解ptr_fun、men_fun和mem_fun_ref的来由

  • ptr_fun:函数对象容器,将函数指针变成可适配的;
  • mem_fun:对象指针成员函数,将对象的成员函数变成可适配的,支持多态;
  • mem_func_ref:对象成员函数,不支持多态。

  如果不知道什么时候要是用这些,可以在所有用到STL的地方都加上不会有额外的损失,也可以根据编译器的错误判断要不要加。

42 确保less< T >与operator<具有相同的语义

  保证less<T>和```operator<``语义相同,否则可能误导他人。

七 在程序中使用STL

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

  使用算法的优点:

  • 效率;
  • 可读性;
  • 可维护性。

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

  容器的成员数相比于同名的算法一般成员函数和容器的结合更好,效率等方面实现的更好。

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

  在选择具体的查找策略时,由迭代器指定的区间是否是排序的,这是一个至关重要的决定条件。如果区间是排序的,那么通过binary_searchlower_boundupper_boundequal_range,你可以获得更快的查找速度(通常是对数时间的效率)。如果迭代器并没有指定一个排序的区间,那么你的选择范围将局限于countcount_iffind以及find_if,而这些算法仅能提供线性时间的效率。
在这里插入图片描述

46 考虑使用函数对象而不是函数做为STL算法的参数

  将函数对象传递给STL算法往往比实际的函数更加高效。
  在C/C++中并不能真正地将一个函数作为参数传递给另一个函数。如果我们试图将一个函数作为参数进行传递,则编译器会隐式地将它转换成一个指向该函数的指针,并将该指针传递过去。函数指针参数抑制了内联机制。
  以函数对象作为STL算法的参数,这种做法提供了包括效率在内的多种优势。从代码被编译器接受的程度而言,它们更加稳定可靠。当然,普通函数在C++中也是非常实用的,但是就有效使用STL而言,函数对象通常更加实用一些。

47 避免产生“直写型”(write-only)的代码

  当你编写代码的时候,它看似非常直接和简捷,因为它是由某些基本想法(比如,erase-remove习惯用法加上在find中使用reverse_interator的概念)自然而形成的。然而,阅读代码的人却很难将最终的语句还原成它所依据的思路,这就是”直写型的代码”叫法的来历:虽然很容易编写,但是难以阅读和理解。一段代码是否是”直写型”取决于其读者的知识水平。

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

  无论是否需要都要正确的包含头文件,增强可移植性。

  1. 几乎所有的标准STL容器都被声明在与之同名的头文件中,比如vector被声明在<vector>中,list被声明在<list>中,等等。但是<set><map>是个例外,<set>中声明了setmultiset<map>中声明了mapmultimap
  2. 除了4个STL算法以外,其它所有的算法都被声明在<algorithm>中,这4个算法使accumulateinner_productadjacent_differencepartial_sum,它们被声明在头文件<numeric>中。
  3. 特殊类型的迭代器,包括istream_iteratoristreambuf_iterator,被声明在<iterator>中。
  4. 标准的函数子(比如less<T>)和函数子配接器(比如not1bind2nd)被声明在头文件<functional>中。

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

  1. vectorstring的迭代器通常就是指针,所以当错误地使用了iterator的时候,编译器的诊断信息中可能会引用到指针类型。例如,如果源代码中引用了vector<double>::iterator,那么编译器的诊断信息中极有可能就会提及double*指针。
  2. 如果诊断信息中提到了back_insert_iteratorfront_insert_iterator或者insert_iterator,则几乎总是意味着你错误地调用了back_inserterfront_inserter或者inserter。如果你并没有直接调用这些函数,则一定是你所调用的某个函数直接或者间接地调用了这些函数。
  3. 类似地,如果诊断信息中提到了binder1st或者binder2nd,那么你可能是错误地使用了bind1stbind2nd
  4. 输出迭代器(如ostream_iteratorostreambuf_iterator以及那些由back_inserterfront_inserterfront_inserterinserter函数返回的迭代器)在赋值操作符内部完成其输出或者插入工作,所以,如果在使用这些迭代器的时候犯了错误,那么你所看到的错误消息中可能会提到与赋值操作符有关的内容。
  5. 如果你得到的错误消息来源于某一个STL算法的内部实现(例如,引起错误的源代码在<algorithm>中),那也许是你在调用算法的时候使用了错误的类型。例如,你可能使用了不恰当的迭代器类型。
  6. 如果你正在使用一个很常见的STL组件,比如vectorstring或者for_each算法,但是从错误消息来看,编译器好像对此一无所知,那么可能是你没有包含相应的头文件。

  也可以使用工具进行过滤STLFilt: An STL Error Message Decryptor for C++

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值