-cpp代码重用(其他继承&模板)

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了-cpp代码重用(其他继承&模板)相关的知识,希望对你有一定的参考价值。

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

一、包含对象成员的类

valarray类:

valarray类由头文件<valarray>支持。它是个模块类。模板特性意味着声明对象时,必须指定具体的数据类型。

valarray<int> q_values;// an array of int
valarray<double> weights;// an array of double
double gpa[5] = 3.1, 3.5, 3.8, 2.9, 3.3;
valarray<double> v1;// an array of double, size 0
valarray<int> v2(8);// an array of 8 int elements
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

下面是这个类的一些方法:

  • operator[]:让你能够访问各个元素。
  • size():返回包含的元素数。
  • sum():返回所有元素的总和。
  • max():返回最大的元素。
  • min():返回最小的元素。

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

对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如,string类将+运算符重载为将两个字符串连接起来;但从概念上说,将两个Student对象串接起来是没有意义的。这也是这里不使用公有继承的原因之一。另一方面,被包含的类的接口部分对新类来说可能是有意义的。例如,可能希望使用string接口中的operator<()方法将Student对象按姓名进行排序,为此可以定义Student::operator<()成员函数,它在内部使用函数string::operator<()。

// studentc.h -- defining a Student class using containment
#ifndef STUDENTC_H_
#define STUDENTC_H_
#include <iostream>
#include <string>   
#include <valarray>
class Student    
private:
    typedef std::valarray<double> ArrayDb;
    std::string name;       // contained object
    ArrayDb scores;         // contained object
    // private method for scores output
    std::ostream & arr_out(std::ostream & os) const;
public:
    Student() : name("Null Student"), scores() 
    explicit Student(const std::string & s)
        : name(s), scores() 
    explicit Student(int n) : name("Nully"), scores(n) 
    Student(const std::string & s, int n)
        : name(s), scores(n) 
    Student(const std::string & s, const ArrayDb & a)
        : name(s), scores(a) 
    Student(const char * str, const double * pd, int n)
        : name(str), scores(pd, n) 
    ~Student() 
    double Average() const;
    const std::string& Name() const;
    double& operator[](int i);
    double operator[](int i) const;
// friends
    // input
    friend std::istream & operator>>(std::istream & is, Student & stu);  // 1 word
    friend std::istream & getline(std::istream & is, Student & stu);     // 1 line
    // output
    friend std::ostream & operator<<(std::ostream & os, const Student & stu);
;
#endif

请注意关键字explicit的用法:

可以用只有一个参数调用的构造函数将用作从参数类型到类类型的隐式转换函数;但这通常不是好主意。在上述第二个构造函数中,第一个参数表示数组的元素个数,而不是数组中的值,因此将一个构造函数用作int到Student的转换函数是没有意义的,所以使用explicit关闭隐式转换。

如果省略该关键字,则可以编写如下所示的代码:

Student doh("Homer", 10);
doh = 5;// 隐式转换
Student st = Student(8);// 8将转换为一个临时Student对象

在这里,马虎的程序员键入了doh而不是doh[0]。如果构造函数省略了explicit,则将使用构造函数调用Student(5)将5转换为一个临时Student对象,并使用“Nully”来设置成员name的值。因此赋值操作将使用临时对象替换原来的doh值。使用了explicit后,编译器将认为上述赋值运算符是错误的。

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

注意最后一个构造函数:

Student(const char * str, const double * pd, int n)
        : name(str), scores(pd, n) 

因为该构造函数初始化的是成员对象,而不是继承的对象,所以在初始化列表中使用的是成员名,而不是类名。初始化列表中的每一项都调用与之匹配的构造函数,即name(str)调用构造函数string(const char *),scores(pd, n)调用构造函数ArrayDb(const double *, int)

成员列表项初始化顺序:与排列顺序无关,谁先在类中被声明,就先初始化谁!

使用被包含对象的接口,被包含对象的接口不是公有的,但可以在类方法中使用它。如下面代码中实现的Average()函数,就使用了被包含对象——scores的sum()和size()方法。

// studentc.cpp -- Student class using containment
#include "studentc.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;
//public methods
double Student::Average() const 
    if (scores.size() > 0)
        return scores.sum() / scores.size();
    else
        return 0;

const string& Student::Name() const 
    return name;

double& Student::operator[](int i) 
    return scores[i];         // use valarray<double>::operator[]()

double Student::operator[](int i) const 
    return scores[i];

// private method
ostream& Student::arr_out(ostream &os) const 
    int i;
    int lim = scores.size();
    if (lim > 0) 
        for (i = 0; i < lim; i++) 
            os << scores[i] << " ";
            if (i % 5 == 4)
                os << endl;
        
        if (i % 5 != 0)
            os << endl;
     else
        os << " empty array ";
    return os;

// friends
// use string version of operator>>()
istream& operator>>(istream &is, Student &stu) 
    is >> stu.name;
    return is;

