Effective STL

来源:互联网 发布:怎么用服务器ip做域名 编辑:程序博客网 时间:2024/05/17 23:39

effective stl 读书笔记

本文在赖勇浩的笔记上继续写的^_^ 

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

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

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

第4条:调用 empty 而不是检查 size() 是否为 0。 
第5条:区间成员函数优先于与之对应的单元素成员函数。

区间成员函数写起来更容易,更能清楚地表达你的意图,而且它们表现出了更高的效率。

几乎所有目标区间被插入迭代器指定的copy的使用都可以用调用的区间成员函数的来代替。

vector单个插入会有三方面 效率损耗:

1 每次调用insert函数开销(可能被inline节省)2 多余的元素移动,每次插入要把所有元素向后移动  3多余的内存分配次数, 当insert时如果容量不够就会重新分配内存

同理对于其他顺序容器

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

意思大概是在函数调用时避免使用匿名对象作实参,以消除对编译器的二义性。

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

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

auto_ptr在拷贝时转移所有权

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

关键点:序列容器和关联容器的删除方法不一样,list尤其特别。

关联容器 : erase(iter)后 iter失效

序列容器:erase(iter)后 返回下一个元素

结论


去除一个容器中有特定值的所有对象:
如果容器是vector、string或deque,使用erase-remove惯用法。

c.erase(remove(c.begin(), c.end(), 1963),c.end());   // 当c是vector、string 或deque时,

如果容器是list,使用list::remove。
如果容器是标准关联容器,使用它的erase成员函数。

去除一个容器中满足一个特定判定式的所有对象:
如果容器是vector、string或deque,使用erase-remove_if惯用法。

bool badValue(int x); // 返回x是否是“bad”
c.erase(remove_if(c.begin(), c.end(), badValue),c.end());

如果容器是list,使用list::remove_if。

如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代
器传给erase时记得后置递增它。

AssocContainer<int> c;// c现在是一种/标准关联容器
AssocContainer<int> goodValues;// 用于容纳不删除的值的临时容器
remove_copy_if(c.begin(), c.end(),inserter(goodValues,goodValues.end()),badValue); // 从c拷贝不删除的值到goodValues
c.swap(goodValues);

OR

AssocContainer<int> c;
for (AssocContainer<int>::iterator i = c.begin();  i !=c.end(); ){ // 循环条件和前面一样
    if (badValue(*i)){
    //do something
    c.erase(i++); // 删除元素
    }else    ++i;

 }


在循环内做某些事情(除了删除对象之外):
如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你
的迭代器。
如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

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

第11条:理解自定义分配子的合理用法。 
第12条:切勿对STL容器的线程安全性有不切实际的依赖。

只能期望(不可依赖哦)多线程读是安全的,多个线程对不同的容器做写入操作是安全的。

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

主要因为它们的内存管理和各种嵌套类型定义方便编程,另外讨论到基于引用计数的string 可能在多线程环境中损失性能。

string引用计数的一个例外:如果你在多线程环境中使用了引用计数的字符串,你可能发现避免分配和拷贝所节省下的时间都花费在后台并发控制上了。

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

先分析了vector/string的内存策略和相关的方法.每次不够用时内存会 X2 。

size capacity resize reserve的用法

size()告诉你容器中有多少元素。它没有告诉你容器为它容纳的元素分配了多少内存。

