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

  1. vector、list和deque有着不同的复杂度,vector是默认使用的序列类型。当需要频繁在序列中间做插入和删除操作时,应使用list。当大多数插入和删除操作发生在序列的头部和尾部时,应使用deque。
  2. 可以将容器分为连续内存容器和基于节点的容器两类。连续内存容器把元素存放在一块或多块(动态分配的)内存中,当有新元素插入或已有的元素被删除时,同一块内存中的其他元素要向前或向后移动,以便为新元素让出空间,或者是填充被删除元素所留下的空隙。这种移动会影响到效率和异常安全性。标准的连续内存容器有vector、string和deque,非标准的有rope。
  3. 基于节点的容器在每一个(动态分配的)内存块中只存放一个元素。容器中元素的插入或删除只影响指向节点的指针,而不影响节点本身,所以插入或删除操作时元素是不需要移动的。链表实现的容器list和slist是基于节点的,标准关联容器也是(通常的实现方式是平衡树),非标准的哈希容器使用不同的基于节点的实现。
  4. 是否需要在容器的任意位置插入新元素?需要则选择序列容器,关联容器是不行的。
  5. 是否关心容器中的元素是如何排序的?如果不关心,可以选择哈希容器,否则不能选哈希容器(unordered)。
  6. 需要哪种类型的迭代器?如果是随机访问迭代器,那就只能选择vector、deque和string。如果使用双向迭代器,那么就不能选slist和哈希容器。
  7. 是否希望在插入或删除元素时避免移动元素?如果是,则不能选择连续内存的容器。
  8. 容器的数据布局是否需要和C兼容?如果需要只能选vector。
  9. 元素的查找速度是否是关键的考虑因素?如果是就考虑哈希容器、排序的vector和标准关联容器。
  10. 是否介意容器内部使用引用计数技术,如果是则避免使用string,因为string的实现大多使用了引用计数,可以考虑用vector<char>替代。
  11. 对插入和删除操作需要提供事务语义吗?就是说在插入和删除操作失败时,需要回滚的能力吗?如果需要则使用基于节点的容器。如果是对多个元素的插入操作(针对区间)需要事务语义,则需要选择list,因为在标准容器中,只有list对多个元素的插入操作提供了事务语义。对希望编写异常安全代码的程序员,事务语义尤为重要。使用连续内存的容器也可以获得事务语义,但是要付出性能上的代价,而且代码也不那么直截了当。
  12. 需要使迭代器、指针和引用变为无效的次数最少吗?如果是则选择基于节点的容器,因为对这类容器的插入和删除操作从来不会使迭代器、指针和引用变成无效(除非它们指向一个正在删除的元素)。而对连续内存容器的插入和删除一般会使得指向该容器的迭代器、指针和引用变为无效。

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

  1. 容器是以类型作为参数的,而试图把容器本身作为参数,写出独立于容器类型的代码是不可能实现的。因为不同的容器支持的操作是不同的,即使操作是相同的,但是实现又是不同的,比如带有一个迭代器参数的erase作用于序列容器时,会返回一个新的迭代器,但作用于关联容器则没有返回值。这些限制的根源在于,对于不同类型的序列容器,使迭代器、指针和引用无效的规则是不同的。
  2. 有时候不可避免要从一个容器类型转到另一种,可以使用封装技术来实现。最简单的方式是对容器类型和其迭代器类型使用typedef,如typedef vector<Widget> widgetContainer; typedef widgetContainer::iterator WCIterator;如果想减少在替换容器类型时所需要修改的代码,可以把容器隐藏到一个类中,并尽量减少那些通过类接口可见的、与容器有关的信息。

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

  1. 容器中保存的对象,并不是你提供给容器的那些对象。从容器中取出对象时,也不是容器中保存的那份对象。当通过如insert或push_back之类的操作向容器中加入对象时,存入容器的是该对象的拷贝。当通过如front或back之类的操作从容器中 取出对象时,所得到的是容器中对象的拷贝。进去会拷贝,出来也是拷贝,这就是STL的工作方式。
  2. 当对象被保存到容器中,它经常会进一步被拷贝。当对vector、string或deque进行元素的插入或删除时,现有元素的位置通常会被移动(拷贝)。如果使用下列任何算法,next_permutation或previous_permutation,remove、unique,rotate或reverse等等,那么对象将会被移动(拷贝),这就是STL的工作方式。
  3. 如果向容器中填充的对象拷贝操作很费时,那么向容器中填充对象这一简单操作将会成为程序的性能瓶颈。而且如果这些对象的拷贝有特殊含义,那么把它们放入容器还将不可避免地会产生错误。
  4. 当存在继承关系时,拷贝动作会导致剥离。也就是说,如果创建了一个存放基类对象的容器,却向其中插入派生类的对象,那么派生类对象(通过基类的拷贝构造函数)被拷贝进容器时,它派生类的部分将会丢失。
  5. 使拷贝动作高效、正确,并防止剥离问题发生的一个简单办法就是使容器包含对象指针,而不是对象本身。拷贝指针的速度非常快,而且总是会按你期望的方式进行。如果考虑资源的释放,智能指针是一个很好的选择。

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

  1. 这两个判断在功能上是一样的,但是使用empty而不是size的理由很简单,empty对所有标准容器都是常数时间操作,而一些list实现,size耗费线性时间。

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

  1. 区间成员函数是使用两个迭代器参数来确定成员操作执行的区间,像STL算法一样。区间成员函数和for循环处理单元素成员函数在功能上是相同的,但是在效率和代码清晰度上要更胜一筹。如将一个元素插入到vector中,而它的内存满了,那么vector将申请更大容量的内容,把它的元素从旧内存拷贝到新内存,销毁旧内存中的元素,并释放旧内存,再把要插入的元素插入进来,因此插入n个新元素最多可导致次新内存的分配。
  2. 几乎所有通过插入迭代器(即利用inserter、back_inserter或front_inserter)来操作目标区间的copy调用,都可以/应该被替换为对区间成员函数的调用。
  3. 对于区间创建,所有的标准容器都提供了如下形式的构造函数:container::container(InputIterator begin, InputIterator end);
  4. 对于区间插入,所有的标准序列容器都提供了如下形式的insert:void container::insert(iterator position, InputIterator begin, InputIterator end);表示在position位置插入begin到end的元素。关联容器利用比较函数决定元素的插入位置,所以不需要提供插入位置:void container::insert(InputIterator begin, InputIterator end);
  5. 对于区间删除,所有的标准容器都提供了区间形式的erase操作,但是对于序列容器和关联容器,其返回值不同。序列容器提供了如下的形式:iterator container::erase(iterator begin, iterator end);而关联容器则提供了如下形式:void container::erase(iterator begin, iterator end);为什么会有这种区别呢?据说是关联容器的erase如果返回迭代器(指向被删除元素之后的元素)会导致不可接受的性能负担。
  6. 对于区间赋值,所有的标准容器都提供了区间形式的assign:void container::assign(InputIterator begin, InputIterator end);

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

  1. 分析这样一段程序:有一个存有整数的文件,你想把这些整数拷贝到一个list中
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());//小心!结果不会是你期望的那样

这样做的思路是把一对istream_iterator传入到list的区间构造函数,从而把文件中的整数拷贝到list中,这段代码可以通过编译,但是在运行时什么也不会做,它也不会创建list,这是因为第二条语句并没有声明创建一个list。

从最基本的说起,int f(double d);声明了一个接收double参数,返回int的函数,下面两种形式是做了同样的事:int f(double (d));还有int  f(double);再看int g(double (*pf)());声明了一个函数g,参数是一个函数指针,这个函数指针不接受参数,返回值是double,下面两种形式同样是做了这样的事,int g(double pf());还有int g(double ()),第三种形式省略了函数名,double后有一个空格和(),表示这是一个函数指针。

再来看最开始的例子,第二行其实不是声明了一个变量,而是声明了一个函数,其返回值是list<int>,第一个参数名称是dataFile,类型是istream_iterator<int>,dataFile两边的括号是多余的,会被忽略。第二个参数没有名称,类型是指向不带参数的函数的指针,该函数返回一个istream_iterator<int>。

这符合C++中一个普遍规律,尽可能地解释为函数声明,比如这样一段代码,class Widget{ ... }; Widget w();它并没有声明一个Widget类型的对象w,而是声明了一个名为w的函数。学会识别这类言不达意是成为C++程序员的必经之路。

而为了实现最开始那段代码想要实现的功能,可以为第一个参数参数加上一对括号,即list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>());不幸地是,并不是所有编译器都知道这一点,几乎有一半都会拒绝上述正确的声明形式,而接收最开始错误的声明形式。更好的方式是在data声明中避免使用匿名的istream_iterator对象,将那句代码分成三句来写。

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

  1. STL容器很智能,但是还没有智能到知道是否该释放自己所包含的指针的程度。当你使用指针的容器,而其中的指针应该被删除时,为了避免资源泄露,你应该使用引用计数形式的智能指针(如shared_ptr)代替指针(普通指针不具有异常安全性),或者当容器被析构时手动删除其中的每个指针。
  2. 分析下面的代码:
