C++PrimerPlus6-14-C++中的代码重用

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

  • has-a关系
  • 包含对象成员的类
  • 模板类valarray
  • 私有和保护继承
  • 多重继承
  • 虚基类
  • 创建类模板
  • 使用类模板
  • 模板的具体化

14.1 包含对象成员的类

14.1.1 valarray类简介

头文件valarray,是一个模板类,处理不同的数据类型。使用尖括号指明数据类型。P437

valarray<int> q_values;
valarray<double> weights;

double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3};
valarray<double> v1;
valarray<int> v2(8);                // an array of double, size 0
valarray<int> v3(10, 8);            // an array of 8 int elements, each set to 10
valarray<double> v4(gpa, 4);        // an array of 4 elements, initialized to the first 4 elements of gpa
valarray<int> v5 = {20, 32, 17, 9}; // C++11
  • operator
  • size()
  • sum()
  • max()
  • min()

14.1.2 Student类的设计

使用组合(包含)(private类成员),创建一个包含其他类对象的类。关系是has-a

class Student
{
private:
  string name;              // use a string object for name
  valarray<double> scores;  // use a valarray<double> object for scores
  ...
}

14.1.3 Student类示例

P438

14.2 私有继承

用私有继承来实现has-a关系。基类的公有成员和保护成员都将成为派生类的私有成员,基类方法不会成为派生对象公有接口的一部分。

14.2.1 Student类示例(新版本)

多重继承(multiple inheritance, MI)

class Student : private std::string, private std::valarray<double>
{
public:
  ...
};

14.2.2 使用包含还是私有继承

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

14.2.3 保护继承

class Student : protected std::string, protected std::valarray<double> {...};

当派生类派生出另一个对象时,使用私有继承时,第三代类不能使用基类接口;保护继承时,基类的公有方法在第二代中是保护,在第三代类可以使用。

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

让Student类能够使用valarray类的sum()方法:

double Student::sum() const // public Student method
{
  return std::valarray<double>::sum(); // use privately-inherited method
}

14.3 多重继承

公有MI表示is-a,私有和保护MI可以表示has-a关系。使用MI可能带来的问题:

  1. 从两个不同的基类继承同名方法。
  2. 从两个或更多相关基类继承同一个类的多个实例。

14.3.1 有多少Worker

从Singer和Waiter公有派生出SingingWaiter,因为Singer和Waiter都继承了一个Worker组件,所以SingingWaiter将包含两个Worker组件。
使用 虚基类 virtual base class。

虚基类使从多个类(他们的基类相同)派生出的对象只继承一个基类对象。(virtual和public的次序不关键)

class Singer : virtual public Worker {...};
class Waiter : public virtual Worker {...};

class SingingWaiter : public Singer, public Waiter {...};

这样SingingWaiter对象只包含Worker对象的一个副本。(继承的Singer和Waiter对象共享一个Worker对象)
基类是虚时,禁止信息通过中间类自动传递给基类。除非只使用虚基类的默认构造函数,否则必须显式的调用虚基类的某个构造函数。

SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other)
                  : Worker(wk), Waiter(wk, p), Singer(wk, v) {}

14.3.2 哪个方法

MI可能导致函数调用的二义性。
使用域解析运算符,或者重写方法:

newhire.Singer::Show(); // use Singer version
void SingingWaiter::Show()
{
  Singer::Show();
}

当类通过多条虚途径和非虚途径继承某个指定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
近的同名函数优先级高,如果优先级一样将导致二义性。优先级与访问规则无关,如果一个优先级高的访问规则是私有,意味着要调用不可访问的方法。

class B
{
public:
  short q();
};

class C : virtual public B
{
public:
  long q();
  int omg();
};

class D : public C {};

class E : virtual public B
{
private:
  int omg();
}

class F : public D, public E {};

C的q()优先于B,因此F中可以用q()表示C::q()。
omg()优先级一样,在F中使用omg()将导致二义性。

14.3.3 MI小结

  • 不使用虚基类的MI
    • 需要用::指明方法,否则二义性。
    • 每种途径分别继承非虚基类的一个实例。
  • 使用虚基类的MI
    • 从虚基类的一个或多个实例派生而来的类,只继承一个基类对象。
    • 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数。
    • 通过名称优先规则解决名称二义性。