// use string friend getline(ostream &, const string &)
istream& getline(istream &is, Student &stu) 
    getline(is, stu.name);
    return is;

// use string version of operator<<()
ostream& operator<<(ostream &os, const Student &stu) 
    os << "Scores for " << stu.name << ":\\n";
    stu.arr_out(os);  // use private method for scores
    return os;
// use_stuc.cpp -- using a composite class
// compile with studentc.cpp
#include <iostream>
#include "studentc.h"
using std::cin;
using std::cout;
using std::endl;
void set(Student &sa, int n);
const int pupils = 3;
const int quizzes = 5;
int main() 
    Student ada[pupils] = Student(quizzes), Student(quizzes), Student(quizzes);
    int i;
    for (i = 0; i < pupils; ++i)
        set(ada[i], quizzes);
    cout << "\\nStudent List:\\n";
    for (i = 0; i < pupils; ++i)
        cout << ada[i].Name() << endl;
    cout << "\\nResults:";
    for (i = 0; i < pupils; ++i) 
        cout << endl << ada[i];
        cout << "average: " << ada[i].Average() << endl;
    
    cout << "Done.\\n";
    return 0;

void set(Student &sa, int n) 
    cout << "Please enter the student's name: ";
    getline(cin, sa);
    cout << "Please enter " << n << " quiz scores:\\n";
    for (int i = 0; i < n; i++)
        cin >> sa[i];
    while (cin.get() != '\\n')
        continue;

二、私有继承

C++还有另一种实现has-a关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分但可以在派生类的[成员函数]中使用它们

下面更深入地探讨接口问题。使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是is-a关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。

使用私有继承,类将继承实现。例如,如果从String类派生出Student类,后者将有一个String类组件,可用于保存字符串。另外,Student方法可以使用String方法来访问String组件。

包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。

因此私有继承提供的特性与[包含]相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。接下来介绍如何使用私有继承来重新设计Student类。

// studenti.h -- defining a Student class using private inheritance
#ifndef STUDENTC_H_
#define STUDENTC_H_
#include <iostream>
#include <valarray>
#include <string>   
class Student : private std::string, private std::valarray<double>    
private:
    typedef std::valarray<double> ArrayDb;
    // private method for scores output
    std::ostream & arr_out(std::ostream & os) const;
public:
    Student() : std::string("Null Student"), ArrayDb() 
    explicit Student(const std::string & s)
            : std::string(s), ArrayDb() 
    explicit Student(int n)
            : std::string("Nully"), ArrayDb(n) 
    Student(const std::string & s, int n)
            : std::string(s), ArrayDb(n) 
    Student(const std::string & s, const ArrayDb & a)
            : std::string(s), ArrayDb(a) 
    Student(const char * str, const double * pd, int n)
            : std::string(str), ArrayDb(pd, n) 
    ~Student() 
    double Average() const;
    double & operator[](int i);
    double operator[](int i) const;
    const std::string & Name() const;
// friends
    // input
    friend std::istream & operator>>(std::istream & is, Student & stu);  // 1 word
    friend std::istream & getline(std::istream & is, Student & stu);     // 1 line
    // output
    friend std::ostream & operator<<(std::ostream & os, const Student & stu);
;
#endif
// studenti.cpp -- Student class using private inheritance
#include "studenti.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;
// public methods
double Student::Average() const 
    if (ArrayDb::size() > 0)
        return ArrayDb::sum() / ArrayDb::size();
    else
        return 0;

const string &Student::Name() const 
    return (const string &) *this;

double & Student::operator[](int i) 
    return ArrayDb::operator[](i);         // use ArrayDb::operator[]()

double Student::operator[](int i) const 
    return ArrayDb::operator[](i);

// private method
ostream & Student::arr_out(ostream &os) const 
    int i;
    int lim = ArrayDb::size();
    if (lim > 0) 
        for (i = 0; i < lim; i++) 
            os << ArrayDb::operator[](i) << " ";
            if (i % 5 == 4)
                os << endl;
        
        if (i % 5 != 0)
            os << endl;
     else
        os << " empty array ";
    return os;

// friends
// use String version of operator>>()
istream & operator>>(istream &is, Student &stu) 
    is >> (string &) stu;
    return is;

// use string friend getline(ostream &, const string &)
istream & getline(istream &is, Student &stu) 
    getline(is, (string &) stu);
    return is;

// use string version of operator<<()
ostream & operator<<(ostream &os, const Student &stu) 
    os << "Scores for " << (const string &) stu << ":\\n";
    stu.arr_out(os);  // use private method for scores
    return os;

1.初始化基类组件

对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数:

Student(const char * str, const double * pd, int n)
       : std::string(str), ArrayDb(pd, n) 

在这里,ArrayDb是std::valarray<double>的别名。成员初始化列表使用std::string(str),而不是name(str)。

2.访问基类的方法

使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。例如,在类声明中提出可以使用average()函数。和包含一样,要实现这样的目的,可以在公有Student::average()函数中使用私有Student::Average()函数。

