《C++ Primer Plus》读书笔记之十二—C++中的代码重用

第14章  C++中的代码重用

  1、C++代码重用方法:公有继承、使用本身是另一个类的对象的类成员(这种方法称为包含、组合或层次化)、私有或保护继承、类模板等。

  2、模板特性意味着声明对象时,必须指定具体的数据类型。

  3、用于建立has-a关系的C++技术是组合(包含),即创建一个包含其他类对象的类。

  4、接口和实现:使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分

  5、C++和约束:C++包含让程序员能够限制程序结构的特性——使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。

  6、对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。对于成员对象,构造函数则使用成员名。

  7、初始化顺序:当初始化列表包含多个项目时,这些项目被初始化的顺序为它们在类定义中被声明的顺序,而不是它们在初始化列表中的顺序。

  8、私有继承:使用私有继承,基类的公有成员和保护成员都将称成为派生类的私有成员。这意味着基类的方法将不会成为派生类对象公有接口的一部分,即派生类不继承基类的接口,但可以在派生类的成员函数中使用它们。

  9、包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。因此,私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。

  10、使用多个基类的继承被称为多重继承(MI)

  11、包含与私有继承的主要区别:①包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的对象成员。②对于构造函数,包含版本的构造函数的成员初始化列表使用对象名称进行初始化;私有继承的构造函数的成员初始化列表使用类名而不是成员名来初始化。③使用包含将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析操作符来调用方法

  12、访问基类对象:使用类名和作用域解析操作符可以访问基类的方法,但如何使用基类对象本身呢??例如:student类中有数据成员:string name;类中有方法Name()可以返回name;但使用私有继承时,该string对象没有名称。那么student类的方法如何访问内部的string对象呢??答案是使用强制类型转换!!!!

class student:private std::string,private std::valarray<double>

{

};

由于student类是从string类派生来的,因此可以通过强制类型转换,将student对象转换成string对象;结果为继承而来的string对象。*this为用来调用方法的对象。

const string & student::Name()const

{

  return (const string &)*this;

}

该方法返回一个引用,该引用指向用于调用该方法的student对象中的继承而来的string对象。

  13、访问基类友元函数:可以通过显式地转换为基类来调用正确的函数。例如:

ostream & operator<<(ostream & os,const student & stu)

{

  os<<(const string &)stu<<":\n";

}

显式的将stu转换为string对象引用,这与operator<<(ostream &,const string &)函数匹配。stu不会自动转换为string引用,根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针

  14、使用包含还是私有继承:包含能够包括多个同类的子对象,比如某个类可以声明三个独立的string对象。而私有继承则只能则只能使用一个这样的对象(当对象没有名称时,将难以区分)。通常:应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。派生类可以重新定义虚函数,而包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。

  15、保护继承:基类的公有成员和保护成员都将成为派生类的保护成员。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。

  16、使用保护派生或私有派生时,基类的公有成员将成为保护成员和私有成员。假设要让基类的方法在派生类外面使用,方法①定义一个使用该基类方法的派生类方法。②将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如,希望通过student类能够使用valarray的方法min()和max(),可以使用如下using声明:

class student:private std::string,private std::valarray<double>

{

public:

  using std::valarray<double>::min;

  using std::valarray<double>::max;

}

上述声明使得这两个方法就像是student的公有方法一样。

  17、注意,必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生。

  18、多重继承(MI):假设首先从singer和waiter公有派生出singerwaiter:class singerwaiter:public singer,public waiter{...};而singer和waiter是worker的两个派生类。因为singer和waiter都继承了一个worker组件,因此singerwaiter将包含两个worker组件。这将引起问题:

  singerwaiter ed;

  worker *pw=&ed;  // 二义性

通常这种赋值将把基类指针设置为派生对象中的基类对象的地址。正确的方法是使用类型转换来指定对象:

  worker *pw1=(worker *)&ed;

  worker *pw2=(singer *)&ed;

  19、当C++引入多重继承的同时,它引入了一种新技术——虚基类,是MI成为可能。虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个

  class singer:virtual public worker{...};

  class waiter: public virtual worker{...}; 

  class singerwaiter:public singer,public waiter{...};