void doSomething(){vector<Widget*> vwp;for(int i=0; i<n; ++i){vwp.push_back(new Widget);}
}

当vwp的作用域结束,其中的指针并没有释放,造成了资源泄露。如果希望它们被删除,你可能会写出如下的代码:

void doSomething(){vector<Widget*> vwp;for(int i=0; i<n; ++i){vwp.push_back(new Widget);}//do somethingfor(auto i=vwp.begin(); i!=vwp.end(); ++i){delete *i;}
}

这样确实可以,只要你不太挑剔的话。一个问题是这段代码做的事情和for_each相同,但是不如for_each更清晰,另一个问题是这段代码不是异常安全的。如果在vwp填充指针和从其中删除指针的过程中有异常抛出的话,同样会有资源泄露。下面就要克服这两个问题。

首先使用for_each替换上面的for循环,为了做到这一点,需要把delete变成一个函数对象。

template <typename T>
struct DeleteObject : public unary_function<const T*, void>{//条款40会解释为什么有这个继承void operator()(const T* ptr) const {//这是函数对象,接收一个指针作为参数delete ptr;}
}void doSomething(){vector<Widget*> vwp;for(int i=0; i<n; ++i){vwp.push_back(new Widget);}//do somethingfor_each(vwp.begin(), vwp.end(), DeleteObject<Widget>());
}

不幸的是,这种形式的函数对象需要指定要删除的对象类型(这里是Widget),vector中保存的Widget*,DeleteObject当然是要删除Widget*类型的指针。这种形式不仅是多余,同样可能会导致一些难以追踪的错误,比如说有代码很不明智的从string中继承。这样做非常危险,因为同其他标准STL容器一样,string是没有虚析构函数的,从没有虚析构函数的类进行共有继承是一项重要禁忌(Effective C++有详细描述)。先抛开禁忌不谈,假如有人就是写出了这样的代码,在调用for_each(vwp.begin(), vwp.end(), DeleteObject<string>());时,因为通过基类指针删除派生类对象,而基类又没有虚析构函数的话,会产生不确定的行为。所以应该让编译器自己推断出应该删除的指针类型,如下是改进后的代码。

struct DeleteObject {template <typename T>void operator()(const T* ptr) const {//这是函数对象,接收一个指针作为参数delete ptr;}
}void doSomething(){vector<Widget*> vwp;for(int i=0; i<n; ++i){vwp.push_back(new Widget);}//do somethingfor_each(vwp.begin(), vwp.end(), DeleteObject());
}

但是上述代码依然不是类型安全的,如果在创建Widget对象和执行for_each销毁对象之间有异常被抛出,就会有资源泄露,可以使用带引用计数的智能指针来解决这个问题,如下是最终优化的版本。

void doSomething(){typedef std::shared_ptr<Widget> SPW;//SPW表示指向Widget的shared_ptrvector<SPW> vwp;for(int i=0; i<n; ++i){vwp.push_back(SPW(new Widget));}
}

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

  1. 包含auto_ptr的容器是被禁止的,这样的代码不应该被编译通过(可惜目前有些编译器做不到这一点)。首先是因为这样的容器是不可移植的,其次,拷贝一个auto_ptr意味着它指向的对象的所有权被移交到拷入的auto_ptr上,而它自身被置为NULL(相当于是移动),这种拷贝包括调用拷贝构造函数和赋值构造函数的时候。而STL容器中的拷贝遍地都是,举一个例子,调用sort函数给包含auto_ptr<Widget>的vector排序时(定义好了排序函数谓词),在排序过程中,Widget的一个或几个auto_ptr可能会被置为NULL。这是因为sort的常见实现算法是,把容器中的某个元素当作“基准元素”,然后对大于或小于等于该元素的其他元素递归调用排序操作,会把待交换的元素首先赋值给一个临时变量,则自身被置为NULL了,该临时对象超过作用域也会被销毁,从而导致结果vector中的元素被置为NULL。

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

  1. 如果你想删除一个标准容器c中的所有整数元素n,完成这个任务的方式随容器类型的不同而不同。如果是连续内容的容器,最好的方法就是使用erase-remove惯用法:c.erase(remove(c.begin(), c.end(), n), c.end());对于list这种方法适用,但是用成员函数remove更加高效:c.remove(n);而当c是关联容器时,使用任何名为remove的函数都是错误的,因为关联容器没有名为remove的成员函数,正确的方法是调用erase:c.erase(n);
  2. 现在把问题改为删除使得判定式返回true的所有对象,判定式的形式是:bool badValue(int);对于序列容器,改为调用remove_if即可,c.erase(remove_if(c.begin(), c.end(), badValue), c.end());和对于list的c.remove_if(badValue);对于标准关联容器,解决方法就不是那么直截了当了。有两种方法可以解决,一种是易于编码,另一种是效率更高。第一种方法是利用remove_copy_if把需要的值拷贝到一个新容器中,然后交换这两个容器。
AssocContainer<int> c;//标准关联容器
AssocContainer<int> goodValues;//保存不被删除值的临时容器
remove_copy_if(c.begin(), c.end(), inserter(goodValues, goodValues.end()), badValue);
c.swap(goodValues);

这种方法的缺点在于需要拷贝所有不被删除的元素,而我们不希望付出那么多拷贝代价。现在考虑第二种方法,因为关联容器没有提供remove_if的成员函数,所以必须手写一个循环来遍历容器的中元素,并在遍历的过程中删除元素。这个过程看似简单,但是有很多隐藏需要注意的地方,比如下面首先可以想到的代码:

AssocContainer<int> c;
for(auto i=c.begin(); i!=c.end(); ++i){if(badValue(*i)) c.erase(i);
}

这个代码会导致不确定的行为,当关联容器中一个元素被删除时,指向该元素的所有迭代器都将变得无效,即删除动作发生时,i变的无效了,但是后面要执行++i,所以这会导致不确定的行为。为了避免这个问题,要确保在删除i指向的对象之前,有一个迭代器保存了i的下一个遍历的位置,见如下代码:

AssocContainer<int> c;
for(auto i=c.begin(); i!=c.end();){//i不在这里自增if(badValue(*i)) c.erase(i++);//对坏值,先将i传给erase函数,然后i自增,最后erase执行删除else ++i;
}

3.  现在把问题再改一下,不仅要删除使badValue返回true的元素,还要在删除时,向日志中写一条信息。对于关联容器非常  简单,只需要在刚刚的循环中做一些简单的修改。

ofstream logFile;
AssocContainer<int> c;
for(auto i=c.begin(); i!=c.end();){//i不在这里自增if(badValue(*i)){logFile << "Erasing" << *i << "\n";c.erase(i++);//对坏值,先将i传给erase函数,然后i自增,最后erase执行删除}else ++i;
}

但是麻烦的是序列容器,首先不能使用erase-remove惯用法了,因为这样不能向日志中写信息。其次也不能使用像遍历关联容器的循环,因为对于vector、string和deque,调用erase不仅会使指向被删除元素的迭代器无效,也会使被删除元素之后的所有迭代器都无效。在上面的例子中,就是i之后所有的迭代器都无效,i++,++i这些形式当然也没有作用。所以要设计新的循环,关联容器的erase没有返回值,但是序列容器的erase有返回值,要好好利用这个返回值。

ofstream logFile;
AssocContainer<int> c;
for(auto i=c.begin(); i!=c.end();){//i不在这里自增if(badValue(*i)){logFile << "Erasing" << *i << "\n";i = c.erase(i);//此处没有i++的操作,返回值i即指向被删除元素的下一个位置}else ++i;
}

对于list的删除来说,上面两种遍历方式都适用,但一般是采用和vector、string、deque相同的方式。

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

  1. 像new操作符和new[]操作符一样,STL内存分配子负责分配和释放原始内存,但是多数标准容器从不向与之关联的分配子申请内存。
  2. allocator是一个模板,模板参数T表示为它分配内存的对象的类型,提供类型定义pointer和reference,但是始终让pointer为T*,reference为T&。
  3. 千万不能让自定义的allocator拥有随对象而不同的状态,即不应该有非静态成员变量。
  4. 传给allocator的allocate成员函数的参数是对象的个数,而不是所需字节的大小,这一点和new相同,和operator new,malloc相反。同时allocate的返回值是T*指针,即使尚未有T对象被构造出来,而operator new,malloc返回都是void*,void*是用来指向未初始化内存的传统方式。
  5. 自定义的allocator一定要提供嵌套的rebind模板,因为标准容器依赖该模板

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

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

  1. STL容器只提供对多个线程读和多个线程对不同容器写操作是安全的保证。考虑一段单线程可以成功执行的代码。
//将vector中的5都替换成0
vector<int> v;
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if(first5 != v.end()){*first5 = 0;
}

但是在多线程环境中,执行第二行语句后返回的first5的值可能会被改变,导致第三行的判断不准确,甚至一些插入/删除操作会让first5无效。所以必须在操作vector之前,再其上下位置加锁。

vector<int> v;
getMutex(v);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if(first5 != v.end()){*first5 = 0;
}
releaseMutex(v);

更为完善的方法是实现一个Lock类,在构造函数中加锁,在析构函数中释放锁,即RAII。

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