私有继承使得能够使用类名和作用域解析运算符来调用基类的公有方法。

对比最先前的Student类与现在Student类的Average()函数的实现:

// 先前版本(有成员变量private ArrayDb scores;)
double Student::Average() const 
    if (scores.size() > 0)
        return scores.sum()/scores.size();
    else
        return 0;

// 私有继承版本
double Student::Average() const 
    if (ArrayDb::size() > 0)
        return ArrayDb::sum()/ArrayDb::size();
    else
        return 0;

总之,使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法。

3.访问基类对象

使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢?例如,Student类的包含版本实现了Name()方法,它返回string对象成员name;但使用私有继承时,该string对象没有名称。那么,Student类的代码如何访问内部的string对象呢?

答案是使用强制类型转换。由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象。本书前面介绍过,指针this指向用来调用方法的对象,因此*this为用来调用方法的对象,在这个例子中,为类型为Student的对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:

const string & Student::Name() const 
    return (const string &) *this;

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

4.访问基类的友元函数

用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:

ostream& operator<<(ostream& os, const Student& stu) 
    os << "Scores for " << (const string&) stu << ":\\n";
    ...

如果plato是一个Student对象,则下面的语句将调用上述函数,stu将是指向plato的引用,而os将是指向cout的引用: cout << plato;

下面的代码:

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

 显式地将stu转换为string对象引用,进而调用函数operator<<(ostream &, const String &)

// use_stui.cpp -- using a class with private inheritance
// compile with studenti.cpp
#include <iostream>
#include "studenti.h"
using std::cin;
using std::cout;
using std::endl;
void set(Student &sa, int n);
const int pupils = 3;
const int quizzes = 5;
int main() 
    Student ada[pupils] = Student(quizzes), Student(quizzes), Student(quizzes);
    int i;
    for (i = 0; i < pupils; i++)
        set(ada[i], quizzes);
    cout << "\\nStudent List:\\n";
    for (i = 0; i < pupils; ++i)
        cout << ada[i].Name() << endl;
    cout << "\\nResults:";
    for (i = 0; i < pupils; i++) 
        cout << endl << ada[i];
        cout << "average: " << ada[i].Average() << endl;
    
    cout << "Done.\\n";
    // cin.get();
    return 0;

void set(Student &sa, int n) 
    cout << "Please enter the student's name: ";
    getline(cin, sa);
    cout << "Please enter " << n << " quiz scores:\\n";
    for (int i = 0; i < n; i++)
        cin >> sa[i];
    while (cin.get() != '\\n')
        continue;

由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用种方式呢?大多数C++程序员倾向于使用包含。首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。总之,使用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要3个string对象,可以使用包含声明3个独立的string成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。

然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。

另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。

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

保护继承:

保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:

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

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的(比如类对象在main()中是不能调用保护成员的)。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。

下表总结了公有、私有和保护继承。隐式向上转换(implicit upcasting)意味着无需进行显式类型转换,就可以将基类指针或引用指向派生类对象。

class Student : private std::string, private std::valarray<double> 
public: ⇐⇤⇤⇤◙
    using std::valarray<double>::min;// 重新定义了【访问权限】
    using std::valarray<double>::max;
    ...
;

上述using声明使得valarray<double>::min()valarray<double>::max()可用,就像它们是Student的公有方法一样:

Student ada[pupils] = Student(quizzes), ...;
----------------------------------------------
cout << "high score:" << ada[i].max() << endl;

注意,using声明只使用成员名——没有圆括号、函数特征标和返回类型。例如,为使Student类可以使用valarray的operator 方法,只需在Student类声明的公有部分包含下面的using声明:

using std::valarray<double>::operator[];

这将使两个版本(const和非const)都可用。这样,便可以删除Student::operator[]()的原型和定义。using声明只适用于继承,而不适用于包含。

有一种老式方式可用于在私有派生类中重新声明基类方法,即将方法名放在派生类的公有部分,如下所示:

class Student : private std::string, private std::valarray<double> 
public:
    std::valarray<double>::operator[];// redeclare as public, just use name
    ...

这看起来像不包含关键字using的using声明。这种方法已被摒弃,即将停止使用!因此,如果编译器支持using声明,应使用它来使派生类可以使用私有基类中的方法。

多重继承(MI)

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

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

必须使用关键字public来限定每一个基类。因为缺省情况下,默认是私有派生。

MI可能会给程序员带来很多新问题。其中两个主要的问题是:从两个不同的基类继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例。为解决这些问题,需要使用一些新规则和不同的语法。因此,与使用单继承相比,使用MI更困难,也更容易出现问题。由于这个原因,很多C++用户强烈反对使用MI,一些人甚至希望删除MI;而喜欢MI的人则认为,对一些特殊的工程来说,MI很有用,甚至是必不可少的;也有一些人建议谨慎、适度地使用MI。

