《C++ PrimerPlus》 第十四章

第十四章 C++中的代码重用

除了公有继承之外,还有其他促进代码重用的方法:

  • 包含/组合/层次话:在类中使用另一个类的对象做成员
  • 私有或保护继承:用于实现has-a关系,即新的类将包含另一个类的对象

14.1 包含对象成员的类

string类

valarray类:这是一个模板类

14.2 私有继承

另一种实现has-a关系的途径——私有继承。

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

“包含”是将对象作为一个命名的成员添加到类中,而“私有继承”将对象作为一个未被命名的继承对象添加到类中。

  1. 初始化基类组件,对于继承类,需要使用成员初始化列表语法,使用类名而不是成员名称来标识构造函数。
  2. 访问基类的方法:可以通过将私有的成员函数包含在一个公有函数中来访问该私有方法。
  3. 访问基类对象:通过this指针和强制类型转换来创建对应的对象或引用
  4. 访问基类的友元函数:用类名显式的限定函数名不适合友元函数,这是因为友元不属于类。然而,可以通过显式的转换为基类来调用正确的函数。

14.2.2 使用包含还是私有继承。

通常,应使用包含来建立has-a关系。如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

14.2.3 保护继承

保护继承是私有继承的变体。使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。

当从派生类派生出另一个类时,私有继承和保护继承之间的区别在于:

使用私有继承时,第三代类将不能使用基类的接口,这是因为继承的公有方法在派生类中将变成私有方法。使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。

各种继承方式:

特征 公有继承 保护继承 私有继承
公有成员变成 派生类的公有成员 派生类的保护成员 派生类的私有成员
保护成员变成 派生类的保护成员 派生类的保护成员 派生类的私有成员
私有成员变成 只能通过基类接口访问 只能通过基类接口访问 只能通过基类接口访问
能否隐式向上转换 能(但只能在派生类中)

隐式向上转换(implicit upcasting):意味着无需进行显式类型转换,就可以将基类指针或引用指向派生类对象。

14.2.4 使用using重新定义访问权限

使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要设即为的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。

另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如,假设希望通过Student类能够使用valarray的方法min()和max(),可以如下书写:

1
2
3
4
5
class Student: private std::string, private std::valarray<double>{
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
}

注意:using声明只使用成员名——没有圆括号、函数特征表和返回类型。

有一种老式的不带using声明的方法,它看起来就像是不包含关键字using的using声明,但是这种方法已被摒弃,即将体制使用。

14.3 多重继承

MI描述的是有多个直接基类的类,与单继承一样,公有MI表示的也是is-a关系。例如,可以从Waiter类和Singer类派生出SingingWaiter类。

私有MI和保护MI可以表示has-a关系。

MI可能会带来很多新问题,其中最重要的问题是:

  • 从两个不同的基类继承同名方法;
  • 从两个或更多相关基类那里继承同一个类的多个实例。

14.3.1 有多少个Work

如果多个类来自于同一个基类,而当前类又继承这多个类,那么,就会有多个最原始的基类副本,者造成了二义性。

为了解决以上问题,C++引入了一种新技术——虚基类(virtual base calss),使MI成为可能。

  1. 虚基类:虚基类使得从多个类(它们的基类相同)派生出的对象之只继承一个基类对象。
  2. 新的构造函数规则:使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。(详细请看p558)

14.3.2 哪个方法

因为多重继承,有时会继承多个同名方法,因此,需要指出使用哪一个方法。

可以使用作用域解析运算符来指定使用的方法:

1
2
SingingWaiter newhire("Elise", 2005, 6, soprano);
newhire.Singer::Show();

然而,更好的方法是在SingingWaiter中重新定义Show(),并指出要使用哪个Show()。如下所示:

1
2
3
void SingingWaiter::Show(){
Singer::Show;
}

对于多继承,使用模块化的方式而不是递增方式来在派生类的同名函数中使用基类函数,即提供一个只显示Work组件的方法和一个只显示Waiter组件 Singer组件的方法。然后,在SingingWaiter::Show()方法中将组件组合起来。详细见p559。

总结: 在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。

下面介绍一些有关MI的问题。

  1. 混合使用虚基类和非虚基类