  1. 如果使用动态分配的数组,意味着你需要承担三个责任,首先必须确保最后会调用delete来释放申请的内存,其次是必须确保使用了正确的delete形式,如果是分配了数组的话,应该使用delete[],最后必须确保只delete了一次,而不是多次。而使用vector或者string就不需要承担这样的责任。
  2. 如果当前使用的string是以引用计数的方式实现的,而又运行在多线程环境中,并且string的引用计数实现会影响效率(有时会出现同步控制所花费的时间比避免内存分配和字符拷贝节约下来的时间还要多),那么你至少有三种选择方案,且没有一种是放弃使用string。第一种是检查string实现,看看是否有可能禁止引用计数,通常是通过改变某个预处理变量的值。第二种是寻找或开发不使用引用计数的string实现。第三是考虑使用vector<char>而不是string。

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

  1. vector是内存随元素数量自动增长的容器,在当前容量不够的情况下,vector会重新申请一个更大的内存(一般大小是当前内存大小的两倍),将元素从旧内存拷贝到新内存,最后再析构掉旧元素,释放旧内存。可以发现这个过程还是非常耗时的,reserve成员函数能使重新分配的次数减少到最低,避免重新分配和指针/迭代器/引用失效带来的开销。
  2. vector和string提供以下四个函数:size表明当前容器元素的个数,capacity表明当前容器的空间一共可以容纳多少元素,resize强迫容器包含n个元素,如果当前元素多余n个,则析构多余的元素,如果少于n个,则重新分配内存之后,调用元素的默认构造函数创建新元素添加到元素末尾。reserve把容器容量变为n,如果当前容量比n大,则什么都不做,如果当前容量比n小,则重新分配n的内存。
  3. reserve和resize的区别,reserve只是改变了容器的容量,并没有改变了容器中的大小,resize是改变了容器的大小。举例来说,当对空容器执行resize(n)之后,再执行push_back,后面添加的元素是在n+1的位置,但是对空容器执行reserve(n),再执行push_back,后面添加的元素是在0位置。

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

  1. 考虑sizeof(string)的结果,它的大小是和string的实现相关的,可能是和char*的大小相同,也可能是前者的7倍。但是不同的string实现都会包含如下信息:string的大小,容量,string的值,分配子的一份拷贝,还可能包含对string值的引用计数。下面介绍string四种可能的实现。A实现sizeof(string)的值是指针大小的4倍,B实现的值和指针大小相同,C实现的值也和指针大小相同,D实现的值是指针大小的7倍。D实现不会导致任何动态分配,因为小于15个字符的字符串使用内部的内存,超过15时才会再申请一块内存。A实现和C实现会导致一次内存分配,而B实现会导致2次内存分配,一次是string对象指向的内存,另一次是为该对象申请的字符缓冲区。

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

  1. 对于获取vector中指向其元素的指针,可以使用&v[0],对于string,使用s.c_str(),
  2. v.begin()和&v[0]的辨析,begin()的返回值是一个迭代器,不是指针,当你需要一个指向vector中数据的指针时,永远不应该使用v.begin()。
  3. 相对于vector中元素一定是连续存储,string中的数据不一定存储在连续的内存中,并且内部表示不一定是以空字符结尾的。使用c_str返回一个指向字符串值的指针,且该指针可用于C。但是string内部包含空字符没有关系,对于char*的C API会将第一个空字符作为结束符。
  4. string的c_str所产生的指针并不一定指向字符串数据的内部表示,可能是指向一个字符串数据的不可修改的拷贝,且该拷贝已经做了适当的格式化,以满足C API的要求。
  5. 对于vector,如果你将内部数据的指针传递给C API,并且C API内部改变了vector中元素值的话,通常是没有问题的。但是该函数不能试图改变vector中元素的个数,比如不能在vector未使用的容量中创建新元素,不然vector内部会变的不一致,因为它无法知道自己的正确大小,size()将产生不正确的结果。
  6. 如果想用C API中的元素初始化数组,可以利用vector和数组的内存布局兼容性,向函数传入该vector中元素的存储地址。
size_t fillArray(double* pArray, size_t n);//向pArray处填充n个double,并返回填充的数量
vector<double> v(n);
v.resize(fillArray(&v[0], v.size()));//借助C API填充数据,再把v的大小更新为size

如果想用C API初始化string,可以借助vector<char>完成。

size_t fillArray(char* pArray, size_t n);
vector<char> v(n);
size_t written = fillArray(&v[0], v.size()));
string s(v.begin(), v.begin()+written);

除了初始化string,利用这种方法可以初始化其他容器。

size_t fillArray(double* pArray, size_t n);//向pArray处填充n个double,并返回填充的数量
vector<double> v(n);
v.resize(fillArray(&v[0], v.size()));//借助C API填充数据,再把v的大小更新为sizedeque<double> d(v.begin(), v.end());
list<double> l(v.begin(), v.end());
set<double> s(v.begin(), v.end());

也可以将其他容器的数据传给C API,但是要借助vector的帮助。

size_t doSomething(double* p, size_t n);
set<double> s;
vector<double> v(s.begin(), s.end());
if(!v.empty()) doSomething(&v[0], v.size());

条款17:使用“swap技巧“除去多余的容量

  1. 现在vector中有shrink_to_fit方法,所以这节的swap技巧就不赘述了

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

  1. vector<bool>有两点不对,首先它不是一个STL容器,其次它并不存储bool。
  2. 不能获取vector<bool>首元素的地址,因为为了节省空间,vector存储的不是真正的bool,其中每个bool仅占一个二进制位,一个字节可以容纳8个bool。operator[]也不能使用
  3. 如果需要vector<bool>,可以考虑其替代,第一个是deque<bool>,它是一个STL容器,并且其中存储真正的bool值,当然元素的内存并不连续。第二个是bitset,虽然它不是STL容器,但是它是标准C++库的一部分。它的大小在编译时就确定了,所以不支持插入和删除元素。因为它不是STL容器,所以不支持迭代器。它是一种紧凑表示,每个bool仅占一个二进制位。

条款19:理解相等(equality)和等价(equivalencce)的区别

  1. find算法和set的insert成员函数都需要比较两个值是否相同,find返回指定元素位置的迭代器,set::insert需要在插入前确定元素是否已经存在于set中了。但是这两个函数是不同的方法判断两个值是否相同。find对相同的定义是相等,基于operator==,set::insert对相同的定义是等价,基于operator<。但是相等也不一定意味着对象的所有成员都相等,因为可以重写operator==,制定我们自己的相等。等价是以在已排序区间中对象值的相对顺序为基础的,对于两个关联容器的对象x和y,如果它们都不在另一个的前面,那么称这两个对象具有等价的值,即!(x < y) && !(y < x)成立。但是一般情况下,关联容器的比较函数并不是operator<,甚至不是less,它是用户自定义的判别式。每个关联容器都通过key_comp成员函数使排序判别式可被外界使用,所以更一般的等价是 !c.key_comp() (x, y) && !c.key_comp() (y, x)成立,key_comp()返回一个比较函数。
  2. 为了进一步理解相等和等价的区别,考虑这样一个不区分大小写的set<string>,它认为STL和stl是等价的,下面是实现:
struct CIStringCompare : public binary_function<string, string, bool>{//该基类信息参考条款40bool operator()(const string& lhs, const string& rhs) const{return ciStringCompare(lhs, rhs);//不区分大小写的函数对象,具体实现参考条款35}
}set<string, CIStringCompare> ciss;

ciss就是一个不区分大小写的集合,如果在set中插入STL和stl,只有第一个字符串会被插入,因为第二个和第一个等价。如果使用set的find成员函数查找stl,是可以查找成功的,但是如果使用非成员的find算法就会查找失败,因为STL和stl并不相等。这个例子也印证了条款44中的,优先使用成员函数,而不是与之对应的非成员函数算法。

3.  那么为什么关联容器要使用等价,而不是相等呢?标准容器总是保持排列顺序的,所以每个容器必须有一个比较函数(默认是less),如果关联容器使用相等来决定两个对象是否相同的话,意味着要提供另一个比较函数来判断相等。同样是那个不区分大小写的例子,STL和stl因为不相等,所以都会被插入到set中,但是它们之间的顺序是什么呢?因为排序是用的less,所以之间的顺序是判断不了的。

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

  1. 假定有一个包含string*指针的set,你将一些字符串指针放入其中,你可能期望set会按照字符串的字母顺序来排序,实则不然。如果想要按照期望的形式输出,就必须编写比较函数子类。
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 sps;void print(const string* ps){cout << *ps << endl;
}
for_each(sps.begin(), sps.end(), print);//对sps的每个对象调用print

这里需要注意的是set模板的三个参数都是一个类型,所以给参数传递一个比较函数是不行的,无法通过编译。set不需要函数,它需要一个类型,在内部用它创建函数,所以下面的代码是不能通过编译的。

bool stringPtrLess(const string* ps1, const string* ps2) const {return *ps1 < *ps2;
}
typedef set<string*, stringPtrLess> stringPtrSet;//不能通过编译
stringPtrSet sps;

每当创建包含指针的关联容器时,一般同时需要指定容器的比较类型,所以可以准备一个模板比较函数。