capacity()告诉你容器在它已经分配的内存中可以容纳多少元素。
resize(Container::size_type n)强制把容器改为容纳n个元素。调用resize之后,size将会返回n。如果n小于当前大小,容器尾部的元素会被销毁。如果n大于当前大小,新默认构造的元素会添加到容器尾部。如果n大于当前容量,在元素加入之前会发生重新分配。
reserve(Container::size_type n)强制容器把它的容量改为至少n,提供的n不小于当前大小。这一般强迫进行一次重新分配,因为容量需要增加。(如果n小于当前容量,vector忽略它,这个调用什么都不做,string可能把它的容量减少为size()和n中大的数,但string的大小没有改变。


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

谈到了四个不同的实现,并分析了他们占用的内存大小,引用和共享能力。

字符串值可能是或可能不是引用计数的。默认情况下,很多实现的确是用了引用计数,但它们通常提供了关闭的方法,一般是通过预处理器宏。

string对象的大小可能从1到至少7倍char*指针的大小。

新字符串值的建立可能需要0、1或2次动态分配。

string对象可能是或可能不共享字符串的大小和容量信息。

string可能是或可能不支持每对象配置器。

不同实现对于最小化字符缓冲区的配置器有不同策略。

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

因为只有vector string才保证和数组有同样的内存布局,所以如果要把map之类的容器内容传给C-API,需要vector中转一下。

然后传递&v[0] 给C-API。

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

只能使vector容量和这个实现可以尽量给容器的当前大小一样小,因为具体要看stl实现, 并不能使vector尽可能的小,stl有可能有最小容量的限制

vector<Contestant>(contestants_vec.begin(), contestants_vec.end()).swap(contestants_vec);//

vector<Contestant>().swap(v);// clear v and minimize its capacity

string().swap(str);// clear s and minimize its capacity


第18条:避免使用vector<bool>。

因为代码中  T *p = &c[0];是 不能够编译的。vector并不是存储bool , 而是伪装存储bool,使用了代理类的概念。

替代品: deque<bool>   or  bitset (不是标准STL)
第19条:理解相等(equality)和等价(equivalence)的区别。

相等,以 operator== 为基础,

等价,以 operator< 为基础,当 !(a<b)&&!(b<a) 时两者等价。

关联容器为什么不能采用相等作为判断相等的条件?

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

这条已经约定成俗了,没什么好说的。 如果需要比较,当然需要自定义一个比较类型了,作者有点小罗嗦。

这条主要是介绍一个添加比较的方法。

struct StringPtrLess:
public binary_function<const string*,   const string*,   bool> {      // see Item 40 for the reason for this base
    bool operator()(const string *ps1, const string *ps2) const
   {
        return *ps1 < *ps2;
   }
};
typedef set<string*, StringPtrLess> StringPtrSet;

打印StringPtrSet ssp:

循环打印

for (StringPtrSet::const_iterator i = ssp.begin();  i != ssp.end();  ++i)
     cout << **i << endl;

使用stl算法打印StringPtrSet;、

void print(const string *ps)
{
    cout << *ps << endl;
}
t(ssp.begin(), ssp.end(), print);

或者使用反向引用方式 用transform打印

struct Dereference {
    template <typename T>
    const T& operator()(const T *ptr) const
    {
        return *ptr;
    }
};
transform( ssp.begin(), ssp.end(), ostream_iterator<string>(cout, "\n"),  Dereference());

问题:为什么要定义一个比较类型而不是使用函数?因为string  要求穿的参数时类型

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

听起来像废话?嗯,重要的是要记得 !(a<b) 的意思是 a>=b,切记要把前者写成 b<a 形式。
第22条:切勿直接修改set或multiset中的键。

关键是要记住如果直接修改了,那么要保证该容器仍然是排序的。对python之类的语言其实也是如此。

这个还是有体会的,原来曾经想返回集合元素的引用,然后修改。

对于某些实现的set 其实是可以修改的,只要不修改它的比较的key,就不会引起错误,应该这样使用

struct IDNumberLess:
public binary_function<Employee, Employee, bool> {// see Item 40
  bool operator()( const Employee& lhs,
    const Employee& rhs) const
    {
       return lhs.idNumber() < rhs.idNumber();
    }
};
typedef set<Employee, IDNumberLess> EmpIDSet;

EmpIDSet::iterator i = se.find(selectedID);

if (i != se.end()) {
const_cast<Employee&>(*i).setTitle("Corporate Deity");
}


static_cast<Employee>(*i).setTitle("Corporate Deity");  or ((Employee)(*i)).setTitle("Corporate Deity");是错误的,因为改变的是临时变量。

强制转换很危险,如果非要改变set中的元素,可以将先取出来,更改完,删除掉,再插入

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

额,我觉得除非证明了关联容器性能不行了,不然不应该考虑。map 不行了用 hashmap。

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

简言之,增加时使用insert,更新时使用[]。 

第25条:熟悉非标准的哈希容器。嗯,这些玩意儿是非标准的,C++0x 出来以后就会有标准了……
第26条:iterator优先于const_iterator、reverse_...以及const_reverse...。

因为 insert/erase等函数需要,以及彼此间的比较,还有就是iterator更能灵活转换。
第27条:使用distance和advance将容器的const_iterator转换成iterator。

其实这是前一条的延伸,讲述了相应的转换方法和要注意的地方,比如显式指定distance的类型参数为const_...,以避免编译器推断。 

typedef deque<int> IntDeque;
// as before
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
IntDeque d;
ConstIter ci;
...
// make ci point into d
Iter i(d.begin());

advance(i, distance<ConstIter>(i, ci));

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

简言之就是base()返回的迭代器有偏移,插入和删除操作的时候要注意。再介绍了一个v.erase((++ri).base())惯用法。

reverse_iterator的base成员函数返回一个“对应的”iterator的说法并不准确。对于插入操作而言,的确如此;但是对于删除操作,并非如此。

第29条:对于逐个字符的输入请考虑使用istreambuf_iterator。

先说了一下istream_iterator会跳过空白符的问题,然后 引入...buf...,后者的性能也会更好,嗯,例子里的string使用了区间构造函数,又一个推荐的惯用法。 

 

第30条:确保目标区间足够大。

特别是做覆盖的时候,一定要注意,可以先用resize撑大。插入时用back_inserter、front_...、 inserter和ostream_iterator。 

 OutputIterator transform (InputIterator first1, InputIterator last1,  OutputIterator result, UnaryOperation op);

例子中用std::transform   进行插入时,  目标位置使用vec.begin(),那么是对vec的覆盖, 如果vec的size不够大 ,就是出错,此时要改为使用back_inserter(vec.end())等插入函数。
第31条:了解各种与排序有关的选择。

介绍了

partition/stable_partition   获得满足条件的元素

nth_element  获得前n名 , 但是无序

partial_sort  只排序前n名

sort  全部排序 不稳定

stable_sort  稳定的全排序


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

就是erase-remove惯用法的由来,另外在讲了一次不同容器删除元素的方法是不同的。 

remove是一个算法,不必关心容器是什么 ,所以remove不删除元素。
第33条:对包含指针的容器使用remove这一类算法时要特别小心。

作为cpp程序员,一定要时刻警惕资源泄漏。boost::shared_ptr是一个好选择。 

只要清楚remove的原理本条不难理解,因为remove过程中会覆盖,所以指针容器remove时可能覆盖。


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

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

第35条:通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较。 
第36条:理解copy_if算法的正确实现。

文中给出了一个正确实现,注意点是不能要求使用的函数子是可配接的,STL 算法都这样。 

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进行区间统计,前者的代码更明了一些,重要的是它们接受的函数子要求不同。 

求和统计,这个很方便。


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

换句话说就是让它们小巧,而且单态。这个条款的意义在于为赘重而且多态的函数子带来的问题提出一个解决方案,pimpl 惯用法。 

一个使用函数类的例子

class DoSomething:
public unary_function<int, void> {
void operator()(int x) {...}
};
typedef deque<int>::iterator DequeIntIter; //convenience typedef
deque<int> di;
...
DoSomething d:
// create a function object
...
for_each<DequeIntIter,DoSomething&>(di.begin(),di.end(),d);

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

纯函数即返回值仅仅依赖于其参数的函数.

为啥呢? 应为函数类时只传递的, 有过不是纯函数的类,是有状态的,那么值传递不能保证状态的一致。

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

not1, not2, bind1st, and bind2nd 等函数要求传入参数是可适配的 adpatable。

因为 STL 的函数配接器要求一些特殊的类型定义,argument_type,result_type...之类。编写函数子从unary_function或 binary_function继承是一个不错的方案。 
第41条:理解ptr_fun、mem_fun和mem_fun_ref的来由。

 mem_fun and mem_fun_ref是为了解决遍历调用成员函数的问题,

class Widget {
  public:
  void test();

};

void test(Widget& w);

list<Widget*> Ipw;

for_each(vw.begin(), vw.end(), test);// Call #1 (compiles)

for_each( vw.begin(), vw.end(),&Widget::test);// Call #2 (won't compile)

for_each( lpw.begin(), Ipw.end(),&Widget::test);/Call #3 (also won't compile)

#1可以编译, #2 #3这样是不会编译成功的,所以需要mem_fun and mem_fun_ref

for_each(lpw.begin(), Ipw.end(),mem_fun(&Widget::test));  这样就可以成功编译了。

为啥这样就编译了呢?

先看看mem_fun的定义  

template<typename R, typename C>
mem_fun_t<R,C>
mem_fun(R (C::*pmf)());

传入参数pmf是类C的一个成员函数,返回值是R

就把for_each 的参数从&Widget::test ,一个成员函数 转换为 mem_fun, 一个非成员函数, 正常编译

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

重要的习惯。

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

三个理由:效率更高,更不容易出错,和更好的可维护性。

这个令人怀疑的,stl的粉丝自然是欢喜, 不喜欢的完全被搞晕了。不过是不是个人原因么,不愿意接收新食物。
第44条:容器的成员函数优先于同名的算法。

原因:速度更快,且与容器结合得更加紧密,更能够与容器的行为保持一致。  

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

这与传入的区间是否已经排序有关,与你的目的有关,与容器有关,总之复杂,要自己去看这一小节两次。 

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

因为函数对象更容易让编译器乐于内联,所以速度会快一些。从代码被编译器接受的程度而言,它们更加稳定可靠。 
第47条:避免产生“直写型”(write-only)的代码。

即所谓容易编写,但难以阅读和理解的代码,比如一行调用函数12次,其中 10 个是互不相同的。 

例子

vector<int> v;
int x, y;
...
v.erase(
    remove_if(

        find_if(v.begin(), v.end(),bind2nd(greater_equal<int>(), y)),
        v.end(),
        bind2nd(less<int>(), x)

     ),
     v.end()

);

例子是对于排序过的vector是可以,否则有问题的。所以stl算法使用前一定要验证下,除非知道很清楚 , 否则不要用。

第48条:总是包含正确 的头文件。

因为C++标准没有规定头文件的互相包含关系,所以不同的STL实现有所不同。要记住容器基本上声明在同名文件中,算法是algo..和 num..,迭代器在iterator中,函数子和配接器在functional中。 
第49条:学会分析与STL相关的编译器诊断信息。

第一招是替换大法,然后介绍了一下与容器、插入迭代器、绑定器、输出迭代器或算法相关的错误大概有什么套路看。

第50条:熟悉与STL相关的web站点。

三个:www.sgi.com/tech/stl、www.stlport.org  www.boost.org。

0 0
原创粉丝点击