看一个例子,要使用MI,需要几个类。我们将定义一个抽象基类Worker,并使用它派生出Waiter类和Singer类。然后,便可以使用MI从Waiter类和Singer类派生出SingingWaiter类。

// worker0.h  -- working classes
#ifndef WORKER0_H_
#define WORKER0_H_
#include <string>
class Worker    // an abstract base class
 private:
	std::string fullname;
	long id;
 public:
	Worker() : fullname("no one"), id(0L) 
	Worker(const std::string& s, long n) : fullname(s), id(n) 
	virtual ~Worker() = 0;   // pure virtual destructor
	virtual void Set();
	virtual void Show() const;
;

class Waiter : public Worker 
 private:
	int panache;
 public:
	Waiter() : Worker(), panache(0) 
	Waiter(const std::string& s, long n, int p = 0) : Worker(s, n), panache(p) 
	Waiter(const Worker& wk, int p = 0) : Worker(wk), panache(p) 
	void Set();
	void Show() const;
;

class Singer : public Worker 
 protected:
	enum  other, alto, contralto, soprano, bass, baritone, tenor ;
	enum  Vtypes = 7 ;
 private:
	static char* pv[Vtypes];    // string equivs of voice types
	int voice;
 public:
	Singer() : Worker(), voice(other) 
	Singer(const std::string& s, long n, int v = other) : Worker(s, n), voice(v) 
	Singer(const Worker& wk, int v = other) : Worker(wk), voice(v) 
	void Set();
	void Show() const;
;
#endif
// worker0.cpp -- working class methods
#include "worker0.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// Worker methods
Worker::~Worker() // 【must】 implement virtual destructor, even if pure
void Worker::Set() 
	cout << "Enter worker's name: ";
	getline(cin, fullname);
	cout << "Enter worker's ID: ";
	cin >> id;
	while (cin.get() != '\\n')
		continue;

void Worker::Show() const 
	cout << "Name: " << fullname << "\\n";
	cout << "Employee ID: " << id << "\\n";


// Waiter methods
void Waiter::Set() 
	Worker::Set();
	cout << "Enter waiter's panache rating: ";
	cin >> panache;
	while (cin.get() != '\\n')
		continue;

void Waiter::Show() const 
	cout << "Category: waiter\\n";
	Worker::Show();
	cout << "Panache rating: " << panache << "\\n";


// Singer methods
char* Singer::pv[] =  "other", "alto", "contralto",
					   "soprano", "bass", "baritone", "tenor" ;
void Singer::Set() 
	Worker::Set();
	cout << "Enter number for singer's vocal range:\\n";
	int i;
	for (i = 0; i < Vtypes; i++) 
		cout << i << ": " << pv[i] << "   ";
		if (i % 4 == 3)
			cout << endl;
	
	if (i % 4 != 0)
		cout << endl;
	while (cin >> voice && (voice < 0 || voice >= Vtypes))
		cout << "Please enter a value >= 0 and < " << Vtypes << endl;
	while (cin.get() != '\\n')
		continue;

void Singer::Show() const 
	cout << "Category: singer\\n";
	Worker::Show();
	cout << "Vocal range: " << pv[voice] << endl;
// worktest.cpp -- test worker class hierarchy
#include <iostream>
#include "worker0.h"
const int LIM = 4;
int main() 
	Waiter bob("Bob Apple", 314L, 5);
	Singer bev("Beverly Hills", 522L, 3);
	Waiter w_temp;
	Singer s_temp;
	Worker* pw[LIM] =  &bob, &bev, &w_temp, &s_temp ;
	int i;
	for (i = 2; i < LIM; i++)
		pw[i]->Set();
	for (i = 0; i < LIM; i++) 
		pw[i]->Show();
		std::cout << std::endl;
	
	return 0;

假设首先从Singer和Waiter公有派生出SingingWaiter:

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

因为Singer和Waiter都继承了一个Worker组件,因此SingingWaiter将包含两个Worker组件,如下图:

通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:

SingingWaiter ed;
Worker * pw = &ed;// ambiguous

通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象:

 

Worker * pw1 = (Waiter *) &ed;// the Worker in Waiter
Worker * pw2 = (Singer *) &ed;// the Worker in Singer

包含两个Worker对象拷贝还会导致其他的问题。然而,真正的问题是:为什么需要Worker对象的两个拷贝?唱歌的侍者和其他Worker对象一样,也应只包含一个姓名和一个ID。C++引入多重继承的同时,引入了一种新技术——虚基类(virtual base class),使MI成为可能。

关于纯虚析构函数

有纯虚函数的类是无法创建类的实例对象的,所以在基类中声明一个纯虚函数,可以避免不经意直接创建了基类的实例对象,毕竟有纯虚函数的类是一个抽象类,本就不应该也不应该能创建该类的实例。

首先,由于基类析构函数是虚函数,所以析构时会从派生类一直析构到基类。又由于基类的析构函数是纯虚函数,所以,如果它没有实现体的话,当析构到基类时,由于没有实现体,就会导致基类对象无法析构,最终导致了析构畸形,因此,特殊的地方就在于这里,纯虚析构函数需要提供一个实现体,以完成对基类对象的析构。