struct dereferenceLess{template <typename PtrType>bool operator()(PtrType pt1, PtrType pt2) const{return *pt1 < *pt2;}
}set<string*, dereferenceLess> sps;

最后一件事,本条款是关于关联容器的,但它也同样适用于其他一些容器,这些容器包含指针,智能指针或迭代器,那么同样需要为这些容器指定一个比较类型。

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

  1. 看一个例子,set<int, less_equal<int> > s;其中less_equal是指定的比较类型,相当于<=。当执行s.insert(10);,容器中有一个10的元素了,然后再执行一次s.insert(10);,容器会先判断内部有没有和10等价的元素,即调用判断 !(10 <= 10) && !(10 <= 10), &&两边都是false,所以结果也是false,意思为容器中没有与当前待插入元素等价的元素!看出问题了吧?相等却不等价。当第二个10被插入到set中,意味着set不是一个set了,就破坏了这个容器。所以一定要保证对关联容器适用的比较函数总是对相等值返回false。
  2. 再看一个例子,就是条款20中的stringPtrLess比较类型,实现的是string*按照字母升序排列,加入我们希望按照字幕降序排序,可以直接将它的判断置反吗?不可以!将判断直接置反得到的新判断是>=,而不是>。
struct stringPtrLess : public binary_function<const string*, const string*, bool> {bool operator()(const string* ps1, const string* ps2) const {return !(*ps1 < *ps2);//这是错误演示}
}

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

  1. 所有的关联容器都会按照一定顺序存储自己的元素,如果改变了关联容器的元素的键,那么新的键可能不在原来的位置上,这就会打破容器的有序性。对于map和multimap很简单,因为键的类型是const的,但是set和multiset中的元素却不是const的。首先考虑一下为什么set中的元素不能是const的,加入有一个雇员类,其中有id和salary两个成员,set是按照id的顺序进行排序的,所以更改salary不会影响雇员对象的位置,正因为可以更改雇员对象,这意味着set中存储的对象不能是const的。正因为更改set中的元素是如此简单,所以才要提醒你,如果你改变了set或multiset中的元素,一定不能改变键部分,如果你改变了,那么可能会破坏容器,再使用该容器将导致不确定的结果。
  2. 尽管set和multiset的元素不是const,但是STL有办法防止其被修改。有种实现会使set<T>::iterator的operator*返回一个const T&,在这种情况下是无法修改set和multiset中的元素的。
  3. 第一条提到可以更改雇员对象中非键的成员变量,但是有的编译器不允许这样的行为,所以修改set或multiset中元素的值是不可移植的代码。如果你不重视移植性,那么就可以更改对象中的非键成员,如果你重视移植性,那么就不能改变set和multiset中的对象。不对不允许改变非键的成员变量,可以先执行const_cast转换之后再改变。但是要注意转换成引用,即const_cast<Employee&>(*i),如果不是引用的话,类型转换首先会产生一个临时对象,在临时对象上做更改salary的动作,而*i本身是并没有被更改的。
  4. 对于修改map或multimap情况又有所不同,map<K, V>或multimap<K, V>包含的是pair<const K, V>类型的元素,如果把const属性去掉,就意味着可以改变键部分。理论上,一种STL实现可以将这样的值卸载一个只读的内存区域,一旦写入后,将由一个系统调用进行写保护,这是若试图修改它,最好的结果就是没有效果。但是如果要坚持C++标准的规则,那就永远不要试图修改map或multimap中的键部分。
  5. 除了强制类型转换,还有一种安全的方式完成更改对象的工作。第一步找到要修改的对象的位置。第二步为将被修改的元素做一份拷贝。第三步修改该拷贝。第四步把容器中的元素删除,通常是使用erase。第五步是把新的值插入到容器中,通常是使用insert。

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

  1. 当你需要一个快速查找功能的数据结构时,一般会立即想到标准关联容器。但是哈希容器的查找速度更快,通常提供常数时间的查找能力,而关联容器时对数时间的查找能力。如果你觉得对数时间的查找能力也可,那么可能排序的vector可能更符合你的要求。这是因为标准关联容器通常被实现为平衡二叉树,这种数据结构对混合的插入、删除和查找做了优化,即它适用于程序插入、删除和查找混在一起,没有明显的阶段的操作。但是很多应用程序使用数据结构的方式并没有这么乱,一般可以明显地分成三个阶段。设置阶段,这个阶段主要是插入和删除,几乎没有查找。查找阶段,这个阶段主要是查找,几乎没有插入和删除。重组阶段,这个阶段主要是插入和删除,几乎没有查找。对于这种方式,vector可能比关联容器提供了更好的性能,但是必须是排序的容器才可以,因为只有对排序的vector容器才能够正确底使用查找算法binary_search、lower_bound和equal_range等。
  2. 下面探究为什么排序的vector在查找性能上会比关联容器要快呢?第一个原因是大小,平衡二叉树存储对象,除了对象本身以外,还通常包含了三个指针,一个指向左儿子,一个指向右儿子,通常还有一个指向父节点,而使用vector存储对象的话,除了对象本身以外,就没有多余的开销了。假设我们的数据足够大,它们被分割后将跨越多个内存页面,但是vector将比关联容器需要更少的页面。第二个原因是vector是连续内存容器,关联容器是基于节点的容器,虽然绝大多数STL实现使用了自定义的内存管理器使得二叉树的节点聚集在相对较少的内存页面,但是如果你的STL并没有这样做,那这些节点就会散布在全部地址空间中,这会导致更多的页面错误。与vector这样的内存连续容器不同,基于节点的容器想保证容器中相邻的元素在物理内存中也是相邻是十分困难的。
  3. 但是需要注意的是,插入和删除操作对于vector来说是昂贵的,尤其是对于需要保持有序的vector。因为每当有元素被插入,新元素之后的元素都要向后移动一个位置,当有元素被删除,删除位置之后的元素都要向前移动一个位置。所以只有删除插入操作不和查找操作混在一起的才考虑使用排序的vector替代关联容器。
  4. 当使用vector替换map或multimap时,存储在vector中的数据必须是pair<K, V>,而不是pair<const K, V>。因为当对vector进行排序时,他的元素的值将通过赋值操作被移动,这意味着pair的两个部分都必须是可以被赋值的。map和multimap在排序时只看元素的键部分,所以你需要为自己的pair写一个自定义的比较函数,因为pair的operator<对pair的两个部分都会检查。而且你需要另一个比较函数来执行查找,用来做排序的比较函数需要两个pair对象作为参数,但是查找的比较函数的一个参数是与键相同类型的对象,另一个是pair对象,只需要一个键值对。另外你不知道传进来的第一个参数是键还是pair,所以实际上需要两个查找的比较函数,一个是假定键部分作为第一个参数传入,另一个是假定pair先传入。

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

  1. map::operator[]的功能是添加和更新,当map中没有[]中指定的键时,则加入一个新pair,如果[]中有指定的键时,则更新这个键的值。假如有一个map的值是Widget对象,键是一个简单类型(如int),Widget有一个默认无参构造函数和一个接受一个参数的有参构造函数和赋值构造函数。当map中没有相应的key时,map::insert是比map::operator[]更快的,因为map::operator[]会构造一个临时对象(调用无参构造函数),再将赋给他新值,而map::insert是直接调用有参构造函数。但是当map中有相应的key时,map::operator[]是比map::insert更快的,因为map::insert需要构造和析构对象,而map::operator[]不需要。

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

  1. SGI的哈希容器是使用了equal_to作为默认的比较函数的,与关联容器默认使用的less不同,SGI的哈希容器通过测试两个对象是否相等,而不是是否等价来决定容器中的两个对象是否有相同的值。

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

  1. STL中所有的标准容器都提供了4种迭代器类型,对容器类container<T>而言,iterator类型相当于T*,const_iterator相当于const T*。reverse_iterator和const_reverse_iterator递增的效果是从容器的尾部反向遍历到头部。
  2. 对于vector容器的insert函数和erase函数,这些函数只接受iterator类型的参数,而不是const_iterator、reverse_iterator或者const_reverse_iterator。下面这张图展示了不同类型迭代器之间的关系。黑色箭头,并且上面未标函数的表示隐式类型转换,标函数的表示显示类型转换,但是需要注意的是,通过base()得到的迭代器或许并非是你期望的迭代器类型。也可以看出想隐式转换const_iterator到iterator是不可行的。从reverse_iterator转换来的iterator在使用之前可能需要进行相应的调整,条款28将更详细地说明这一点。由此可见,尽量使用iterator,而不是const或reverse型的迭代器,可以使容器的使用更为简单有效,并且可以避免潜在的问题。

3.  假设有个iterator i和一个const_iterator ci指向同一个对象,但是在比较这两个迭代器时,即if(i == ci)的结果却是假,甚至不能通过编译,因为这些STL实现将const_iterator的operator==作为成员函数,而不是一个非成员函数,ci不能隐式转成i,但是i可以隐式转成ci,所以判断if(ci == i)是真。避免这种问题最简单的办法是减少混用不同类型的迭代器,尽量使用iterator来代替const_iterator。

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