现在,singerwaiter对象将只包含worker对象的一个拷贝。从本质上说,继承的的singer和waiter对象共享一个worker对象,而不是各自引入自己的worker对象拷贝。因为singerwaiter现在只包含一个worker对象,所以可以使用多态。

  20、使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数,例如B是A的派生类,C是B的派生类:C类的构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。但如果worker是虚基类,则这种信息自动传递将不起作用。例如:

  singerwaiter(const worker & wk,int p=0,int v=0):waiter(wk,p),singer(wk,v){  }

C++在基类是虚拟的时候,禁止信息通过中间类自动传递给基类。即上面的wk参数中的信息将不会传递给子对象worker。

  21、如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式的调用该虚基类的某个构造函数。

  singerwaiter(const worker & wk,int p=0,int v=0):worker(wk),waiter(wk,p),singer(wk,v){  }  // 显式调用构造函数worker(const worker &)。

  22、多重继承可能导致函数调用的二义性。例如,BadDude类可能从Gunslinger类和PokerPlayer类那里继承两个完全不同的Draw()方法。可以使用作用域解析操作符来解决

  singerwaiter newhire("aa",2005,6);

  newhire.singer::show();

不过更好的方法是在singerwaiter中重新定义show(),并指出要使用哪个show()

  23、简而言之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写它们。

  24、类模板:

  template<class T>

  class stack

  {

    T a;

    ...

  };

  如果在类声明中定义了方法(内联函数),则可以省略模板前缀和类限定符。

  25、类模板和成员函数模板不是类和成员函数的定义。它们是C++的编译器指令,说明了如何生成类和成员函数定义。模板的具体实现被称为实例化或具体化。除非编译器实现了新的export关键字,否则将模板成员函数放置在一个独立的实现文件中将无法运行。因为模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。如果编译器实现了export关键字,则可以将模板方法定义放在一个独立地文件中,条件是每个模板声明都以export开始。

  26、stack<int> a;stack<double> b;  编译器将按stack<T>模板生成两个独立的类声明和两组独立的类方法。这里的 T  称为类型参数。注意:必须显式地提供所需地类型,这与常规地函数模板是不同地,因为编译器可以根据函数地参数类型来确定要生成哪种函数。

  27、正确使用指针堆栈模板类:

  template<class T>

  class stack

  {

    enum{SIZE=10};

    int stacksize;

    T * items;

    int top;

    ...

    public:

      explicit stack(int ss);

      bool push(const T &item);

  }

  stack<T>::stack(int ss):stacksize(ss),top(0)

  {

    items=new T[stacksize]; // 动态分配内存

  }

  bool stack<T>::push(const T& item)

  {

    if(top<stacksize)

    {

      items[top++]=item; // 往指针数组插值

      return true;

    }

    else

      return false;

  }

  28、允许指定数组大小地数组模板:方法①在类中使用动态数组和构造函数参数来提供元素数目(如上)。方法②使用模板参数来提供常规数组地大小。如下:

  template<class T,int n>

  class arryTP

  {

    T ar[n];

    ...

  }

  关键字class指出T为类型参数,int指出n地类型为int。这种参数——指定特殊地类型而不是用作通用类型名,称为非类型或表达式参数表达式参数有一些限制:①表达式参数可以是整型、枚举、引用或指针。因此,double m是不合法的。②模板代码不能修改表达式参数的值,也不能使用参数的地址。所以在arryTP模板中不能使用n++和&n等表达式。③在实例化模板时,用作表达式参数的值必须是常量表达式。

注:构造函数方法(方法①)使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈,这样,执行速度将更快,尤其在使用了许多小型数组时

  表达式的缺点:每种数组大小都将生成自己的模板。如:arryTP<double 12> a;和arryTP<double 13> b;将生成两个独立的类声明。但stack<int> a(12);