➊虚基类

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关紧要):

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

 然后,可以将SingingWaiter类定义为:

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

现在,SingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本(请参见下图)。因为SingingWaiter现在只包含了一个Worker子对象,所以可以使用多态。

您可能会有这样的疑问:
•为什么使用术语“虚”?
•为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为多MI的准则呢?
•是否存在麻烦呢?
        首先,为什么使用术语虚?毕竟,在虚函数和虚基类之间并不存在明显的联系。C++用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。例如,如果新关键字与重要程序中的重要函数或变量的名称相同,这将非常麻烦。因此,C++对这种新特性也使用关键字virtual——有点像关键字重载。

        其次,为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为MI的准则呢?第一,在一些情况下,可能需要基类的多个拷贝;第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应当的;第三,这样做有其缺点,将在下一段介绍。
        最后,是否存在麻烦?是的。为使虚基类能够工作,需要对C++规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码。例如,将SingingWaiter类添加到Worker集成层次中时,需要在Singer和Waiter类中添加关键字virtual。

➋新的构造函数规则

使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:

class A 
    int a;
public:
    A(int n = 0) : a(n) 
    ...
;

class B : public A 
    int b;
public:
    B(int m = 0; int n = 0) : A(n), b(m) 
    ...
;

class C : public B 
    int c;
public:
    C(int q = 0, int m = 0, int n = 0) : B(m, n), c(q) 
    ...
;

C类的构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。这里,C类的构造函数使用值q,并将值m和n传递给B类的构造函数;而B类的构造函数使用值m,并将值n传递给A类的构造函数。

如果Worker是虚基类,则这种信息自动传递将不起作用。例如,对于下面的MI构造函数:

SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other)
             : Waiter(wk, p), Singer(wk, v)  // 有缺陷!

存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter和Singer)将wk传递给Worker对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员panache和voice,但wk参数中的信息将不会传递给子对象Waiter。然而,编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker的默认构造函数。如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该是这样:

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

上述代码将显式地调用构造函数Worker(const Worker &)请注意,这种用法是合法的,对于虚基类,[必须]这样做;但对于非虚基类,则是[非法]的。

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

哪个方法?

假设要在SingingWaiter类中扩展Show()方法。因为SingingWaiter对象没有新的数据成员,所以可能会认为它只需使用继承的方法即可。这引出了第一个问题。假设没有在SingingWaiter类中重新定义Show()方法,并试图使用SingingWaiter对象调用继承的Show()方法:

SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Show();// ambiguous

在多重继承中,每个直接祖先都有一个Show()函数,这使得上述调用是二义性的。

可以使用作用域解析运算符来澄清意图:

SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
newhire.Singer::Show();// use Singer version

然而,更好的方法是在SingingWaiter中重新定义Show(),并指出要使用哪个Show()。例如,如果希望SingingWaiter对象使用Singer版本的Show(),则可以这样做:

void SingingWaiter::Show() 
    Singer::Show();

可以通过同时调用Waiter版本的Show()来补救:

void Worker::Show() const 
    cout << "Name:" << fullname << endl;
    cout << "Employee ID:" << id << endl;

void Waiter::Show() const 
    Worker::Show();
    cout << "Panache rating:" << panache << endl;

void HeadWaiter::Show() const 
    Waiter::Show();
    cout << "Presence rating:" << presence << endl;

----------------------------------------------------
void SingingWaiter::Show() 
    Singer::Show();
    Waiter::Show();

然而,这将显示姓名和ID两次,因为Singer::Show()和Waiter::Show()都调用了Worker::Show()。
如果解决呢?一种办法是使用模块化方式,而不是递增方式,即提供一个只显示Worker组件的方法和一个只显示Waiter组件或Singer组件(而不是Waiter和Worker组件)的方法。然后,在SingingWaiter::Show()方法中将组件[组合]起来。例如,可以这样做:

void Worker::Data() const 
    cout << "Name:" << fullname << "\\n";
    cout << "Employee ID:" << id << "\\n";

void Waiter::Data() const 
    cout << "Panache rating:" << panache << "\\n";

void Singer::Data() const 
    cout << "Vocal range:" << pv[voice] << "\\n";

void SingingWaiter::Data() const 
    Singer::Data();
    Waiter::Data();

void SingingWaiter::Show() const 
    cout << "Category:singing waiter\\n";
    Worker::Data();
    Data();

采用这种方式,对象仍可使用Show()方法。而Data()方法只在类内部可用,作为协助公有接口的辅助方法。然而,使Data()方法成为私有的将阻止Waiter中的代码使用Worker::Data(),这正是保护访问类的用武之地。如果Data()方法是保护的,则只能在继承层次结构中的类中使用它,在其他地方则不能使用。

另一种办法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。