  1. 首先考虑类型转换达到该条款的目的,包括两种代码,Iter i(ci);和Iter i(const_cast<Iter>(ci));,这两种代码都不能通过编译,原因在于iterator和const_iterator是完全不同的两个类,相当于int和complex<double>之间互转,当然不可能成功。不过对于vector和string来说,上面的代码可能通过编译,因为大多数STL将vector<T>::iterator和vector<T>::const_iterator分别定义为T*和const T*,string::iterator和string::const_iterator定义为char*和const char*,因此对于这两个容器强转可能是成功的,但是即使在这两个容器种,reverse_iterator和const_reverse_iterator仍然是两个类,它们之间是不能强转的
  2. 可以通过distance函数进行转换,代码如下:
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;IntDeque d;
ConstIter ci;
Iter i(d.begin());
advance(i, distance(i, ci));//目前不能通过编译,但是思想是通过distance计算出ci和begin之间的距离,然后移动这么多距离

上面这个程序不能通过编译的原因是distance函数只能接受一种类型的迭代器,而i和ci是两种不同的迭代器。要通过编译最简单的方法是显示指定distance使用的类型,即advance(i, distance<ConstIter>(i, ci));除了达成效率,再考虑这么做的效率如何,它的执行效率取决于你使用的迭代器,对于随机访问迭代器(vector、string和deque),它是常数时间操作,对于双向迭代器(其他所有),它是线性时间操作。

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

  1. 假设通过reverse_iterator查找容器中值为3的元素,ri表示3的位置,但是在调用base()函数将其转换成iterator类型时,因为偏移变成i所指向的位置。假设要在ri的位置插入新元素,我们预期新元素会插入在现在元素3的位置,然后3和其后的元素需要往右移动一个位置,但是因为insert会将新元素插入到迭代器指向位置的前面,而逆序遍历的顺序是由后向前的,所以会将新元素插入在3的“后面”,实际对reverse_iterator来说就是“前面”。所以如果是在reverse_iterator类型ri位置插入,只需要在ir.base()位置插入即可。但是如果是在ri位置删除元素,则需要在ri.base()位置前面的位置执行删除。但是其中还是有坑,请看如下代码:
//这段代码通不过编译
vector<int> v;
vector<int>::reverse_iterator ri = find(v.rbegin(), v.rend(), 3);
v.erase(--ri.base());//iterator的--是左移,reverse_iterator的--是右移

这段代码对于vector和string不能通过编译的原因在于,这两种容器的iterator和const_iterator是以内置指针的方式实现的,所以ri.base()的结果是一个指针,而C和C++都规定了从函数返回的指针不应该被修改。所以必须换个调用方式:v.erase((++ri).base());,先让ri左移再取指针。

条款29:对于逐个字符的输入请考虑使用istreambuf::iterator

  1. 假设你想将一个文本文件中的内容拷贝到一个string对象中,考虑如下的实现方式:
ifstream inputFile("inputData.txt");
string fileData((istream_iterator<char>(inputFile)), istream_iterator<char>());//注意第一个参数用括号包起来

但是这种读取方式是不包含空白字符的,因为istream_iterator使用operator>>完成读操作,而默认情况下operator>>会跳过空白字符,假定你要保留空白字符,可以更改这种默认行为,如下代码:

ifstream inputFile("inputData.txt");
inputFile.unsetf(ios::skipws);
string fileData((istream_iterator<char>(inputFile)), istream_iterator<char>());//注意第一个参数用括号包起来

上述代码是可以完成要求的功能的,但是你会发现它并不够快,istream_iterator内部使用的operator>>实际上执行了格式化输出,这意味着每次调用operator>>操作符,都会执行许多附加的操作。一种更为有效的途径是使用istreambuf_iterator,istreambuf_iterator的使用方法与istream_iterator大致相同,但是istream_iterator<char>使用operator>>从输入流中读取单个字符,而istreambuf_iterator<char>从一个输入流的缓冲区读取下一个字符。使用的代码就是将istream_iterator改成istreambuf_iterator。

ifstream inputFile("inputData.txt");
string fileData((istreambuf_iterator<char>(inputFile)), istreambuf_iterator<char>());//注意第一个参数用括号包起来

同样对于非格式化的逐个字符的输出,也可以考虑使用ostreambuf_iterator替换ostream_iterator。

条款30:确保目标空间足够大

int transmogrify(int x);//该函数功能是根据x生成一个新的值
vector<int> values;
vector<int> results;
transform(values.begin(), values.end(), results.end(), transmogrify);//将values中的所有值经过transmogrify处理,然后放到results的末尾

先看上面一段代码,这段代码是有错误的,因为transform是希望将经过transmogrify处理后的元素放入results中,但是results并没有空间。你可以通过调用back_inserter生成一个迭代器来指定目标区间的起始位置。

int transmogrify(int x);//该函数功能是根据x生成一个新的值
vector<int> values;
vector<int> results;
transform(values.begin(), values.end(), back_inserter(results), transmogrify);//将values中的所有值经过transmogrify处理,然后放到results的末尾

back_inserter返回的迭代器将使得push_back被调用,所以back_inserter适用于所有提供了push_back方法的容器(如所有的序列容器)。如果需要在容器的头部插入可以使用front_inserter,它会在内部调用push_front,所以front_inserter只适用于那些提供了push_front成员函数的容器(如deque和list),但是每次调用push_front,会使得插入的元素顺序和容器中的顺序相反,所以front_inserter不如back_inserter常用。

如果你希望将新元素插入在容器的前端,但是保持插入顺序和在容器中的顺序一致,可以按相反顺序遍历容器。

transform(values.rbegin(), values.rend(), front_inserter(results), transmogrify);

还有一个是inserter,用于将元素插入到指定位置上。

int transmogrify(int x);//该函数功能是根据x生成一个新的值
vector<int> values;
vector<int> results;
transform(values.begin(), values.end(), inserter(results, results.begin() + results.size()/2), transmogrify);//将元素逐个插入在results的中间位置

如果插入操作的目标容器是vector或string,可以在插入之前调用reserve,减少重新分配容器内存带来的开销,但是需要注意的是,reserve只是改变了容器的容量,并没有改变容器的大小,由于赋值总是在两个对象之间,而不是在一个对象和一个未初始化内存块之间进行的,所以下面的调用结果的是不确定的。

results.reserve(values.size());
transform(values.begin(), values.end(), results.end(), transmogrify);//运行错误

同样还是要加上插入迭代器。

results.reserve(values.size());
transform(values.begin(), values.end(), back_inserter(results), transmogrify);//运行正确

如果不想通过插入迭代器的方式调用,就需要先调用resize初始化内存。

results.resize(values.size());
transform(values.begin(), values.end(), results.end(), transmogrify);//运行正确

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

排序算法除了sort以外,还有一些排序算法在特定任务上更胜任。假设你不想将所有的元素排序,只想取前20个元素(Topk问题),可以使用partial_sort。

bool qualityCompare(const Widget& lhs, const Widget& rhs){//返回比较lhs和rhs大小的结果
}
partial_sort(widgets.begin(), widgets.begin()+20, widgets.end(), qualityCompare);//将最大的20个元素放在widgets的前20个位置

如果只关心选出前20个元素,但是并不关心这20个元素本身的排序的话,可以使用nth_element。

nth_element(widgets.begin(), widgets.begin()+19, widgets.end(), qualityCompare);//这里为啥就是19了?

对于有多个相等的元素是如何处理的呢?partial_sort和nth_element在排序等价元素时,有自己的做法,你无法控制它们的行为。还有一个定义是稳定排序,所谓稳定的排序就是排序前如果区间中有两个元素是等价的,在排序后它们的相对位置不会发生变化。但是partial_sort,nth_element和sort都属于非稳定的排序算法,要想稳定排序,可以选择stable_sort。

nth_element还可以用于查找排序后特定位置上的元素(本身并没有执行排序)。假设你希望获得具有中间大小的Widget。

vector<Widget>::iterator begin(widgets.begin());
vector<Widget>::iterator end(widgets.end());
vector<Widget>::iterator goalPosition;//定位感兴趣位置的元素
goalPosition = begin + widgets.size()/2;
nth_element(begin, goalPosition, end, qualityCompare);
//还可以找到排序在1/4位置的元素
goalPosition = begin + 0.25 * widgets.size();
nth_element(begin, goalPosition, end, qualityCompare);

完全排序会有大量的比较和交换工作,可以先删选出符合要求的元素,对剩下的元素再执行排序,筛选可以使用partition。

bool hasQuality(const Widget& w){//判断元素是否大于10
}
//将满足条件的元素移到前部,返回第一个指向不满足条件的迭代器
vector<Widget>::iterator goodEnd = partition(widgets.begin(), widgets.end(), hasQuality);

如果保持相同大小Widget在容器中原有的顺序很重要,使用stable_partition。

sort、stable_sort、partial_sort和nth_element算法都要求随机访问迭代器,所以这些算法只能被用在vector、string、deque和数组上。list需要排序,但是不能使用这些排序算法,可以使用其sort成员函数(稳定的),除了sort的其他排序算法,list可以通过别的方式完成,比如将自己的元素拷贝到vector中再执行相应的函数。

partition和stable_partition只要求双向迭代器,所以所有的标准序列容器都可以使用这两个函数。

最后分析这些排序算法的时间效率,总的来说,做的工作越多,并保持稳定的算法更耗时间。所以排序算法的时间效率排序是:partition<stable_partition<nth_element<partial_sort<sort<stable_sort。

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