14.4 类模板

模板提供参数化(parameterized)类型,能够将类型名作为参数传递给接收方来建立类或函数。

14.4.1 定义类模板

template <class Type>
template <typename Type>
template <class Type>
class Stack
{
private:
  enum {MAX=10};
  Type items[MAX];
  int top;
public:
  Stack();
  bool push(const Type& item);
};

template <class Type>
Stack<Type>::Stack()
{
  top = 0;
}

template <class Type>
bool Stack<Type>::push(const Type& item)
{
  if (top < MAX)
  {
    item[top++] = item;
    return true;
  }
  else return false;
}

用模板成员函数替换原有类的方法,每个函数头都将以相同的模板声明打头。类限定符从Stack::改为Stack::

14.4.2 使用模板类

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

与函数模板的区别是,必须显式的提供所需的类型。

14.4.3 深入探讨模板类

可以将指针作为类型,但不好。(如果不对程序做重大修改,将无法很好的工作)。P465

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

template <class T, int n>
ArrayTP<T, n>::ArrayTP(const T& v)
{
  for (int i = 0; i < n; i++) ar[i] = v;
}

ArrayTP<double, 12> eggweights;

14.4.5 模板多功能性

可以将用于常规类的技术用于模板类。例如作为基类、组件类、类型参数。

  • 递归使用:ArrayTP<ArrayTP<int, 5>, 10>
  • 使用多个类型参数:pair<string, int>
  • 默认类型模板参数:template <class T1, class T2 = int> class Topo {…};

14.4.6 模板的具体化

具体化是使用具体的类型生成类声明:

  1. 隐式实例化:ArrayTP<int, 100> stuff;
  2. 显式实例化:使用关键字template并指出所需类型来声明类,template class ArrayTP<string, 100>;
  3. 显式具体化:template <> class Classname {…};
  4. 部分具体化:可以给类型参数之一指定具体的类型,template class Pair<T1, int> {…};

14.4.7 成员模板

模板可用作结构、类、模板类的成员。

14.4.8 将模板用作参数

模板可以包含本身就是模板的参数(套娃)。

14.4.9 模板类和友元

  • 非模板友元
  • 约束模板友元(bound),友元的类型取决于类被实例化时的类型
  • 非约束模板友元(unbound),友元的所有具体化都是类的每一个具体化的友元。

非模板友元,需要为要使用的友元定义显式具体化:

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

约束模板友元,使友元函数本身成为模板:

// 先在类定义前声明每个模板函数
template <typename T> void counts();
template <typename T> void report(T&);

// 然后在函数中再次将模板声明为友元
template <typename TT>
class HasFriendT
{
  friend void counts<TT>();
  friend void report<>(HasFriendT<TT>&);
}

约束模板友元,是在类外面声明的模板的具体化。非约束模板友元,是在类内部声明模板,每个函数具体化都是每个类具体化的友元。非约束友元模板类型参数与模板类类型参数不同。

template <typename T>
class ManyFriend
{
  template <typename C, typename D> friend void show2(C&, D&);
}

14.4.10 模板别名(C++11)

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

// define three typedef aliases
typedef std::array<double, 12> arrd;
typedef std::array<int, 12> arri;
typedef std::array<std::string, 12> arrst;
arrd gallons;
arri days;
arrst months;

template<typename T>
using arrtype = std::array<T, 12>;
arrtype<double> gallons;
arrtype<int> days;
arrtype<std::string> months;

// 将using=用于非模板时,语法与常规typedef等价
typedef const char* pc1;
using pc2 = const char*;

14.5 总结

  • 公有继承建立is-a关系
  • 私有继承和保护继承建立has-a关系,但只继承实现不继承接口(基类的公有接口都将成为派生类的内部接口。)
  • 包含(层次化或组合):开发包含对象成员的类,建立has-a关系、
  • 多重继承(MI)使得在类设计中重用多个类的代码。私有和保护MI建立has-a关系,公有MI建立is-a关系。
    • 可以用类限定符解决名称二义性
    • 使用虚基类避免继承多个基类对象
  • 类模板使得能够创建通用的类设计,类型由类型参数表示。

14.6 复习题

14.7 编程练习


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 cdd@ahucd.cn

×

喜欢就点赞,疼爱就打赏

B站 cdd的庇护之地 github itch