Set()方法取得数据,以设置对象值,该方法也有类似的问题。例如,SingingWaiter::Set()应请求Worker信息一次,而不是两次。对此,可以使用前面的解决方法。可以提供一个受保护的Get()方法,该方法只请求一个类的信息,然后将使用Get()方法作为构造块的Set()方法集合起来。

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

下面程序清单列出了修改后的类声明和实现:

// workermi.h  -- working classes with MI
#ifndef WORKERMI_H_
#define WORKERMI_H_
#include <string>
class Worker   // an abstract base class
private:
    std::string fullname;
    long id;
protected:
    virtual void Data() const;
    virtual void Get();
public:
    Worker() : fullname("no one"), id(0L) 
    Worker(const std::string & s, long n) : fullname(s), id(n) 
    virtual ~Worker() = 0; // pure virtual function
    virtual void Set() = 0;
    virtual void Show() const = 0;
;

class Waiter : virtual public Worker 
private:
    int panache;
protected:
    void Data() const;
    void Get();
public:
    Waiter() : Worker(), panache(0) 
    Waiter(const std::string & s, long n, int p = 0)
            : Worker(s, n), panache(p) 
    Waiter(const Worker & wk, int p = 0)
            : Worker(wk), panache(p) 
    void Set();
    void Show() const;
;

class Singer : virtual public Worker 
protected:
    enum other, alto, contralto, soprano, bass, baritone, tenor;
    enum Vtypes = 7;
    void Data() const;
    void Get();
private:
    static char *pv[Vtypes];    // string equivs of voice types
    int voice;
public:
    Singer() : Worker(), voice(other) 
    Singer(const std::string & s, long n, int v = other)
            : Worker(s, n), voice(v) 
    Singer(const Worker & wk, int v = other)
            : Worker(wk), voice(v) 
    void Set();
    void Show() const;
;

// multiple inheritance
class SingingWaiter : public Singer, public Waiter 
protected:
    void Data() const;
    void Get();
public:
    SingingWaiter()  
    SingingWaiter(const std::string & s, long n, int p = 0, int v = other)
            : Worker(s,n), Waiter(s, n, p), Singer(s, n, v) 
    SingingWaiter(const Worker & wk, int p = 0, int v = other)
            : Worker(wk), Waiter(wk,p), Singer(wk,v) 
    SingingWaiter(const Waiter & wt, int v = other)
            : Worker(wt),Waiter(wt), Singer(wt,v) 
    SingingWaiter(const Singer & wt, int p = 0)
            : Worker(wt),Waiter(wt,p), Singer(wt) 
    void Set();
    void Show() const; 
;
#endif
// workermi.cpp -- working class methods with MI
#include "workermi.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// Worker methods
Worker::~Worker()  
// ----protected methods----
void Worker::Data() const 
    cout << "Name: " << fullname << endl;
    cout << "Employee ID: " << id << endl;

void Worker::Get() 
    getline(cin, fullname);
    cout << "Enter worker's ID: ";
    cin >> id;
    while (cin.get() != '\\n')
        continue;


// Waiter methods
void Waiter::Set() 
    cout << "Enter waiter's name: ";
    Worker::Get();
    Get();

void Waiter::Show() const 
    cout << "Category: waiter\\n";
    Worker::Data();
    Data();

// ----protected methods----
void Waiter::Data() const 
    cout << "Panache rating: " << panache << endl;

void Waiter::Get() 
    cout << "Enter waiter's panache rating: ";
    cin >> panache;
    while (cin.get() != '\\n')
        continue;


// Singer methods
char * Singer::pv[Singer::Vtypes] = "other", "alto", "contralto",
            "soprano", "bass", "baritone", "tenor";
void Singer::Set() 
    cout << "Enter singer's name: ";
    Worker::Get();
    Get();

void Singer::Show() const 
    cout << "Category: singer\\n";
    Worker::Data();
    Data();

// ----protected methods----
void Singer::Data() const 
    cout << "Vocal range: " << pv[voice] << endl;

void Singer::Get() 
    cout << "Enter number for singer's vocal range:\\n";
    int i;
    for (i = 0; i < Vtypes; i++) 
        cout << i << ": " << pv[i] << "   ";
        if ( i % 4 == 3)
            cout << endl;
    
    if (i % 4 != 0)
        cout << '\\n';
    while (cin >>  voice && (voice < 0 || voice >= Vtypes) )
        cout << "Please enter a value >= 0 and < " << Vtypes << endl;
    while (cin.get() != '\\n')
        continue;


// SingingWaiter methods
void SingingWaiter::Set() 
    cout << "Enter singing waiter's name: ";
    Worker::Get();
    Get();

void SingingWaiter::Show() const 
    cout << "Category: singing waiter\\n";
    Worker::Data();
    Data();

// ----protected methods----
void SingingWaiter::Data() const 
    Singer::Data();
    Waiter::Data();

void SingingWaiter::Get() 
    Waiter::Get();
    Singer::Get();