  1. 因为删除元素的唯一办法是调用容器的成员函数,而remove并不知道它操作的元素所在的容器,所以remove不可能从容器中删除元素。所以使用remove从容器删除元素,容器中的元素数目并不会因此减少。所以remove不是真正意义上的删除,因为它做不到。
  2. remove真正做的工作就是将不用被删除的元素移到容器的前部,返回的迭代器位置是第一个应该被删除的元素。而且应该被删除的元素此时是出于容器的尾部,但是它们的值已经不是应该被删除的值了,这是因为remove在遍历整个区间的时候,用后一个需要保留的元素覆盖了前面应该被删除的元素的值。
  3. 所以remove应该配合erase使用才可以真正的删除元素。v.erase(remove(v.begin(), v.end(), 99), v.end());
  4. list的remove成员函数是唯一一个名为remove并且真正删除了容器中元素的函数。
  5. 除了remove,remove_if和unique同样属于这种情况,unique是删除容器中的相邻重复元素,如果想真正的删除,同样需要配合调用erase。

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

假设你希望通过erase_remove删除包含指针的容器,这些容器中的指针是指向动态分配内存的对象。

vector<Widget*> v;
v.push_back(new Widget);
v.erase(remove_if(v.begin(), v.end(), not1(mem_fun(&Widget::isCertified))), v.end());//删除指向不符合要求的对象的指针,mem_fun参考条款41

这段代码有两处问题,第一处删除容器中的指针并不意味着释放了指针指向的内存。第二处是remove_if的工作流程会将不符合要求的元素(这里是指针)用后一个符合条件的元素覆盖(赋值),这会导致当前应该被删除的元素指针和后一个不被删除的元素指针指向同一块内存,而应该被释放的对象已经没有指针指向它了,即在执行erase之前已经产生了内存泄露。

如果不能避免对指针容器使用erase-remove,则应该在执行这句代码之前,先将不符合要求的元素释放。

void delUncertified(Widget* pWidget){if(!pWidget->isCertified()){delete pWidget;pWidget = 0;}
}
for_each(v.begin(), v.end(), delUncertified);//删除不符合条件的元素,并将指针置空
v.erase(remove(v.begin(), v.end(), static_cast<Widget*>(0)), v.end());//将空指针转换成指针,这样才能推断出删除的类型

而且上面的代码是将容器中所有的空指针都删除,如果你想保留空指针,那只能自己写循环来删除满足条件的指针了。

如果容器中存放的不是普通指针,而是具有引用计数的智能指针,那么可以直接使用erase-remove惯用法。因为智能指针是一个对象,在超过自己的作用域时,会先析构掉其所指向的对象。

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

  1. 要求排序区间的STL算法:binary_search、lower_bound、upper_bound、equal_range、set_union、set_intersection、set_difference、set_symmetric_difference、merge、inplace_merge、includes。还有的算法不一定要求排序的区间,但是一般都是在排序区间上使用:unique、unique_copy。
  2. 用于查找的算法binary_search、lower_bound、upper_bound、equal_range要求排序的区间,是因为它们用二分法查找数据。集合操作函数set_union、set_intersection、set_difference、set_symmetric_difference是因为只有在排序区间上才可以线性时间完成工作。merge、inplace_merge实现了合并和排序的联合操作,它们读入两个排序的区间,然后将它们合并成一个新的排序区间,只有在排序区间上才可以线性时间完成工作。includes用来判断一个区间的所有元素是否都在另一个区间中,同样是在排序区间才有线性时间效率。unique是删除区间中所有重复的元素,严格来说是相邻重复元素,所以要实现删除所有重复元素,就应该将区间排序。
  3. 还需要注意的是,算法要求的排序和区间的排序,binary_search默认情况是假设区间用<排序(即升序),如果实际区间是降序排列,那binary_search就不能正常工作。
  4. 所有要求排序区间的算法(除unique、unique_copy以外)都使用等价来判断两个对象是否相同,这是与关联容器相同的,unique、unique_copy在默认情况下使用相等来判断两个对象是否相同,当然你可以改变这种默认的行为,只需要给算法提供一个用于比较的谓词。

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

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

copy_if的功能是将容器中符合条件的元素复制到另一个容器中,但是STL并没有提供一个名为copy_if的算法,你需要自己实现。STL中提供了一个remove_copy_if的算法,这个算法允许复制所有使判别式不为真的元素,所以我们可能会考虑通过这个算法来实现自己的copy_if。

//无法完成我们想要的功能
template<typename InputIterator, typename OutputIterator, typename Predicate>
OutputIterator copy_if(InputIterator begin, InputIterator end, OutputIterator destBegin, Predicate p){return remove_copy_if(begin, end, destBegin, not1(p));
}

条款41会解释为什么not1不能被直接用在函数指针上,函数指针必须先用ptr_fun进行转换。为了调用copy_if这个实现,你传入的不仅是一个函数对象,而且还应该是一个可配接(adaptable)的函数对象。下面才是正确的实现

template<typename InputIterator, typename OutputIterator, typename Predicate>
OutputIterator copy_if(InputIterator begin, InputIterator end, OutputIterator destBegin, Predicate p){while(begin != end){if(p(*begin))  *destBegin++ = *begin;++begin;}return destBegin;
}

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

  1. accumulate有两种形式,第一种形式接受两个迭代器和一个初始值,它返回该初始值加上迭代器标识区间的值的总和。
list<double> ld;
double sum = accumulate(ld.begin(), ld.end(), 0.0);

函数最后一个参数0.0很重要,它表示accumulate内部用double变量保存计算的总和,如果是传入0,则使用int来保存。因为accumulate只要求输入迭代器,所以可以接受cin中的数。

cout << accumulate(istream_iterator<int>(cin), istream_iterator<int>(), 0);

第二种形式是接受两个迭代器,一个初值和一个函数指针。accumulate会对区间中所有的元素应用传入的函数。

string::size_type stringSum(string::size_type sum, const string& s){return sum + s.size();//string::size_type在所有的标准容器中就是size_t
}
set<string> ss;
string::size_type length = accumulate(ss.begin(), ss.end(), static_cast<string::size_type>(0), stringSum);

对于计算区间内的乘积就更容易了,可以直接传入标准的multiplies函数子类。

vector<float> vf;
float total = accumulate(vf.begin(), vf.end(), 1.0f, multiplies<float>());

2. 下面看一个更复杂的例子,它要求计算一个区间所有点的平均值。

struct Point{Point(double initX, double initY): x(initX), y(initY) {}double x, y;
};
//定义传入accumulate的计算函数
class PointAverage : public binary_function<Point, Point, Point> {//此处的继承参见条款40
public:
PointAverage() : xSum(0), ySum(0), numPoints(0){}
const Point operator()(const Point& avgSoFar, const Point& p){++numPoints;xSum += p.x;ySum += p.y;return Point(xSum/numPoints, ySum/numPoints);//每次返回当前遍历元素的平均值
}
private:
size_t numPoints;//记录当前读取元素的总数
double xSum;//记录所有横坐标的总和
double ySum;//记录所有纵坐标的总和
};
//调用函数
list<Point> lp;
Point avg = accumulate(lp.begin(), lp.end(), Point(0, 0), PointAverage());

这段代码是可以工作的,但是它有一处并不符合STL标准,即传给accumulate的函数不允许有副作用,而修改numPoints、xSum、ySum的值会带来副作用,所以技术上讲上面代码的结果是不可预测的,但是实践来讲很难想象它不能工作。

3. for_each是另一个统计区间的算法,它也带有三个参数,前两个是区间,后一个是函数,对区间的每一个元素都调用这个函数,但是这个函数只接受一个实参,这个函数也可以用副作用,并且for_each执行完会返回它的函数。for_each也可以完成上面计算一个区间所有点的平均值,但是和accumulate相比不那么直观。

struct Point{Point(double initX, double initY): x(initX), y(initY) {}double x, y;
};
//定义传入accumulate的计算函数
class PointAverage : public unary_function<Point, void> {//此处继承的函数不同了
public:
PointAverage() : xSum(0), ySum(0), numPoints(0){}
void operator()(const Point& p){++numPoints;xSum += p.x;ySum += p.y;return Point(xSum/numPoints, ySum/numPoints);//每次返回当前遍历元素的平均值
}
Point result() const{return Point(xSum/numPoints, ySum/numPoints);
}
private:
size_t numPoints;//记录当前读取元素的总数
double xSum;//记录所有横坐标的总和
double ySum;//记录所有纵坐标的总和
};
//调用函数
list<Point> lp;
Point avg = for_each(lp.begin(), lp.end(), PointAverage()).result();

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

STL函数对象是函数指针的一种抽象和建模形式,所以函数对象在函数之间的传递也是按值传递的,这意味着两件事,一是函数对象应该尽可能小,否则拷贝的开销很昂贵,二是函数对象必须是单态的,也就是说,它们不得使用虚函数,这是因为如果参数类型是父类类型,而实参是子类对象,在传递时就会产生剥离问题,子类对象只有父类部分被传递进来。但是要完全避免这两点有时也是不现实的,所以可以将所需的数据和虚函数从函数子类中分离出来,放到新的类中,然后在函数子类中包含一个指针,指向这个新类的对象。

假如你有一个包含数据和虚函数的函数子类:

template<typename T>
class BPFC : public unary_function<T, void>{
private:
Widget w;
int x;
public:
virtual void operator()(const T& val) const;
};

可以改写成下面的代码:

template <typename T>
class BPFCImpl : public unary_function<T, void>{
private:
Widget w;
int x;
virtual ~BPFCImpl();
virtual void operator()(const T7 val) const;
friend class BPFC<T>;
};template <typename T>
class BPFC : public unary_function<T, void>{
private:
BPFCImpl<T>* pImpl;
public:
void operator()(const T& val) const{pImpl->operator()(val);
}
};

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

