首页 > 代码库 > 《C++标准程序库》学习笔记(一)C++相关特性

《C++标准程序库》学习笔记(一)C++相关特性

  抱着本厚厚的《C++标准库》读了几天,想想也该写点关于用法的总结,一来怕今后容易忘记,二来将书上的事例重新敲一遍,巩固对程序库相关知识的了解。今天开第一篇,以后不固定更新。当然,笔者所读为该书为基于C++98的第一版,已有一定的年代感,不过虽然C++11的推出已有一定的时日,但是在普及上还需要一定的时间,因而,这本中文译本还是有一定的可读性的。这本书更新的版本为英文第二版,很遗憾还未出现其中文译本。

  由于是开篇,本文所讲都很基础,但这些基础内容对后面的学习是非常重要的。

1  C++标准

  C++的标准化是一个漫长的过程。为何要标准化?作为一个语言,如果不设立相应的标准规格那是万万不能的。世界上有很多C++程序员,他们都有各自的编程风格,如果不做统一的话,那么这门语言将会演变出相当多个版本,这也会让C++进入分崩离析的状态,其中的一个严重后果就是兼容性问题,因而,标准化是必不可少的,标准规格的建立,是C++的一个重要里程碑。

  标准程序库是C++标准规格的一部分,提供一系列核心组件,用以支持I/O、字符串(string)、容器(数据结构)、算法(排序、搜索、合并等等)、数值计算、国别(例如不同字符集,character set)等主题。

2  C++语言特性

  C++语言核心和C++程序库是同时被标准化的。C++每5年会有一个新版本。如果你没有紧跟其发展,可能会对某些新的语言特性大感惊讶。接下来的几个小节,将描述与C++标准程序库有关的几个最重要的语言特性。

2.1   template(模板)

  程序库中几乎所有东西都被设计为template形式。不支持template,就不能使用标准程序库。

  所谓template,是针对“一个或多个尚未明确的类型”所撰写的函数或类别。使用templete时,可以显式地或隐式地将类型当做参数来传递。下面的一个典型例子,传回两数中的较大数:

template <class T>const T& max( const T& a, const T& b){    return a < b ? b : a;}

  在这里,第一行将T定义为任意数据类型,在函数被调用时由调用者指定。任何合法标示符都可以拿来做参数名,但通常以T表示,这差不多成了一个“准”标准。这个类别用关键字class引导,但类型本身不一定得是class-------任何数据类型只要提供template定义式内所用到的操作,都可适用于此template。

  遵循同样原则,你可以将class参数化,并以任意类型作为实际参数。这一点对容器类别非常有用。你可以实现出“有能力操控任意类型元素”的容器。C++标准程序库提供了许多模板容器类。

  欲实现C++标准程序库的完整功能,编译器不仅要提供一般的template支持,还需要很多新的template标准特性,以下分别探讨。

2.1.1  Nontype Templates参数(非类型模板参数)

  类型(type)可以作为模板(template)参数,非类型(nontype)也可以作为模板(template)参数。因而后者可被看为前者的一部分。例如,可以把标准类型bitset<>的bits数量以template参数指定。以下定义了两个由bits构成的容器,分别为32个bits空间和50个bits空间:

    bitset<32> flags32; //32个bits空间的bitset    bitset<50> flags50; //50个bits空间的bitset

  这些bitsets由于使用不同的template参数,所以有不同的类型,不能互相复制(assign)或比较(除非提供了相应的类型转换机制)。

2.1.2  Default Template Parameters(缺省模板参数)

  template classes可以缺省参数。例如一下声明,允许你使用一个或两个template参数来声明MyClass对象:

template <class T, class container = vector<T> >    //注意两个>之间必须有一个空格,如果没写空格“>>”会被解读为移位运算子,导致语法错误。class MyClass;

  如果只传给它一个参数,那么缺省参数可作为第二个参数使用:

    //以下两条语句等价    MyClass<int> x1;    MyClass<int, vector<int> > x1;

  注意:template缺省参数可根据前一个(或前一些)参数定义。

2.1.3  关键字typename

关键字typename被用来作为类型之前的标识符号。考虑下面例子:

template <class T>class MyClass {    typename T::SubType* ptr;    //To do something...};

  这里,typename指出SubType是class T中定义的一个类型,因此ptr是一个指向T::SubType类型的指针。如果没有关键字typename,Subtype会被当成一个static成员,于是:

         T::Subtype * ptr 会被解释为类型T内的数值Subtype与ptr的乘积。

         Subtype成为一个类型的条件是,任何一个用来取代T的类型,其内都必须提供一个内部类型(inner type)Subtype的定义。例如,将类型Q当做template参数:

    MyClass<Q> x;

         必要条件是类型Q有如下的内部类型定义:

class Q {    typedef int SubType;    //To do something...};

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

  typename还可以在template声明中用来替换关键字class:

template <typename T> class MyClass;

2.1.4  Member Template(成员模板)

  类成员函数可以是个template(模板),但是这样的成员模板既不能是virtual,也不能缺省参数。例如:

class MyClass {    //To do something...    template<class T> void f(T);    //To do something...};

  在这里,MyClass::f() 声明了一个成员函数集,适用任何类型参数。只要某个类型提供有f()用到的所有操作,它就可以被当做参数传递进去。这个特性通常用来为模板类中的成员提供自动类型转换。例如以下定义式中,assign()的参数x,其类型必须和调用端所提供的对象的类型完全吻合

template <class T>class MyClass {private:    T value;public:    void assign(const MyClass<T>& x) {  //x必须和*this有同样的T        value =http://www.mamicode.com/ x.value;    }    //To do something...};

  即使两个类型之间可以自动转换,如果我们对assign()使用不同的template类型,也会出错:

void f(){    MyClass<double> d;    MyClass<int> i;    d.assign(d);    //OK    a.assign(i);    //错误,i是MyClass<int>,但这里需要MyClass<double>}

  如果C++允许我们为成员函数提供不同的template类型,就可以放宽“必须精确吻合”这条规则:只要类型可被赋值,就可以被当做上述成员模板函数的参数。

template <class T>class MyClass {private:    T value;public:    template <class X>  //成员模板    void assign(const MyClass<X>& x) {   //允许不同的模板类型        value.x.getValue();    }    T getValue() const {        return value;    }    //To do something...};void f(){    MyClass<double> d;    MyClass<int> i;    d.assign(d);    //OK    a.assign(i);    //OK,int对于double是可赋值的。}

  请注意,现在,assign()的参数x和*this的类型并不相同,所以你不能够直接存取MyClass<>的private成员和protected成员,取而代之的是,此例中你必须使用类似getValue()之类的东西。

  模板构造函数(templeta constructor)是成员模板的一种特殊形式,它通常用于“在复制对象时实现隐式类型转换”。注意,模板构造函数并不遮蔽隐式的复制构造函数。如果类型完全吻合,隐式复制构造函数就会被产生出来并被调用。举个例子:

template <class T>class MyClass {public:    //带隐式类型转换的复制构造函数并不遮蔽隐式的复制构造函数    template <class U> MyClass(const &MyClass<U>& x);};void f(){    MyClass<double> xd;    MyClass<double> xd2(xd);    //调用内建复制构造函数(隐式)    MyClass<int> xi(xd);        //调用模板构造函数    //To do something...}

  在这里,xd2和xd的类型完全一致,所以它被内建的复制构造函数初始化,xi和xd的类型不同,所以它使用模板构造韩式进行初始化。因此,撰写模板复制构造函数时,如果default辅助构造函数不符合你的要求,别忘了自己提供一个复制构造函数。

2.1.5  嵌套模板类

  嵌套类本身也可以是个template:

template <class T>class MyClass {    //To do something...    template <class T2> //嵌套类    class NestedClass;    //...};

2.2   基本类型的显式初始化

  如果采用不含参数的、明确的构造函数调用语法,基本类型会被初始化为零:

    int i1;         //未定义的值    int i2 = int(); //初始化为0

  这个特性可以确保我们在撰写template程序代码时,任何型别都有一个确切的初值。例如下面的这个函数中,x保证被初始化为零。

template <class T> void f(){    T x = T();    //...}

2.3   异常处理

  通过异常处理,C++标准程序库可以在不污染函数接口(亦即参数和返回值)的情况下处理异常。要注意的是这种概念叫做“异常处理”,而不是“错误处理”,两者未必相同。举个例子,许多时候用户的无效输入并非一种异常;这时候最好是在区域范围内采用一般的错误处理技术来处理。

  C++标准库提供了一些通用的异常处理特性,例如标准异常类别(standard exception classes)和auto_ptr类别。

2.4   Namespaces(命名空间)

  越来越多的软件由程序库、模块和组件拼凑而成。各种不同事物的结合,可能导致一场名称大冲突。Namespaces正是用来解决此问题的。

  namespaces将不同的标识符号集合在一个具名作用域(names scope)内。如果你在namespace之内定义所有标识符号,则namespace本身就成了唯一可能与其他全局符号冲突的标识符号。你必须在标识符号前加上namespace名字,才能援引namespace内的符号,这和class处理方式雷同。namespace的名字和标识符号之间以::分割开来(这个符号及其意义和class与其成员之间的联系有点类似)。

//在命名空间josuttis中定义标识符号namespace josuttis {    class File;    void myGlobalFunc();    //...}//使用命名空间中的标识符josuttis::File obj;josuttis::myGlobalFunc();

  不同于class的是,namespace是开放的,你可以在不同模块中定义和扩展namespace。因此你可以使用namespace来定义模块、程序库或组件,甚至在多个文件之间完成。namespace定义的是逻辑模块,而非实质模块。请注意,在UML及其他建模表示法中,模块又被成为package。

  如果某个函数的一个或多个参数类型,定义于函数所处的namespace中,那么你可以不必为该函数指定namespace。这个规则成为Koenig lookup。例如:

//在命名空间josuttis中定义标识符号namespace josuttis {    class File;    void myGlobalFunc(File& );    //...}//使用命名空间中的标识符josuttis::File obj;myGlobalFunc(obj);

           通过using声明(using declaration),我们可以避免一再写出冗长的namespace名称,例如一下声明:

using josuttis::File;       //using declaration

           会使File成为当前作用域内代表josuttis::File的一个同义字。

           Using directive会使namespace内的所有名字曝光,它等于将这些名字声明于namespace之外。但这么一来,名称冲突问题可能死灰复燃。例如:

using namespace josuttis;   //using directive

           会使File和myGlobalFunc()在当前作用域内完全曝光。如果全局范围内已存在同名的File和myGlobalFunc(),而且使用者不加任何资格饰词地使用这两个名字,编译器将东西难辨。

           注意,如果场合(contex)不甚清楚(例如不清楚是在头文件、模块里还是在程序库里),你不应该使用using directive。这个指令可能会改变namespace的作用域,从而使程序代码被包含或使用于另一模块中,导致意外的行为发生。事实上在头文件中使用using directive相当不明智。

           C++标准程序库在namespace std中定义了它的所有标识符号。

2.5   Bool类型

  为了支持布尔值(真假),C++增加了bool类型。Bool可增加程序可读性,并允许你对bool值实现重载动作。两个常数true和false同时亦被引入C++。此外C++还提供bool值与整数值之间的自动转换。0值相当于false,非0值相当于true。

2.6   关键字explicit

  通过关键字explicit的作用,我们可以禁止“单参数构造函数”被用于自动类型转换。典型的例子便是群集类型。你可以将初始长度作为参数传给构造函数,例如你可以声明一个构造函数,以stack的初始大小为参数:

class Stack {    explicit Stack(int size);   //用初始大小size构造Stack    //...};

  在这里,explicit的应用非常重要。如果没有explicit,这个构造函数有能力将一个Int自动转型为Stack。一旦发生这种情况,你甚至可以给Stack指派一个整数值而不会引起任何问题:

    Stack s;    s = 40; //糟糕,创建了一个有40个元素的Stack,并把它赋值给了s

  自动类型转换动作会把40转换为有40个元素的Stack,并指派给s,这几乎肯定不是我们所要的结果。如果我们将构造函数声明为explicit,上述赋值操作就会导致编译错误(那很好)。

  注意,explicit同样也能阻绝“以赋值语法进行带有转型操作的初始化”:

    Stack s1(40);    //OK    Stack s2 = 40;   //Error

  这是因为一下两组操作:

    X x;    Y y(x);     //显式转换

  和

    X x;    Y y = x;     //隐式转换

  存在一个小差异。前者透过显式转换,根据类型X产生了一个类型Y的新对象;后者通过隐式转换,产生了一类型Y的新对象。

2.7   新的类型转换操作符

  为了让你对“参数的显式类型转换”了解更加透彻,C++引入一下4个新的操作符:

2.7.1  static_cast

  将一个值以符合逻辑的形式转换。这可看做是“利用原值重建一个新对象,并在设立初值时使用类型转换”。唯有当上述的类型转换有所定义,真个转换才会成功。所谓“有所定义”,可以是语言内建规则,也可以是程序员自定的转换动作。例如:

    float x;    cout << static_cast<int>(x);        //打印int型的x    f(static_cast<string>("hello"));    //调用f(),参数为string类型,而不是char*

2.7.2  dynamic_cast

  将多态类型向下转型为其实际静态类型。这是唯一在执行期进行检验的转型动作。你可以用它来检验多个多态对象的类型,例如:

class Car;       //抽象的基类,至少用于一个虚函数class Cabriolet : public Car {    //...};class Limousine : public Car {    //...};void f(Car* cp){    Cabriolet* p = dynamic_cast<Cabriolet*>(cp);    if(p == NULL) {        //...    }}

  在这个例子中,面对实际静态类型为Cabriolet的对象,f()有特殊应对行为。当参数是个reference,而且类型转换失败时,dynamic_cast会丢出一个bad_cast异常。注意,从设计者角度而言,你应该在运用多态技术的程序中,避免这种“程序行为取决于具体类型”的写法。

2.7.3  const_cast

  设定或去除类型的常数性,亦可去除volatile饰词。除此之外不允许任何转换。

2.7.4  reinterpret_cast

  此操作符的行为由实际编译器定义。可能重新解释bits意义,但也不一定如此,使用此一转型动作通常带来不可移植性。

 

  这些操作符取代了以往小圆括号所代表的旧式转型,能够清楚阐明转型的目的。小圆括号转型可替换dynamic_cast之外的其他三种转型,也因此当你运用它时,你无法明确显示使用它的确切理由。这些新式转型操作给了编译器更多信息,让编译器清楚知道转型的理由,并在转型失败时释出一份错误报告。

  注意,这些操作符只接受一个参数。试看下面例子:

    static_cast<Fraction>(15, 100) //糟糕,创建了Fraction(100)

  在这个例子中你得不到你想要的结果。它只用一个数值100,将Fraction暂时对象设定初值,而非设定分子15、分母100。逗号在这里并不起分隔作用,而是形成一个comma操作符,将两个表达式组合为一个表达式,并传回第二个表达式作为最终结果。将15和100“转换”为分数的正确做法是:

    Fraction(15, 100);

2.8   常数静态成员的初始化

  如今,我们终于能够在class声明中对“整数型常数静态成员”直接赋予初值。初始化后,这个常数便可用于class之中,例如:

class MyClass {    static const int num = 100;    int elems[num];};

  注意,你还必须为class之中声明的常数静态成员,定义一个空间:

const int MyClass::num; //不用初始化

2.9   main()的定义式

  在C++中有一个重要而又常被误解的问题,那就是正确而可移植的main()的唯一写法。根据C++标准,只有两种main()是可移植的

int main(){    //...}

  和

int main(int argc, char* argv[]){    //...}

  这里argv(命令行参数数组)也可以定义为char**。请注意,由于不允许“不言而喻”的返回类型int,所以返回类型必须明白写为int。你可以使用return语句来结束main(),但不必一定如此。这一点和C不同,换句话说,C++在main()的末尾定义了一个隐式地:

    return 0;

  这意味着如果你不采用return语句离开main(),实际上就表示成功退出(传回任何一个非0值都代表某种失败)。处于这个原因,本范例在main()尾端都没有return语句。有些编译器可能会对此发出警告,那正是标准制定前的黑暗日子。

3 总结

  第一篇到此结束,关于复杂度的问题我在此省略,相信读者能理解。

《C++标准程序库》学习笔记(一)C++相关特性