// workmi.cpp -- multiple inheritance
// compile with workermi.cpp
#include <iostream>
#include <cstring>
#include "workermi.h"
const int SIZE = 5;
int main() 
   using std::cin;
   using std::cout;
   using std::endl;
   using std::strchr;
   Worker * lolas[SIZE];
   int ct;
   for (ct = 0; ct < SIZE; ct++) 
        char choice;
        cout << "Enter the employee category:\\n"
            << "w: waiter  s: singer  "
            << "t: singing waiter  q: quit\\n";
        cin >> choice;
        while (strchr("wstq", choice) == NULL) 
            cout << "Please enter a w, s, t, or q: ";
            cin >> choice;
        
        if (choice == 'q')
            break;
        switch(choice) 
            case 'w': lolas[ct] = new Waiter; break;
            case 's': lolas[ct] = new Singer; break;
            case 't': lolas[ct] = new SingingWaiter; break;
        
        cin.get();
        lolas[ct]->Set();
    
    cout << "\\nHere is your staff:\\n";
    int i;
    for (i = 0; i < ct; i++) 
        cout << endl;
        lolas[i]->Show();
    
    for (i = 0; i < ct; i++)
        delete lolas[i];
    cout << "Bye.\\n";
    // cin.get();
    // cin.get();
    return 0; 

下面介绍其他一些有关MI的问题:
1.混合使用虚基类和非虚基类
再来看一下通过多种途径继承一个基类的派生类的情况。如果基类是虚基类,派生类将包含基类的一个子对象;如果基类不是虚基类,派生类将包含多个子对象。当虚基类和非虚基类混合时,情况将如何呢?例如,假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类,而类M是从C、D、X和Y派生而来的。在这种情况下,类M从虚派生祖先(即类C和D)那里共继承了一个B类子对象,并从每一个非虚派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含三个B类子对象。当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
2.虚基类和支配
使用虚基类将改变C++解析二义性的方式。使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。

那么,一个成员名如何优先于另一个成员名呢?派生类中的名称优先于直接或间接祖先类中的相同名称。例如,在下面的定义中:

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中的q()定义,因为类C是从类B派生而来的。因此,F中的方法可以使用q()来表示C::q()。另一方面,任何一个omg()定义都不优先于其他omg()定义,因为C和E都不是对方的基类。所以,在F中使用非限定的omg()将导致二义性。
虚二义性规则与访问规则无关,也就是说,即使E::omg()是私有的,不能在F类中直接访问,但使用omg()仍将导致二义性。同样,即使C::q()是私有的,它也将优先于D::q()。在这种情况下,可以在类F中调用B::q(),但如果不限定q(),则将意味着要调用不可访问的C::q()。

MI小结:

首先复习一下不使用虚基类的MI。这种形式的MI不会引入新的规则。然而,如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。即在从GunSlinger和PokerPlayer派生而来的BadDude类中,将分别使用Gunslinger::draw()和PokerPlayer::draw()来区分从这两个类那里继承的draw()方法。否则,编译器将指出二义性。

如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。在某些情况下,这可能正是所希望的,但通常情况下,多个基类实例都是问题。

接下来看一看使用虚基类的MI。当派生类使用关键字virtual来指示派生时,基类就成为虚基类:

class marketing : public virtual reality ...;

主要变化(同时也是使用虚基类的原因)是,从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其他要求:

  • 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
  • 通过优先规则解决名称二义性。

正如你看到的,MI会增加编程的复杂程度。然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。避免这种情况后,唯一需要注意的是,在必要时对继承的名称进行限定。

类模板

继承(公有、私有或保护)和包含并不总是能够满足重用代码的需要。例如,Stack类(参见第10章)和Queue类(参见第12章)都是容器类(container class),容器类设计用来存储其他对象或数据类型。例如,第10章的Stack类设计用于存储unsigned long值。可以定义专门用于存储double值或string对象的Stack类,除了保存的对象类型不同外,这两种Stack类的代码是相同的。然而,与其编写新的类声明,不如编写一个泛型(即独立于类型的)栈,然后将具体的类型作为参数传递给这个类。这样就可以使用通用的代码生成存储不同类型值的栈。第10章的Stack示例使用typedef处理这种需求。然而,这种方法有两个缺点:首先,每次修改类型时都需要编辑头文件;其次,在每个程序中只能使用这种技术生成一种栈,即不能让typedef同时代表两种不同的类型,因此不能使用这种方法在同一个程序中同时定义int栈和string栈。

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

C++库提供了多个模板类,本章前面使用了模板类valarray,第4章介绍了模板类vector和array,而第16章将讨论的C++标准模板库(STL)提供了几个功能强大而灵活的容器类模板实现。本章将介绍如何设计一些基本的特性。

定义&使用类模板:

原来的类声明如下:

typedef unsigned long Item;
class Stack 
private:
    enum MAX = 10;
    Item items[MAX];
    int top;