  1. 首先解释一下判别式和纯函数的定义,判别式是一个返回值是bool类型的函数,纯函数是指返回值仅仅依赖于其参数的函数,举例来说,f是一个纯函数,x和y是其两个参数,只有当x或者y值发生变化时,f(x, y)的值才能发生变化。还要介绍一个概念是判别式类,它是一个函数子类,其operator()函数是一个判别式。对于用作判别式的函数对象,当它们被拷贝的时候有一个特别需要关注的地方是:接受函数子的STL算法可能会先创建函数子的拷贝,然后存放起来待以后再使用这些拷贝,所以要求判别式函数必须是纯函数。
  2. 考虑下面一个违反上述规定的例子,它简单地忽略传入的参数,只在第三次被调用的时候返回true,然后利用这个判别式来删除vector<Widget>中的第三个Widget:
class BadPredicate : public unary_function<Widget, bool>{
public:
BadPredicate():timesCalled(0){}
bool operator()(const Widget&){return ++timesCalled == 3;
}
private:
size_t timesCalled;
};vector<Widget> vw;
vw.erase(remove_if(vw.begin(), vw.end(), BadPredicate()), vw.end());

上面的代码看似合理,但是结果不仅是删除了vw中第三个元素,同时也删除了第六个元素。下面是remove_if的一种可能实现:

template <typename FwdIterator, typename Predicate>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p){begin = find_if(begin, end, p);if(begin == end) return begin;else{FwdIterator next = begin;return remove_copy_if(++next, end, begin, p);}
}

上面的思路是将BadPredivate()对象传递给find_if,找到符合要求的对象,再将BadPredivate()对象传递给remove_copy_if删除符合要求的对象,但是因为判别式是按值传递的,所以两个核心处理函数的临时函数对象的timesCalled初值都是0。find_if会连续被调用三次,前两次都是begin == end,第三次begin != end,会执行到remove_copy_if,但是此时p中的timesCalled值是0,remove_copy_if也是当被调用到第三次时,才真正执行删除操作,所以remove_if最终删除了两个Widget对象。

即使是将BadPredicate的operator函数声明为const函数也是不够的,因为const成员函数还是可以访问mutable数据成员,非const的局部static对象,非const的类static对象、命名空间中的非const对象以及非const的全局对象,所以判别式应该是一个纯函数。

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

假设一个包含Widget对象指针的list容器,另有一个函数用来判断Widget对象是否有趣,现在想找到第一个符合这样的元素。

list<Widget*> widgetsPtrs;
bool isInteresting(const Widget* pw);list<Widget*>::iterator i = find_if(widgetsPtrs.begin(), widgetsPtr.end(), isInteresting);
if(i != widgetPtrs.end()) {}

反之,如果是想找到第一个不满足isInteresing函数的指针,一个很显而易见的实现却不能通过编译。

list<Widget*>::iterator i = find_if(widgetsPtrs.begin(), widgetsPtr.end(), not(isInteresting));

而应该是下面这种形式:

list<Widget*>::iterator i = find_if(widgetsPtrs.begin(), widgetsPtr.end(), not(ptr_fun(isInteresting)));

这就引出一些问题,为什么在应用not1之前必须先调用ptr_fun呢?ptr_fun做了哪些工作呢?事实上,ptr_fun只是做了一些类型定义的工作,但是这些类型定义是not1所必需的。STL中并非只有not1才有这样的要求,4个标准的函数配接器(not1,not2,bind1st,bind2nd)都要求一些特殊的类型定义。提供这些必要的类型定义的函数对象被称为是可配接的函数对象,反之,如果函数对象缺少这些类型定义,则称为不可配接。提供这些类型定义最简单的方法是让函数子从特定的基类继承,如果函数子类的operator()只有一个实参,应该从std::unary_function继承,如果函数子类的operator()有两个实参,应该从std::binary_function继承。但是由于unary_function和binary_function是STL提供的模板,所以在继承的时候要指定类型参数,对于unary_function需要指定参数类型和返回类型,如std::unary_function<Widget, bool>,对于binary_function需要指定两个参数类型和返回类型,如std::binary_function<Widget, Widget, bool>

条款41:理解ptr_fun、mem_fun和mem_fun_ref的来由

  1. ptr_fun、mem_fun和mem_fun_ref是为了掩盖C++中一个内在的语法不一致的问题。如果有一个函数f和对象x,如果f是非成员函数,那么调用形式是f(x),如果f是x的成员函数,则调用形式是x.f(),如果f是x的成员函数,p是指向x的指针,则调用形式是p->f()。这三种调用形式就带来了语法不一致的问题,比如说有一个存放Widget的容器,vector<Widget> vw,还有一个测试Widget的函数test,那么如果test是非成员函数,则以下调用形式是正确的,for_each(vw.begin(), vw.end(), test),但是如果test是成员函数,或者容器中存放的是Widget指针,则以下调用形式不能通过编译,for_each(vw.begin(), vw.end(), &Widget::test),因为for_each不能根据函数的不同情况进行相应的调整,采用正确的调用形式。
  2. ptr_fun、mem_fun和mem_fun_ref就是用来解决上面的问题,使用for_each(vw.begin(), vw.end(), mem_fun(&Widget::test))就可以通过编译了,此处的mem_fun接受一个成员函数指针,并且返回一个mem_fun_t类型的对象,mem_fun_t是一个函数子类,它拥有该成员函数的指针,并提供operator函数,在operator()中调用了通过参数传递进来的对象上的该成员函数。for_each接受到一个类型是mem_fun_t的对象,该对象中保存了一个指向Widget::test的指针,对于容器中每一个Widget*指针,for_each将会非成员函数的形式调用mem_fun_t对象,然后该对象会通过成员函数调用Widget::test()。
  3. ptr_fun、mem_fun和mem_fun_ref被称为函数对象配接器,mem_fun是用于1中的第三种情况的,即将指针调用成员函数的形式转变成调用非成员函数,mem_fun_ref会产生一个类型为mem_fun_ref_t类型的配接器对象,是用于1中第二种情况的,即将对象调用成员函数的形式转变成调用非成员函数。ptr_fun是用于1中的第一种情况,但是这种情况不用ptr_fun也不会报错,如果你把握不准什么时候用或者不用ptr_fun,那么可以在所有用到它的地方都加上。

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

  1. 假设有一个multiset<Widget>容器,它默认的比较函数是less<Widget>,而less<Widget>在默认情况下会调用operator<来完成multiset的排序,而operator<是按照Widget中成员变量weight来排序的,现在特殊情况下需要一个按Widget中成员变量speed来排序的multiset,一种方法是全特化less<Widget>,但是这种做法并不好,因为用户可能是觉得自己按照weight来排序,但是其实做的却是按照speed来排序,更好的办法是创建一个函数子类,然后用该子类做比较函数,而不是改变less的默认行为。