当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。

  1. 虚基类和支配

派生类中的名称优先于直接或间接祖先类中的相同名称。

如果无法用优先规则判断出使用哪个名称,则会导致二义性。

14.4 类模板

C++的类模板为生成通用的类声明提供了一种更好的方法(C++最初不支持模板,单模板被引入后,就一直在演化,因此有的编译器可能不支持这里的所有特性)。模板提供参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。

C++库提供了多个模板类,如vector、array、valarray等等。

14.4.1 定义类模板

模板类以下面这样的代码开头:

1
2
3
4
5
6
7
template <class T>
class Stack{
private:
...
public:
...
};

这里使用class并不意味着Type必须是一个类,而只是表明Type是一个通用的类型说明符,在使用模板时,将使用时间的类型替换它。较新的C++实现推荐使用关键字typename来代替class

当模板被调用时,Type将被具体的类型值(如intstring)取代。

同样,可以使用模板成员函数替换原有类的类方法,每个函数头都将以相同的模板声明打头(如果在类声明中定义了方法,即内联定义,则可以省略模板前缀和类限定符:

1
2
3
4
template <typename T>
bool Stack<T>::push(const T& item){
...
}

注意: 模板声明本身并不是类和成员函数,它们属于C++编译器指令,说明了如何生成对应的类和成员函数定义。而模板的具体实现则被称为实例化(instantiation)或具体化(specialization)。

不能将模板成员函数放在独立的实现文件中(以前,C++标准确实提供了关键字export,让您能够将模板成员函数放在独立的实现文件中,但支持该关键字的编译器不多,C++11不再这样使用export,而是将其保留用于其他用途)。 由于模板不是函数,它们不能单独编译,模板必须与特定的模板实例化请求一起使用。为此,最简单的方法是就所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。

14.4.2 使用模板类

可以用所需的具体类型替换泛型名,就可以声明一个类型为模板类的对象:

1
2
Stack<int> kernels;
Stack<string> colonels;

注意,必须显式的提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数。

14.4.3 深入探讨模板类

关于使用指针在作为Stack的类型,比如用字符指针替换string来作为T类型。这样会带来一些问题。

  • char* s:单纯的char* s并没有给s分配合适的空间,这会使s的值存在某些不合适的内存单元中
  • char s[40]:这虽然分配了空间,但是s的大小固定,且s本身是数组名,虽然代表地址,但是无法进行运算,有些操作会引起冲突。
  • char* po = new char[40]:这次分配了空间,po也成为了变量,但仍有问题,具体看p573。

但是并不是说不能使用指针作为T,只是在使用时,需要多家注意,考虑谨慎。

14.4.4 数组模板示例和非类型参数

使用非类型参数来说模板达到某些目的

1
2
3
4
5
6
7
8
9
10
template <typename T, int n>
class ArrayTP{
private:
...
public:
...
};

ArrayTP<double,12> one;
ArrayTP<double,13> two;

表达式参数方法的主要缺点是,每组数组的大小都将生成自己的模板,而利用构造函数的方法只会生成一个类声明,并将数组大小信息传递给类的构造函数,详细见p578。

14.4.5 模板多功能性

可以将常规类的技术用于模板类,模板类可以用作基类,也可用作组件类,还可用作其他模板的类型参数。

  1. 递归使用模板
  1. 使用多个类型参数
1
2
3
4
5
6
7
8
9
10
template <typename T1, typename T2>
class Pair{
private:
T1 a;
T2 b;
public:
T1& first();
T2& second();
...
};
  1. 默认模板参数

可以为类型参数提供默认值:

1
template <class T1, class T2 = int> class Topo{...};

14.4.6 模板的具体化

模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。

  1. 隐式实例化(implicit instantiation)

  声明一个或多个对象,指出所需的类型,编译器使用通用模板提供的处方生成具体的类定义:

1
ArrayTP<int, 100> stuff;

  编译器在需要对象之前,不会生成类的隐式实例化,如下面的代码,第二条语句才会使编译器生成类定义,并根据定义创建一个对象

1
2
ArrayTP<double, 30> *pt;
pt = new ArrayTP<double, 30>;

  1. 显式实例化(explicit instantiation)

  当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。在这种情况下,孙然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。(这里没搞懂

1
template class ArrayTP<string, 100>;

  1. 显式具体化(explicit specialization)

  显式具体化是特定类型(用于替换模板中的泛型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。(这块也没看懂

  另外,假设模板是用>运算符来对值进行比较,对于数字,管用。如果T表示一个type,则只要定义了T::operator>()方法,这也管用。但如果T是由const char*表示的字符串,这将不管用。实际上,模板倒是可以正常工作,但字符串将按地址(按照字母顺序)排序。这要求类定义使用strcmp(),而不是>来对值进行比较。

  1. 部分具体化(partial specialization)

  C++允许部分具体化,即部分限制模板的通用性。例如,可以给类型参数之一指定具体类型,下面的代码将T2具体化为int,但T1保持不变:

1
2
3
4
//general template
template <class T1, class T2> class Pair {...};
//specialization with T2 set to int
template <class T1> class Pair<T1, int> {...};

14.4.7 成员模板

模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。

14.4.8 将模板作为参数

模板除了可以包含类型参数(typename T)和非类型参数(int n)之外,还可以包含本身就是模板的参数,如下所示:

1
2
3

template <template <typename T> class Thing> class Crab
//其中,template<typename T>class是类型,Thin是参数

14.4.9 模板类和友元

模板类声明也可以有友元。模板的友元分三类:

  1. 非模板友元
1
2
3
4
5
6
7
template <class T>
class HasFriend{
public:
friend void counts(); //(1)
friend void report(HasFriend &); //(2) 错误
friend void report(HasFriend<T> &); //(3) 正确
}

  上述代码中的(1)式在模板中将一个常规函数声明为友元,该声明使counts()函数成为模板所有实例化的友元,counts()函数不是通过对象调用的(它是友元,不是成员函数),也没有对象参数。它通过以下几种方式访问HasFriend对象:访问全局对象;使用全局指针访问非全局对象;创建自己的对象;访问独立于对象的模板类的静态数据成员。如果要为友元函数提供模板类参数,则不能通过(2)式来达到目的,原因是不存在HasFriend这样的对象,而只有特定的具体化,如HasFriend<short>,这里short可以用T表示,因为参数传递时就会指明T的类型,因此,要提供模板类参数,必须指明具体化。

  1. 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //(1)
    template <typename T> void counts();
    template <typename T> void reprot(T&);
    //(2)
    template <typename T>
    class HasFriendT
    {
    ...
    friend void counts<TT>();
    friend void report<>(HasFriendT<TT> &);
    };

  使友元本身成为模板,使累得每一个具体化都获得与友元匹配的具体化,包含三步:首先,在类定义之前声明每个模板函数,如(1)所示,然后,在函数中再次将模板声明为友元,声明中的<>指出这是模板具体化,对于report()<>可以为空,因为可以从函数参数推断出如下模板类型参数:HasFriendT<TT>,也可以写完整:report<HasFriendT<TT> > (HasFriendT<TT> &)。但counts函数没有参数,因此必须使用模板参数语法<TT>来指明具体化。最后一步是友元提供模板定义。

  1. 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的方式
1
2
3
4
5
template <typename T>
class ManyFriend{
...
template <typename C, typename D> friend void show2(C&, D&);
};

  对于非约束友元,友元模板类型参数与模板类类型参数是不同的,如上代码所示。

14.4.10 模板别名(C++11)

可以使用typedef为模板具体化指定别名:

1
2
3
4
5
6
7
//define three typedef aliases
typedef std::array<double,12> arrd;
typedef std::array<int, 12> arri;
typedef std:array<std::string,12> arrst;
arrd gollons;
arri days;
arrst months;

C++11提供了一种新的可以简化上述任务的方法——使用模板提供一系列别名,如下所示:

1
2
3
4
5
6
template<typename T> using arrtype = std::array<T,12>;
//这将arrtype定义为一个模板别名,可以使用它来指定类型,如下所示
arrtype<double> gallons;
arrtype<int> days;
arrtype<std::string> months;
//总之, arrtype<T> 就表示类型 std::array<T,12>