stack<int> b(13);只能生成一个类声明,并将数组的大小信息传递给类的构造函数。另一个区别是,构造函数方法更通用。

  29、模板的多功能性:①可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可以用作其他模板的类型参数。

  template<class T>

  class Array

  {

    T entry;

  };

  template <class Type>

  class GrowArray:public Array<Type>{...};  // 模板类可用作基类

  template <class Tp>

  class stack

  {

    Array<Tp> ar;  // 用作组件类

  };

  Array<stack<int>> asi;  // 用作其他模板的类型参数

  可以递归使用模板:例如:ArrayTP<ArrayTP<int,5>,10> twodee;这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。与之等效的常规数组为:int twodee[10][5];

  ③模板可以包含多个类型参数:template<calss T1,class T2>

  ④可以为类型参数提供默认值:template<class 他,class T2=int> class Topo{...};这样,如果省略T2的值,编译器将使用int:

  Topo<double double > m1;  // T1is double T2 is double 

  Topo<double > m2;    // T1 is double T2 is int

  30、模板的具体化:类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化。模板以通用类型的方式描述类,而具体化是使用具体的类型生成类声明。

  ①隐式实例化:声明一个或多个对象,指出所需类型,而编译器使用通用模板提供的处方生成具体的类定义:ArrayTP<int ,100> stuff;编译器在需要对象之前,不会生成类的隐式实例化:

  ArrayTP<double,30>* pt;// a pointer,no object needed yet

  pt=new ArrayTP<double,30>;// now an object is needed

  ②显式实例化:当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间 中。例如:template class ArrayTP<string,100>;将 ArrayTP<string,100>声明为一个类。在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。

  ③显式具体化:特定的类型(用于替换模板中的通用类型)的定义。例如:

  template <> class ArrayTP<char *>

  {...};

  31、成员模板:C++模板的新特性:模板可用作结构、类或模板类的成员。

  32、模板可以包含类型参数(如class T)和非类型参数(如int a)。模板还可以包含本身就是模板的参数。

  33、模板类声明也可以有友元。模板的友元分3类:①非模板友元②约束模板友元,即友元的类型取决于类被实例化时的类型。③非约束模板友元,即友元的所有具体化都是类的一个具体化的友元。

  ①非模板友元:

  template<class T>

  class A

  {

    friend void counts();

    friend void report(A<T> &); // 为友元函数提供模板类

  }

上述声明使counts()函数成为模板所有实例化的友元。注意,report()本身并不是模板函数,而只是使用了一个模板参数。这意味着必须为要使用的友元定义显式具体化。

  void report(A<int> &){...};

  void report(A<short> &){...};

  ②约束模板友元:

  修改前一个例子,使友元函数本身成为模板。具体地说,为约束模板友元做准备,要使类的每一个具体化都获得与友元函数匹配的具体化。这分为三步:首先,在类定义的前面声明每个模板函数。

  template <typename T> void counts();

  template <typename T> void report(T &);

然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:

  template<class T>

 

  class A

 

  {

 

    friend void counts<T>();

 

    friend void report<>(A<T> &); 或者friend void report<A<T>>(A<T> &); // 为友元函数提供模板类

 

  }

声明中的<>指出这是模板具体化。例子:

  A<int> a;

则编译器将用int替换TT,并生成下面的类定义: 

  class A<int>

  {

    friend void counts<int>();

    friend void report<>(A<int> &); 或者friend void report<A<int>>(A<int> &); // 为友元函数提供模板类

  }

基于T的具体化将变为int,基于A<T>的具体化将变为A<int>。因此,模板具体化counts(int)和 report<>(A<int> &)被声明为A<int>类的友元。

最后,为友元提供模板定义

  ③非约束模板友元:

  通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。

  template <typename T>

  class B

  {

    template<typename C,typename D>

    friend void show(C &,D &);

  }

 

  总结:这一章的所有机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。

 

posted @ 2018-05-10 16:57  ~君莫笑~  阅读(514)  评论(0编辑  收藏  举报