《C++标准程序库(The C++ Standard Library)》读书笔记

来源:互联网 发布:app数据统计模板 编辑:程序博客网 时间:2024/05/01 17:50
一 关于本书


一旦有了一个可以依循的标准规格,我们便可能写出跨越PC,乃至大型主机等各种不同平台的程序。此外,如果能够建立起一个标准程序库,程序员便可得以运用可移植的通用组件和更高层次的抽象性,而不必从头创造世界。
注:这儿阐明了标准化的作用,就是可移植。


二 C++及其标准程序库简介


......然而事情总有个结束,终于有一天,人们决定不再考虑任何重大扩张,无论这个扩张多么有价值。就因为这样,hash tables没有被纳入标准-尽管它作为一种常用的数据结构,理应在STL中享有一席之地。
注:终于明白了为什么STL中没有hash tables了。


Nontype Template参数
例如
template<size_t _Bits>
class bitset
{ // store fixed-length sequence of Boolean elements
typedef unsigned long _Ty;// base type for a storage word
enum {digits = _Bits};// extension: compile-time size()
也就是说,这种模板变的不是类型(一般我们见到的),而是某个类型的值。


关键字typename

template<class T>
class MyClass
{
typename T::SubType* ptr;  // 不能将typename替换成class
};


class Q
{
public:
typedef int SubType;
};


class R
{
public:
class SubType{};
};


int _tmain(int argc, _TCHAR* argv[])
{
MyClass<Q> myclass1;
MyClass<R> myclass2;


return 0;
}
注意,如果要把一个template中的某个标识符指定为一个类型,就算意图显而易见,关键字typename也不可或缺,因此C++的一般规则是,除了以typename修饰之外,template内的任何标识符都被视为一个值而非一个类型。


2.2.2 基本类型的显式初始化
如果采用不含参数的、明确的构造函数调用语法,基本类型会被初始化为零:
int i1;  // undefined value
int i2 = int(); // initialized with zero
这个特性可以确保我们在撰写template程序代码时,任何类型都有一个确切的初值。例如下面这个函数中,x保证被初始化为零。
template <class T>
void f()
{
    T x = T();
    ...
}


2.2.3 异常处理
通过异常处理,C++标准程序库可以在不“污染”函数接口(亦即参数和返回值)的情况下处理异常。如果你遇到一个意外情况,可以通过“抛出一个异常”来停止一般的(正常的)处理过程:
class Error;
void f()
{
   ...
   if(exception-condition)
       throw Error();
   ...
}
注:由此看来,异常仅仅是让函数接口更简单而已,否则必须从返回值或参数中选择一个出来携带错误信息。


三 一般概念


和class不同,namespace具有扩展开放性,可以出现在任何源代码文件中。因此你可以利用一个namespace来定义一些组件,而它们可散布于多个实质模块上。
注:实质模块指静态库.lib,动态库.dll等,比如
#include "../LibA/A.h"
#include "../LibB/B.h"
#include "../DllA/DllA.h"


int main(int argc, char* argv[])
{
int a = AB::add(1, 2);
int b = AB::sub(1, 2);
int c = AB::fnDllA();


printf("Hello World!%d %d %d\n", a, b, c);
return 0;
}
AB名字空间存在于两个静态库和一个动态库中,是没有任何问题的。从本质上来说,名字空间仅仅影响标识符名称的修饰而已,只要大家都按同一个名字空间名修饰就行。
注意,在使用名字空间时,声明和对应的实现都要同时放入同一个名字空间中,以保证修饰后的名字一致,否则就链接错误了,呵呵。


3.4 配置器(Allocators)
......但是这种用法还相当新颖,尚未获得广泛的接受(情况正在改变中)。
绝大多数程序都采用缺省配置器,但有时候其它程序库也可能提供某些配置器以满足特定需求。这种情况下只需要简单地将它们当做参数即可。自行设计并实作配置器的实际意义不大。实际生活中最典型的方式还是直接采用缺省配置器。




四 通用工具


4.1 pair


The #ifdef and #ifndef directives perform the same task as the #if directive when it is used with defined( identifier ).
#ifdef identifier
#ifndef identifier
// equivalent to
#if defined identifier
#if !defined identifier
 
4.2 auto_ptr
C++标准程序库提供的auto_ptr是一种智能型指针,帮助程序员防止“被异常抛出时发生资源泄漏”。注意,我说的是“一种”智能型指针,现实生活中还有其他许多有用的智能型指针,auto_ptr只是针对某个特定问题而设计,对于其他问题,auto_ptr无能为力。
......一个显而易见的问题是,我们经常忘掉delete动作,特别是当函数中间存在return语句时更是如此。然而真正的麻烦发生于更隐晦之处,那就是当异常发生时我们所要面临的灾难。
......你看,为了在异常发生时处理对象的删除工作,程序代码变得多么复杂和累赘!如果还有第二个对象,如果还要比照办理,如果还需要更多的catch子句,那简直是一场噩梦。这不是优良的编程风格,复杂而且容易出错,必须尽力避免。
注:用auto_ptr可以解决上面的问题,但平常我们遇到的更难解决的问题可能是文件,socket之类的资源释放,有没有好的解决办法?


绝对不应该出现多个auto_ptr同时拥有一个对象的情况。程序员必须负责防范这种错误。


......在这里,拷贝构造函数更动了“用以初始化新对象”的原对象,而赋值操作符也修改了右侧对象,这和程序语言中惯常的初始化动作和赋值操作动作可说大相径庭。


4.2.4 auto_ptr的错误运用
1.auto_ptr之间不能共享拥有权
2.并不存在针对数组而设计的auto_ptr
因为auto_ptr是透过delete而非delete[]来释放其所拥有的对象。
3.auto_ptr决非一个“四海通用”的智能型指针
特别请注意的是,它不是引用计数型指针
4.auto_ptr不满足STL容器对其元素的要求


注:auto_ptr在发生拥有权转移时容易误用出问题,我的观点是最好让它永远也别发生拥有权转移(比如在函数之间传过去传过来的)。我们仅仅用它解决它本来想解决的问题,别搞复杂了。


4.3 数值极限


一般来说,数值类型的极值是一个与平台相关的特性。
注:也就是,在不同平台上,<climits>等文件的内容是不一样的。待确认!


那么在不同平台之间通信时该咋办呢,比如我需要固定的四个字节,我肯定不能直接用int,那么是不是应该根据平台搞不同的typedef?




五 标准模板库


所谓deque,是“double-ended queue”的缩写。它是一个dynamic array,可以向两端发展,因此不论在尾部或头部安插元素都十分迅速。在中间部分安插元素则比较费时,因为必须移动其它元素。


//make all characters in the list uppercase
list<char>::interator pos;
for (pos = coll.begin; pos != coll.end(); ++pos) {
    *pos = toupper(*pos);
}
注意,这里使用“前置式递增”++pos,因为它比“后置式递增”pos++效率高。后者需要一个额外的临时对象,因为它必须存放迭代器的原本位置并将它返回,随意一般情况下最好使用++pos,不要用pos++。
注:以前老说应该使用前置++,估计仅仅是针对迭代器。如果是普通的整形,前置和后置++的效果应该是一样的。


算法并非容器类型的成员函数,而是一种搭配迭代器使用的全局函数。这么做有一个重要优势:所有算法只需实作出一份,就可以对所有容器运作,不必为每一种容器量身定制。算法甚至可以操作不同类型之容器内的元素,也可以与用户定义的容器搭配。这个概念大幅降低了程序代码的体积,提高了程序库的能力和弹性。
注意,这里所阐述的并非面向对象思维模式,而是泛型函数式编程思维模式。在面向对象编程概念里,数据与操作合为一体,在这里则被明确划分开来,再透过特定的接口彼此互动。当然这需要付出代价:首先,用法有失直观,其次,某些数据结构和算法之间并不兼容。更有甚者,某些容器和算法虽然勉强兼容,却毫无用处。因此,深入学习STL的概念并了解其缺陷,显得十分重要,惟其如此,方能取其厉而避其害。




针对copy()算法, 要想避免上述错误,你可以确认目标区间内有足够的元素空间,或是采用插入迭起器。
要想让目标区间够大,你要不一开始就给它一个正确大小,要不就显式地改变其大小。这两个办法都只适用于序列式容器(vector,deque,list)。关联式容器根本不会有此问题,因为关联式容器不可能被当做覆写型算法的操作目标。


Back inserters的内部调用push_back(),在容器尾端插入元素。在以下语句完成之后,coll1的所有元素都会被附加到coll2中:
copy(coll1.begin(), coll1.end(),  // source
     back_inserter(coll2));       // destination
当然,只有在提供有push_back()成员函数的容器中,back inserters才能派上用场。在C++标准程序库中,这样的容器有三:vector,deque,list。


Front inserters的内部调用push_front(),将元素安插于容器最前端。在以下语句完成之后,coll1的所有元素都会被附加到coll2中:
copy(coll1.begin(), coll1.end(),  // source
     front_inserter(coll2));       // destination
注意,这种动作逆转了呗安插元素的次序。如果你先安插1,在向前安插2,那么1会排列在2的后面。
当然,只有在提供有push_front()成员函数的容器中,front inserters才能派上用场。在C++标准程序库中,这样的容器有三:vector,deque,list。


这种一般性的inserters,简称就叫inserters,它的作用是将元素插入“初始化时接受之第二参数”所指位置的前方。Inserters内部调用成员函数insert(),所有STL容器都提供有insert()成员函数,因此,这是唯一可用于关联式容器身上的一种预先定义好的inserter。
等等,我不是说过,在关联式容器身上安插新元素时,不能指定其位置吗?它们的位置是由它们的值决定的啊!好,我解释一下,很简单:在关联式容器中,你所给的位置只是一个提示,帮助它确定从什么地方开始搜寻正确的安插位置。如果提示不正确,效率上的表现会比“没有提示”更糟糕。


流迭代器
vector<string> coll;
copy(istream_iterator<string>(cin),      // start of source
    istream_iterator<string>,            // end of source
    back_iterater(coll));                // destination


5.6.3 算法 vs. 成员函数
就算我们符合种种条件,得以使用某个算法,那也未必就一定是好。容器本身可能功能相似而性能更佳的成员函数。
一个极佳的例子便是对list的元素调用remove()。算法本身并不知道它工作于list身上,因此它在任何容器中都一样,做些四平八稳的工作:改变元素值,从而重新排列元素。如果它移除第一个元素,后面所有元素就会分别被设给各自的前一个元素。
为了避免这么糟糕的表现,list针对所有“更易型”算法提供了一些对应的成员函数。是的,如果你使用list,你就应该使用这些成员函数。此外请注意,这些成员函数真的移除了“被移除”的元素。
如果高效率是你的最高目标,你应该永远优先选用成员函数。
注:为什么算法remove()不真正的删除呢,估计是考虑到针对vector时真正删除会影响效率。


传递给算法的“函数型参数”,并不一定得是函数,可以是行为类似函数的对象。这种对象称为function object,或称仿函数。当一般函数使不上劲时,你可以使用仿函数。STL大量运用仿函数,也提供了一些很有用的仿函数。


仿函数通常比一般函数速度快
就template概念而言,由于更多细节在编译期就已确定,所以通常可能进行更好的最佳化。所以,传入一个仿函数,可能获得更好的性能。


STL的设计原则是效率优先,安全次之。错误检查相当花时间,所以几乎没有。




六 容器


赋值和swap()


赋值时源容器保持不变,swap()时源容器中所有的元素会被全部移除。
如果两个容器类型相同,而且拷贝后源容器不再被使用,那么我们可以使用一个简单的优化方法:swap()。swap()的性能比赋值优异得多,因为它只交换容器的内部数据。事实上它只交换某些内部指针(指向实际数据如元素、配置器、排序准则-如果有的话),所以时间复杂度是“常数”,不像实际赋值操作的复杂度为“线性”。




6.2.6 vector<bool>
C++标准程序库专门针对元素类型bool的vector设计了一个特殊版本,目的是获取一个优化的vector。其耗用空间远远小于以一般vector实作出来的bool vector。一般vector的实作版本会为每个元素分配一个byte空间,而vector<bool>特殊版本内部只用一个bit来存储一个元素。所以通常小8倍之多。不过这儿有个小麻烦:C++的最小可寻址通常以byte为单位。所以上述的vector特殊版本需针对references和iterators作特殊考虑。
考虑结果是,vector<bool>无法满足其他vector必须的所有条件(例如vector<bool>::reference并不返回真正的lvalue,vector<bool>::iterator不是个真正的随机存取寄存器)。所以某些template程序代码可能适用于任何类型的vector,唯独无法应付vector<bool>。此外,vector<bool>比一般vector慢一些,因为所有元素操作都必须转化为bit操作。不过vector<bool>的具体方案也是由实作版本决定,所以性能(包括速度和空间消耗)也可能都有不同。


namespace std{
class vector<bool> {
   public:
        ...
注:看看上面是怎么实现特殊版本的




6.3 deque


为了获取这种能力,deque通常实作为一组独立区块,第一区块朝某方向扩展,最后一个区块朝另一方向扩展。


在对内存区块有所限制的系统中(例如PC系统),deque可以内含更多元素,因为它使用不止一块内存。因此deque的max_size()可能更大。
deque不支持对容量和内存重分配时机的控制。
deque的内存大小事可缩减的。不过,是不是这样做,以及究竟怎么做,由实作版本定义之。


6.4 list


6.5 set和multiset











































































原创粉丝点击