public:
    Stack();
    bool isempty() const;
    bool isfull() const;
    bool push(const Item& item);
    bool pop(Item& item);
;

现在用模板类:

// stacktp.h -- a stack template
#ifndef STACKTP_H_
#define STACKTP_H_

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

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


template <class Type>
bool Stack<Type>::isempty() 
    return top == 0;


template <class Type>
bool Stack<Type>::isfull() 
    return top == MAX;


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


template <class Type>
bool Stack<Type>::pop(Type & item) 
    if (top > 0) 
        item = items[--top];
        return true;
    
    return false; 

#endif

较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename代替class。

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

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

仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。例如,下面的代码创建两个栈,一个用于存储int,另一个用于存储string对象:

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

看到上述声明后,编译器将按Stack<Type>模板来生成两个独立的类声明和两组独立的类方法。类声明Stack<int>将使用int替换模板中所有的Type,而类声明Stack<string>将用string替换Type。当然,使用的算法必须与类型一致。

// stacktem.cpp -- testing the template stack class
#include <iostream>
#include <string>
#include <cctype>
#include "stacktp.h"
using std::cin;
using std::cout;
int main() 
    Stack<std::string> st;   // create an empty stack
    char ch;
    std::string po;
    cout << "Please enter A to add a purchase order,\\n"
         << "P to process a PO, or Q to quit.\\n";
    while (cin >> ch && std::toupper(ch) != 'Q') 
        while (cin.get() != '\\n')
            continue;
        if (!std::isalpha(ch)) 
            cout << '\\a';
            continue;
        
        switch(ch) 
            case 'A':
            case 'a': cout << "Enter a PO number to add: ";
                      cin >> po;
                      if (st.isfull())
                          cout << "stack already full\\n";
                      else
                          st.push(po);
                      break;
            case 'P':
            case 'p': if (st.isempty())
                          cout << "stack already empty\\n";
                      else 
                          st.pop(po);
                          cout << "PO #" << po << " popped\\n";
                      
                      break;
        
        cout << "Please enter A to add a purchase order,\\n"
             << "P to process a PO, or Q to quit.\\n";
    
    cout << "Bye\\n";
    // cin.get();
    // cin.get();
    return 0; 

深入探讨模板类:

1.不正确地使用指针栈

(1)版本1:string po; 替换为 char* po;

这旨在用char指针而不是string对象来接收键盘输入。这种方法很快就失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间(程序将通过编译,但在cin试图将输入保存在某些不合适的内存单元中时崩溃)。

(2)版本2:string po; 替换为 char po[40];

这为输入的字符串分配了空间。另外,po的类型为char*,因此可以被放在栈中。但数组完全与pop()方法的假设相冲突(算法与类型不一致)。

(2)版本3:string po; 替换为 char* po = new char[40];

这为输入的字符串分配了空间。另外,po是变量,因此与pop()的代码兼容。然而,这里将会遇到最基本的问题:只有一个pop变量,该变量总是指向相同的内存单元。确实,在每当读取新字符串时,内存的内容都将发生改变,但每次执行压入操作时,加入到栈中的的地址都相同。因此,对栈执行弹出操作时,得到的地址总是相同的,它总是指向读入的最后一个字符串。具体地说,栈并没有保存每一个新字符串,因此没有任何用途。

2.正确地使用指针栈

使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。把这些指针放在栈中是有意义的,因为每个指针都将指向不同的字符串。注意,创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针。

例如,假设我们要模拟下面的情况。某人将一车文件夹交付给了Plodson。如果Plodson的收取篮(in-basket)是空的,他将取出车中最上面的文件夹,将其放入收取篮;如果收取篮是满的,Plodson将取出篮中最上面的文件,对它进行处理,然后放入发出篮(out-basket)中。如果收取篮既不是空的也不是满的,Plodson将处理收取篮中最上面的文件,也可能取出车中的下一个文件,把它放入收取篮。他采取了自认为是比较鲁莽的行动——扔硬币来决定要采取的措施。下面来讨论他的方法对原始文件处理顺序的影响。

可以用一个指针数组来模拟这种情况,其中的指针指向表示车中文件的字符串。每个字符串都包含文件所描述的人的姓名。可以用栈表示收取篮,并使用第二个指针数组来表示发出篮。通过将指针从输入数组压入到栈中来表示将文件添加到收取篮中,同时通过从栈中弹出项目,并将它添加到发出篮中来表示处理文件。

应考虑该问题的各个方面,因此栈的大小必须是可变的。下面程序清单重新定义了Stack<Type>类,使Stack构造函数能够接受一个可选大小的参数。这涉及到在内部使用动态数组,因此,Stack类需要包含一个析构函数、一个复制构造函数和一个赋值运算符

以上是关于-cpp代码重用(其他继承&模板)的主要内容,如果未能解决你的问题,请参考以下文章

代码重用和接口重用

c++模板和继承

如何解决这个问题(基础模板与继承)

仅用于代码重用c ++的继承

c++重用和模板

类的继承和重用