Andy Niu �����ĵ�

Andy Niu

Andy Niu Help  1.0.0.0
Effective__STL

变量

 【S01】慎重选择容器类型
 
 【S02】不要试图编写独立于容器类型的代码
 
 【S03】确保容器中的对象拷贝正确而高效
 
 【S04】使用empty而不是判断size是否为0
 
 【S06】当心CPP编译器最烦人的分析机制
 
 【S13】vector和string优先于动态分配的内存
 
 【S16】了解如何把vector和string数据传给旧的API
 
 【S17】使用swap技巧除去多余的容量
 
 【S46】考虑使用函数对象而不是函数作为STL算法的参数
 
 【S47】避免产生直写型的代码
 

详细描述

变量说明

【S01】慎重选择容器类型
1、标准STL序列容器:vector、string、list和deque
2、标准STL关联容器:set、multiset、map和multimap
3、vector和string是基于连续内存,list、set和map是基于节点。
    而deque是二者的结合,deque内部是一块一块的,块与块之间是基于节点,块内部基于连续内存。
4、对比vector,deque是双向开口。
【S02】不要试图编写独立于容器类型的代码
1、为什么?
    从逻辑结构上,容器分为序列容器和关联容器。
    从物理结构上,容器分为基于连续内存的容器和基于节点的容器。
2、对于不同的容器,支持的方法不一样(也就是暴露出来的接口不一样)。
    相同方法名内部做的事情是不一样,迭代器、指针和引用无效的规则是不同的。
【S03】确保容器中的对象拷贝正确而高效
1、容器保存的对象,并不是你提供给容器的对象,而是这些对象的副本。
2、保存对象的时候,调用copy构造,创建出副本。
    这里特别注意:子类对象保存到父类容器中会存在对象切割。
3、因为是整体拷贝,为了高效,可以使用指针。但是指针带来问题,需要管理动态内存,可以使用智能指针解决这个问题。
    但是千万不能是auto_ptr,因为这玩意的拥有权会转移。
4、为了避免不必要的拷贝,对于vector可以使用reserve提前把内存分配好,这个时候并没有调用构造方法创建对象。
【S04】使用empty而不是判断size是否为0
1、二者的作用是一样的,结果也是等价的。就是判断集合是否为空。
2、二者是等价的,为什么强调使用empty,因为empty效率更高。
3、在STL中,对于一般的集合,empty和size都是常数时间。但是对于list,empty是常数时间,size是线性时间;考虑为什么?
    考虑增删操作,对于一般的集合,增删是线性时间,因为涉及到元素的移动,增删的同时也就更新了元素个数。
    但是对list增删,是常数时间,不会更新节点个数。
    因此,对于一般的集合,size是实时更新的,empty与size可认为是等价的。
    但是对于list:对于empty,只需要检查head是否为end就可以了,为常数时间。对于size,必须遍历,为线性时间。
【S06】当心CPP编译器最烦人的分析机制
1、考虑一个包含int的文件,复制到list,如下:
    ifstream dataFile("ints.bat");
    list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>());
2、上面的代码不是预期的行为。
3、先从最简单开始,声明方法 int f(double d); 等价的写法有 int f (double (d)); int f (double);
    也就是说,形参名称可以使用括号括起来,形参名称也可以省略,只保留形参的类型。
4、考虑int g(double (*pf) ()); 形参是一个方法指针,等价的写法有 int g(double pf ()); 
    我们省略形参名称,就变成了 int g(double ());
5、现在考虑list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>()); C++编译器会认为这个一个方法声明,
    第一个形参是:形参类型是istream_iterator<int>,形参名称是 dataFile,只不过使用括号括起来了,
    第二个形参是:形参类型是一个方法指针,指向的方法是返回istream_iterator<int>,接受形参void,省略了形参名称。
6、C++中有一条规律,语句优先解释成 方法声明。当这个解释失败,才进行其他解释。最常见的如下:
    Student s; // OK
    Print(s);
    
    Student s = Student(); // OK
    Print(s);
    
    Print(Student()); // OK
    
    Student* s = new Student(); // OK
    Print(*s);
    
    Student* s = new Student; // OK
    Print(*s);
    
    Student s(); // Error
    Print(s);
    因为C++会把Student s();当成一个方法声明。
7、怎么解决上面的问题?
    两种办法:
    办法一,对于方法调用,实参可以使用括号括起来,而对于方法声明,把整个形参(包括形参类型和形参名称)括起来是错误的,
    因此可以如下:
    list<int> data( (istream_iterator<int>(dataFile) ),istream_iterator<int>());
    办法二:不使用匿名对象,使用具名对象,如下:
    istream_iterator<int> begin(dataFile);
    istream_iterator<int> end;
    list<int> data(begin,end);
【S13】vector和string优先于动态分配的内存
1、使用new动态分配内存,必须承担如下责任:
    a、使用delete释放内存
    b、确保使用了正确的形式,delete与new的形式要匹配
    c、不能重复delete
2、使用vector和string可以消除以上的负担。每当要动态分配一个数组时,都要考虑使用vector和string替代。
    如果元素是字符char,使用string。否则使用vector。注意:有一种特殊情况,使用vector<char>更合理。
3、vector和string的元素分配在堆上,它们内部维护一个指针,指向堆上的元素。vector和string是深拷贝,会把元素逐个拷贝。
4、vector和string,它们自己管理内存,内存会自动增长,当它们析构时,会对每个元素逐个析构。
5、vector和string是功能完全的STL序列容器,可以使用很多STL功能。而数组只支持部分STL算法,
    没有begin,end,size这样的成员方法,也没有iterator这样的嵌套类型,因此STL更好用。