struct speedCompare : public binary_function<Widget, Widget, bool> {
bool operator()(const Widget& lhs, const Widget& rhs) const{return lhs.maxSpeed() < rhs.maxSpeed();}
};multiset<Widget, speedCompare> widgets;

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

  1. 调用算法在三个方面是优先于手写的循环,第一是效率,使用STL算法往往比自己手写的循环更加高效,第二是正确性,在有些情况下(如插入和删除),自己会考虑不到诸如迭代器失效的情况,而STL算法肯定是正确的,第三是可维护性,只要那个人同样熟悉STL算法,那么你的代码就清晰明了,但是如果是阅读你手写的循环,可能就会花费更多的时间。
  2. 但是如果你想表明在在一次迭代中该完成什么工作,那么有时候使用循环是比使用算法更为清晰的。

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

  1. 容器的成员函数优先于同名的算法的理由有两条,第一条是成员函数往往速度更快,像与list成员函数同名的算法,remove、remove_if、unique、sort、merge以及reverse这些算法无一例外都需要拷贝list中的对象,而其对应的成员函数并不需要。第二条是成员函数通常与容器(特别是关联容器)结合的更紧密,所以有时即使算法和成员函数拥有相同的名称,所做的事情不完全相同。比如说STL算法是以相等性来判断两个对象是否具有相同的值,而关联容器则使用等价性来判断是否相同,这就可能导致使用成员函数可以找到想查找的值,但是使用算法却不可以。

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

  1. 假设你要在容器中查找一些信息,标题列出的几个函数应该怎么选择呢?首先应该考虑区间是否是排序的,如果是排序的,则binary_search、lower_bound、upper_bound和equal_range具有更快的查找速度,如果不是排序的,那么你只能选择count、count_if、find、find_if,这些算法仅提供线性时间的查找效率。但是这些函数还是有区别的,count、count_if、find、find_if使用相等性进行查找,binary_search、lower_bound、upper_bound和equal_range使用等价性进行查找。
  2. 考虑count和find的区别,count表示区间是否存在待查找的值,如果存在有多少个?find表示区间是否存在待查找的值,如果存在它在哪里?假设你只想知道区间中是否存在待查找的值,如果是使用count会更方便一些,因为使用find还需要比较find返回的指针是否是容器的end(),但是因为find找到第一个符合查找的值就会返回,count一定会遍历到容器的末尾,所以find的效率更高。
  3. 当你的区间是排序的,那么你就可以使用对数时间查找的四种方法。与标准C/C++函数库中的bsearch不同的是,binary_search仅判断区间是否存在待查找的值(返回值是bool),如果你还想知道待查找值得位置,可以使用其他三种方法,先考虑lower_bound,lower_bound查找特定值会返回一个迭代器,同样你需要判断这个迭代器的结果,除了要判断这个迭代器是否是end(),还要判断其指向的值是否是待查找的值,所以很多人会这么写:
vector<Widget>::iterator i = lower_bound(vw.begin(), vw.end(), w);
if(i != vw.end() && *i == w) { ... }//这里有一个错误

这里*i == w是一个相等性测试,但是lower_bound使用等价性进行搜索的,虽然大多数情况下等价性测试和相等性测试的结果相同,但是条款19也说明了违背这个情况也很常见,所以正确也更方便的方式是使用equal_range,equal_range会返回一对迭代器,第一个迭代器等于lower_bound返回的迭代器,第二个迭代器等于upper_bound返回的迭代器,equal_range返回了一个子区间,其中的值与待查找的值等价,如果返回的两个迭代器相同,等说明查找的区间没有待查找的值,而子区间的长度就是等价值的个数,可以使用distance得到。

4.  再考虑lower_bound和upper_bound的使用场景,这次我不是希望查找某个元素,而是想找到第一个比特定值大的元素的位置,这个特定值可能不在容器中,那么可以使用lower_bound,如果希望找到第一个大于或等于特定值的元素的位置,那么可以使用upper_bound。

条款46:考虑使用函数对象而不是函数作为STL算法的参数

  1. 第一个原因是因为效率。随着抽象程度的提高,所生成代码的效率是在降低的,比如几乎在所有情况下,操作包含一个double的对象比直接操作double都是要慢的,但是可能令你感到惊讶的是,将函数对象传递给STL算法往往比传递函数更加高效。比如说你想生成一个降序排列的vector<double>,你可以使用函数对象,即greater<double>,也可以使用一个自定义的比较函数,而第一种情况往往是更快的,这是因为如果一个函数对象的operator()函数被声明是内联的(可以通过inline显式声明,也可以定义在类中隐式声明),那么它的函数体可被编译器直接优化,但是传递函数指针则不行,编译器不能优化。
  2. 第二个原因是正确性,有的时候STL平台会拒绝一些完全合法的代码,如下:
//完全合法的代码但是不能通过编译
set<string> s;
transform(s.begin(), s.end(), ostream_iterator<string::size_type>(cout, "\n"), mem_fun_ref(&string::size));//可以改用函数对象的形式
struct stringSize : public unary_function<string, string::size_type>{
string::size_type operator()(const string& s) const {return s.size();
}
};

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

  1. 所谓”直写型“的代码是指一行代码中有过于复杂的嵌套函数调用,可能对于编写代码的人,这行代码看似非常直接和简单,但是对于阅读代码的人则显得难以理解。所以在遇到这种写出“直写型”代码时,应该将其拆分成多行代码,或者使用typedef起别名的形式,让代码更易于阅读。

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

  1. 有的时候即使漏掉了必要的头文件,程序同样可以编译,这是因为C++标准并没有规定标准库中头文件之间的相互包含关系,这就导致了某个头文件包含了其他头文件,如<vector>包含了<string>。但是这种程序往往是不可移植的,即使在你的平台上可以编译,但是在其他平台上就可能会编译不过,所以解决此类问题的一条原则就是总是include必要的头文件。

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

  1. 在程序编译或者运行出错时,有时编译器给出的诊断信息非常混乱和难以阅读。对于这些信息可以使用同义词替换的方法进行简化,比如说将std::basic_string<>替换成string,将std::map<>替换成map,将看不懂的STL内部模板std::_Tree替换成something。

条款50:熟悉与STL相关的Web站点

  1. 三个网站:SGI STL,STLport和Boost

《Effective STL》条款解读相关推荐

  1. Effective STL 条款30

    http://blog.csdn.net/amin2001/article/details/8063 STL容器在被添加时(通过insert.push_front.push_back等)自动扩展它们自 ...

  2. effective stl 条款15 小心string实现的多样性

    实际上每个string实现都容纳了下面的信息: ● 字符串的大小,也就是它包含的字符的数目. ● 容纳字符串字符的内存容量.(字符串大小和容量之间差别的回顾,参见条款14.) ● 这个字符串的值,也就 ...

  3. 两本小书的命运 --- 记《Effective STL》和《The Art Of Deception》两本书的出版翻译过程

    这两年来,时常听到读者或者朋友们问我"最近还有新书要出版吗",我的回答是,有两本拖了很久的书快要出版了.我乐观地估计,这两本书在2005年都能出版,然而,不幸的是,这两本书都未能如 ...

  4. 《Effective STL》学习笔记(第一部分)

    本书从STL应用出发,介绍了在项目中应该怎样正确高效的使用STL.本书共有7个小节50个条款,分别为 (1) 容器:占12个条款,主要介绍了所有容器的共同指导法则 (2) vector和string: ...

  5. Effective C++ 条款1、2、3、4

    以下内容均来自Scott Meyers大师所著Effective C++ version3,如有错误地方,欢迎指正!相互学习,促进!! 条款1 视C++为一个语言联邦 理解C++,须认识其主要的次语言 ...

  6. effective c++条款44 将与参数无关的代码抽离templates

    effective c++条款44 将与参数无关的代码抽离templates 首先了解这个条款的含义:使用template可能导致代码膨胀,二进制码会带着重复(或者几乎重复)的代码.数据,或两者.其结 ...

  7. Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

    Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously) 条款39:明智而审慎地使用private继承 1.pr ...

  8. Effective C++条款20:宁以pass-by-reference-to-const替换pass-by-value

    Effective C++条款20:宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to p ...

  9. Effective STL 读书笔记

    Effective STL 读书笔记 标签(空格分隔): 未分类 慎重选择容器类型 标准STL序列容器: vector.string.deque和list(双向列表). 标准STL管理容器: set. ...

  10. 《Effective STL》中文版 读书笔记

    50条有效使用STL的经验 第一条 慎重选择容器类型(20190713) 第二条 不要试图编写独立于容器类型的代码(20190713) 第三条 确保容器中的对象副本正确而高效(20190713) 第四 ...

最新文章

  1. 手动安装K8s第三节:etcd集群部署
  2. SqlServer2000日志文件过大问题处理
  3. java线程池拒绝策略_Java核心知识 多线程并发 线程池原理(二十三)
  4. linux 7.2中文命令,CentOS7如何支持中文显示
  5. [perl]perl界大牛唐凤传说
  6. fastjson JSONObject.toJSONString 出现 $ref: $.的解决办法(重复引用)
  7. ROS学习笔记7(理解ROS服务和参数)
  8. hx711压力传感器工作原理_压电式压力传感器原理,你了解吗?
  9. 阿里云CentOS7服务器搭建邮件服务器,端口:465
  10. nc 二次开发_金蝶云星空(K3CLOUD)和用友NC对比
  11. 这电商代运营公司两月打造一个带泪的超级单品
  12. 使用Python简单地去辅助百万答题
  13. 如何将caj转换成word
  14. 【工具】百度网盘视频类资源下载新思路,轻松优雅解决下载限速方法
  15. genl_ops结构分析
  16. windows bat
  17. Part6:客户端和服务端信息交互模型
  18. max-width min-width max-height min-height
  19. 《游戏数据分析实战》总结思考
  20. LinkPdf转换器-PDF转换成Word使用教程

热门文章

  1. suma服务器 硬盘安装,[Server] HP DL380 G6更新esxi6.0 SATA 硬盘掉线问题
  2. RISC-V 实现整数运算指令(Part 1)
  3. 从数组到类簇的学习总结
  4. 维天运通(路歌)招股书失效:毛利率波动明显,冲刺上市遇挫?
  5. 4G物联卡跟NB物联卡有什么区别
  6. 软件测试自学英语计划,英语学习计划
  7. 移动硬盘安装Ubuntu,并确保在任何电脑都可用
  8. 数独(SuDoku)介绍
  9. android算法实现房贷计算器
  10. 车联网各领域头部企业排行榜