6、为了支持旧的代码,将vector和string转化为数组很简单。
7、有一种特殊情况,需要考虑。string是如此常用,它的使用效率很重要。因此,STL中的string有可能是基于引用计数来实现的。
    这在多线程中,会出现冲突问题。如果string不是引用计数,而是整体拷贝,多线程就不会有问题,因为每个线程修改自己的副本。
8、对于基于引用计数的string,又运行在多线程环境下,有三种可行的选择:
    a、禁止引用计数,这种做法不可移植
    b、寻找一个不使用引用计数的string
    c、考虑vector<char>,代替string
    注意:VS2010中的STL,string没有使用引用计数。
【S16】了解如何把vector和string数据传给旧的API
1、尽量使用vector和string替换数组,但是老的代码还是使用数组。如果老的接口期望是数组,怎么办?
    需要把vector和string,暴露出数组接口,也就是第一个元素的地址。
2、考虑方法DoSomething(const int* pInt,size_t size),对于vector<int> vec,调用如下:
    DoSomething(&vec[0], v.size());
    这里有个问题,vec的大小可能为0,更安全的做法是:
    if(!vec.empty())
    {
      DoSomething(&vec[0], v.size());
    }
3、考虑,能不能使用begin()替换&vec[0]?
    我们知道,begin返回迭代器,是对指针的封装,类似于指针。
    但是,不能把迭代器当成指针使用,可以使用&*begin(),这种方式与&vec[0]等价,显然&vec[0]更直观。
4、考虑DoSomething(const char* pa); 对于string str,调用如下:
  DoSomething(str.c_str());
  c_str返回一个char指针,指向字符串值,尾部再加一个空字符。str长度可以为0,返回空字符,内部也可以包含空字符。
    但是,DoSomething的处理是以第一个空字符作为结束。
5、vector暴露出指针,调用端修改元素的值,通常没有问题。但是不能增加新元素。
    因为在外部增加新元素,vector不知道,不去更新size,size产生不正确的结果。如果大小和容量相等,增加元素就会踩内存了。
    注意:对于排序的vector,修改vector的元素值,也会产生问题。因为修改元素值后,导致vector无序。
6、考虑C API数组初始化vector,很简单。如果C API数组初始化string呢?
    可以先初始化vector<char>,vector<char>再去初始化string。当然,还可以解决更一般化的问题。
    先把C API数组写到vector,再把vector写到期望的STL容器中。对于逆向的转化,同样道理。先把STL容器的元素写到vector,再把vector传递给C API数组。
    也就是说,vector是一个适配器。
【S17】使用swap技巧除去多余的容量
1、考虑下面的需求,对于vec开始的时候有1000个元素,后来只有10个元素,那么vec的capacity至少还是1000,
    后面的990个内存单元,没有使用,但是还被vec霸占着。如何释放这些内存呢?
2、我们知道,vector进行copy构造的时候,根据rhs 的size进行分配内存。因此,我们可以建立一个临时对象,然后交换一下就可以了。如下:
    vector<int>(vec).swap(vec);
    vector<int>(vec) 是个临时对象,可认为capacity为10,而vec的capacity为1000,二者交换后,vec的capacity为10,临时对象析构。
3、这里需要注意两点:
    a、临时对象的capacity有可能还是大于10,不能保证容量最小,而是尽量小。
    b、对于vector 的swap方法,内部实现只是交换了彼此的begin指针和end指针,并没有交换内容。
    这个很好理解,对于资源管理类,也就是内含指针的类,交换的时候,只需要交换彼此的指针就好了。
    举个例子:甲住501,乙住502,现在甲乙想换房子。只要换一下钥匙就好了,如果去把房间里的家电家具换一下,方法也太笨了。
4、考虑一个特殊情况,我想清空一个容器,并释放所有内存,该怎么办?
    首先,clear方法是不行的,因为它只是把元素清空,内存还被霸占着。由上面的分析,很容易想到,拿一个空容器与当前容器交换一下,就行了。
    也就是:vector<int>().swap(vec);
【S46】考虑使用函数对象而不是函数作为STL算法的参数
1、高级语言有个缺点,抽象程度提高了,但是所生成的代码效率降低了。
2、操作一个只包含double类型的对象比直接操作double效率要低。
3、但是将函数对象传递给STL算法往往比传递函数效率要高。这是为什么?
4、第一个原因,C++并不能真正地将一个函数作为参数传递给另一个函数,而是转化为函数指针传递过去。
    这就意味着,主调函数通过指针发出调用,编译器不会进行内联优化。
    也就是说,函数指针参数抑制了内联机制。而函数对象是可以进行内联优化的。
    因此,C++的sort算法就性能而言总是优于C语言的qsort
5、第二个原因,使用函数,有时候编译器会拒绝合法的代码,编译失败。而使用函数对象没有这个问题。
6、第三个原因,使用函数对象有助于避免一些微妙的、语言本身的缺陷。
    也就是说,STL算法对于函数对象有更好的支持。
【S47】避免产生直写型的代码
1、一条语句做多件事情,使用复杂的函数嵌套,会导致代码难以阅读和理解。
2、软件工程的一条真理:代码被阅读的次数远远大于他被编写的次数。
3、更好的方法是:把做的多件事情分成几步来做。
Copyright (c) 2015~2016, Andy Niu @All rights reserved. By Andy Niu Edit.