C++目标是促进代码重用。公有继承是实现这种目标的机制之一,但这并不是唯一的机制。另外一种方法是使用这样的类成员,本身是另一个类的对象。这种方法称为包含(comtainment)、组合(composition)或层次化(layering)。另一种方法是私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系。
多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
14.1 包含对象成员的类
14.1.2 Student类的设计
通常,用于建立has-a关系的C++技术是组合(包含),即创建一个包含其他类对象的类。例如,Student类:
class Student
{
private:
string name;
vector<double> scores;
...
}
上述Student类将数据成员声明为私有的。意味着Student类的成员函数可以使用string和vector<double>类的公有接口来访问和修改name和scores对象,但在类外不能这样做,而只能通过Student类的公有接口访问name和score。对于这种情况,通常被描述为Student类获得了其成员对象的实现,但没有继承接口。
接口实现:
使用继承时,类可以继承接口[函数原型],可能还有实现[函数定义](基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。
14.1.3 Student类示例
// student.h -- defing a Student class using containment
#ifndef STUDENT_H_
#define STUDENT_H_
#include<iostream>
#include<string>
#include<vector>
class Student
{
private:
typedef std::vector<double> ArrayDb;
std::string name;
ArrayDb scores;
std::ostream& arra_out(std::ostream& os)const;
public:
Student() : name("Null Name"), scores() {} // 默认函数
explict Student(const std::string& s) : name(s), socres() {} // 转换构造函数--string
explicit Student(int n) : name("Null Name"), socres(n) {} // 转换构造函数--int, 调用; vector的构造函数
Student(const std::string& s, int n) : name(s), scores(n) {} // 复制构造函数, 调用了vector的构造函数
Student(const std::string& s, const ArrayDb& a) : name(s), scores(a) {} // 复制构造函数,调用了vector的复制构造函数
Student(const char* str, const double* pd, int n) : name(str), scores(pd, a) {} // 复制构造函数,调用了vector构造函数
~Student();
double Average() const;
const std::string& Name() const;
double& operator[](int i);
double operatorp[](int i) const;
// friend
friend std::istream& operator>>(std::istream& is, Student& stu);
friend std::istream& getline(std::istream& is, Student& stu);
friend std::ostream& operator<<(std::ostream& os, Student& stu);
};
#endif
将typedef放在类定义的私有部分意味着在Student类的实现中实现它,但在Student类外面不能使用。
C++和约束:
C++包含让程序员能够限制程序结构的特性--使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。对于成员对象,构造函数则使用成员名。
hasDMA::hasDMA(const hasDMA& hs) : baseDMA(hs) {...}
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)。
如果不使用初始化列表语法,情况将是,C++要去在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
初始化顺序:
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表的顺序。如果一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。
被包含对象的接口不是公有的,但可以在类方法使用它们。例如:
double Student::Average() const
{
if (scores.sizs() > 0)
return scores.sum() / scores.size();
else
return 0;
}
Student类实现代码:
// student.cpp -- Student class using containment
#include "Student.h"
using std::ostream;
using std::cin;
using std::istream;
using std::string;
// public method
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}
const string& Student::Name(0 const
{
return name;
}
double& Student::operator[](int i)
{
return scores[i];
}
double Student::operator[](int i) const
{
return scores[i];
}
// private method
ostream& Student::arr_out(ostream& os) const
{
int lim = score.size();
if (lim > 0)
{
for (int i = 0; i <lim; i++)
{
os << socres[i] << " ";
if (i % 5 == 4)
os << endl;
}
}
else
os << "empty array "
return os;
}
// friends
istream& operator>>(istream& is, Student& stu)
{
is >> stu.name;
return is;
}
istream& getline(istream& is, Student& stu)
{
getline(is, stu.name);
return is;
}
ostream& operator<<(ostream& os, const Studen& stu)
{
os << "Scores for " << stu.name << ":\n";
stu.arr_out(os);
return os;
}
14.2 私有继承
C++还有另一种实现has-a关系的途径--私有继承。使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。这意味着基类方法将不会成为派生类对象接口的一部分,但可以在派生类的成员函数使用它们。
派生类将继承基类的接口;这是is-a关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口,但继承实现。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。
因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。
14.2.1 Student类示例(新版本)
要进行私有继承,请使用关键字private而不是public来定义类。(实际上,private是默认值,因此省略访问限定符也将导致私有继承)。
class Student : private std::string, std::vector<double>
{
...
};
使用多个基类的继承被称为多重继承(multiple inheritance, MI)。通常,MI尤其是公有MI将导致一些问题,必须使用额外的语法规则来解决问题。
Student的包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。
// studenti.h -- defining a Student class using private inheritance
#ifndef STUDENTH_H_
#define STUDENTI_H_
#include <iostream>
#include <vector>
#include <string>
class Student : private std::string, private std::vector<double>
{
private:
typedef std::vector<double> ArrayDb;
std::ostream& arr_out(std::ostream& os) const;
public:
Student() : std::string("Null Name"), ArrayDb() {}
explicit Student(const string& s) : std::string(s), ArrayDb() {}
explicit Student(int n) : std::string("Null Name"), 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() cosnt;
double& operator[](int i);
const std::string& Name() const;
// friends
friend std::istream& operator>>(std::istream& is, Student& stu); // 1 word
friend std::istream& getline(std::istream& is, Student& stu); // 1 line
friend std::ostream& operator<<(std::ostream& os, Student& stu);
};
#endif
1. 初始化基类组件
隐式地继承组件而不是成员对象将影响代码的编写,因为再也不能使用name和scores来描述对象了。而必须使用公有继承的技术。
对于继承类,新版本的构造函数将使用成员初始化列表语法,他使用类名而不是成员名来标识构造函数:
Student(const char* str, const double* pd, int n) : std::string(str), ArrayDb(pd, n){}
2. 访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法。包含使用对象名来调用方法。私有继承能够使用类名和作用域解析符来调用基类的方法。
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
3. 访问基类对象
使用作用域解析运算符可以访问基类方法,但如果要使用基类本身,该如何做? 答案是使用强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象。
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";
...
}
引用stu不会自动转换为string引用。根本原因在于,在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。然而,及时这个例子使用公有继承,也必须使用显式类型转换。原因之一在于,如果不使用类型转换,下述代码将与友元函数原型匹配,从而导致递归调用。另一个原因是,由于这个类使用的是多重继承,编译器将无法确定应转换成那个基类,如果两个基类都是用函数opertor<<()。
// 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 (Array::size() > 0)
return Array::sum() / Array::size();
else
return 0;
}
const string& Student::Name() const
{
return (const string&) *this;
}
double& Student::operator[](int i)
{
return ArrayDb::operator[](i);
}
double Student::operator[](int i) const
{
return ArrayDb::operator[](i);
}
// private methods
ostream& Student::arr_out(ostream& os) const
{
int lim = ArrayDb::size();
if (lim > 0)
{
for (int i = 0; i < lim; i++)
{
os << ArrayDb::operator[](i) << " ";
if (i % 5 == 4)
{
os << endl;
}
}
}
else
os << " empty array ";
return os;
}
// friends
istream& operator>>(istream& is, Student& stu)
{
is >> (string&) stu;
return is;
}
ostream& getline(istream& is, Student& stu)
{
getlilne(is, (string&)stu);
return is;
}
ostream& operator>>(ostream& os, const Student& stu)
{
os << " Scores for " << (const string&) stu << ":\n";
stu.arr_out(os);
return os;
}
14.2.2 使用包含还是私有继承
大多数C++程序倾向于使用包含。首先,它易于理解。类声明中包含表示的显式命名对象,代码可以使用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问题,尤其多个基类继承时,可能必须处理狠毒问题。而包含同名方法的独立的基类或共享祖先的独立基类。另外,包含能够包括多个同类的子对象。
然而,私有继承所提供的特性的确比包含多。比如,派生类能访问基类的保护成员。以及派生类能重新定义虚函数,而包含不行。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
14.2.3 保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:
class Student : protected std::string, protected std::vector<double>
{
...
};
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类也是可用的,但在继承体系层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
隐式向上转换(implicit upcasting)意思着进行显式类型转换,就可以将基类指针或引用指向派生类对象。
特征 | 公有继承 | 保护继承 | 私有继承 |
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
14.2.4 使用using重新定义访问权限
假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。
class Student : private std::string, private std::vector<double>
{
...
public:
using std::vector<double>::min;
using std::vector<double>::max;
...
};
注意,using声明只使用成员名--没有圆括号、函数特征标和返回类型。using声明只适用继承,而不适用于包含。
14.3 多重继承
class SingWaiter : public Waiter, public Singer {...};
请注意,必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生。
MI可能会给程序员带来很多新问题。其中两个主要问题是:从两个不同的基类继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例。
// worker0.h -- working classes
#ifndef WORKER0_H_
#define WORKER0_H_
#include <string>
class Worker
{
private:
std::string fullName;
long int id;
public:
Worker() : fullName("no one"), id(0L) {}
Worker(const std::string& s, long int n) : fullName(s), id(n) {}
virtual ~Worker() = 0;
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 int 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, soprana, bass, bari, tenor};
enum {Vtypes = 7};
private:
static char* pv[Vtypes];
int voice;
public:
Singer() : Worker(), voice(other);
Singer(const std::string& s, long int n, int v = other) : Worker(s, n), voice(v) {}
Singer(const Worker& wk, int v = other) : Worker(wk), voice(v) {}
void set();
void show();
};
#endif
// worker0.cpp -- working class methods
#include"work0.h"
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
// worker methods
Worker::~Worker(){}
void Worker::set()
{
cout << "Enter worker's name: ";
getline(cin, fullName);
cout << "Enter worker' 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() != '\0')
continue;
}
void Waiter::show() const
{
cout << "Category; waiter\n";
Worker::show();
cout << "Panache rating" << panache << '\n';
}
// Singer methods
char* Singer::pv[] = {"other". "alto"...};
void Singer::set()
{
Worker::set();
cout << "Enter number fro single's vocal range";
for (int i = 0; i < Vtypes; i++)
{
cout << i << ":" << pv[i] << " ";
if (i % 4 == 3)
cout << endl;
}
if (int % 4 != 0)
cout <<endl;
while (cin.get() != '\n')
continue;
}
void Singer::show() const
{
cout << "Category: \n";
Worker::show();
cout << "Vocal range: " << pv[voice] << endl;
}
14.3.1 有多少worker
假设Singer和Waiter公有类派生出SingerWaiter,由于Singer和Waiter都继承了一个Worker组件,因此SingerWorker将包含两个Worker组件。这将引起二义性。
SingerWaiter ed;
Worker* pw = &ed; // wrong!
C++引入多重继承的同时,引入了一种新技术--虚基类(virtual base class),使得MI成为可能。
1. 虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字virtual,可以使得Worker被作为Single和Waiter的虚基类。(virtual和public次序无关紧要)。
class Singer : vitual public Worker {...};
class Waiter : vitual public Worker {...};
class SingerWaiter : public Singer, public Waiter {...};
现在SingerWaiter对象将只包含Worker对象的一个副本。从本质来说,继承的Singer和Worker对象共享一个Worker对象,而不是各自引入自己的Worker对象副本。
C++ 用户强烈反对引入新的关键字,因为这将给他们带来很大压力。C++对于虚基类这种新特性也使用关键字virtual--有点像关键字重载。
使虚行为成为MI的准则原因?第一,在一些情况下,可能需要基类的多个拷贝;第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应该的;第三,
2. 新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数即基类构造函数。但这些构造函数可能需要将信息传递给基类。
但对于Worker虚基而言,上述的信息自动传递(成员初始化列表)将不起作用。例如:
SingerWorker(const Worker& wk, int p = 0, int v = Singer::other) : Worker(wk, p), Singer(wk, v) {} // failed!
存在问题是,传递Worker的路径有两条(Waiter和Singer)将wk传递给Worker对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。但是,编译器必须在构造派生类对象之前构造基类对象组件;在上述情况下,编译器将使用默认的Worker的默认构造函数。传递给基类Worker的信息无效。
如果不希望默认构造函数构造基类,则需要显式地调用所需的基类构造函数。因此,构造函数应该是:
SingerWaiter(const Worker& wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk, p), Singer(wk, v) {}
上述代码将显式调用构造函数worker(const Worker&)。请注意,这种用法只对虚基类是合法的,对非虚基类,是非法的。
如果有间接虚基类,则除非只需使用虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
14.3.2 那个方法
除了修改构造函数规则外,MI通常还要求调整其他代码。假设没有在SingerWaiter类中重新定义show方法,并试图继续使用SingerWorker对象调用继承的show方法。对于单继承而言,如果没有重新定义show方法,则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个show函数,这将使得二义性。
此时可以使用作用域解析运算符来澄清程序员的意图。
SingerWaiter newgire{...};
newgire.Singer::show(); // use Singer version
当然,更好的方法是在SingerWaiter中重新定义show(),并指出要使用哪个show()。
void SingerWaiter::show()
{
Singer::show();
}
对于单继承而言,让派生类调用基类方法是可以的。
void Worker::show() const
{
cout << "Name: " << fullName << "\n";
cout << "Employee ID: " << id << "\n";
}
void Waiter::show() const
{
Worker::show();
cout << "Panache rating: " << panache << "\n";
}
void Singer::show() const
{
Worker::show();
cout << " Vocal range: " << voice << "\n";
}
// 调用了两次Worker::show();
void SingerWaiter::show() const
{
Singer::show();
Waiter::show();
}
解决调用两次基类的一种方法是使用模块化方式,而不是递增方式,即只提供一个显式Worker组件的方法和一个只显示Waiter组件或Singer组件(而不是Waiter组件个Worker组件)的方法。然后,在SingerWaiter::show()将组件组合起来。例如:
void Work::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: " << voice << "\n";
}
void Singer::show() const
{
Worker::show();
data();
}
void SingerWaiter::data() const
{
Singer::data();
Waiter::data();
}
void SingerWaiter::show() const
{
cout << "Category: \n";
Worker::data();
data();
}
与此类似,继承链上其他类可以组合适当的Data组件。
采用这种模块组件方式,对象仍可使用show方法。而Data方法是能在类内部使用,作为协助公有接口的辅助方法。然而,使data方式成为私有将阻止Waiter中的代码使用Work::data(),这正是保护访问类的用武之地。如果data()是保护的,则只能在继承层次结构中的类使用它,在其他地方不能使用。
另一种方法是将所有数据组件都设置为保护,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。
总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。
// workeri.h -- working classes
#ifndef WORKERI_H_
#define WORKERI_H_
#include <string>
class Worker
{
private:
std::string fullName;
long int id;
protected:
virtual void data() const;
virtual void get();
public:
Worker() : fullName("no one"), id(0L) {}
Worker(const std::string& s, long int n) : fullName(s), id(n) {}
virtual ~Worker() = 0;
virtual void set();
virtual void show() const;
};
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 int 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, soprana, bass, bari, tenor};
enum {Vtypes = 7};
void data() const;
void get();
private:
static char* pv[Vtypes];
int voice;
public:
Singer() : Worker(), voice(other);
Singer(const std::string& s, long int n, int v = other) : Worker(s, n), voice(v) {}
Singer(const Worker& wk, int v = other) : Worker(wk), voice(v) {}
void set();
void show();
};
// MI
class SingerWaiter : public Singer, public Waiter
{
protected:
void data() const;
void get();
public:
SingerWaiter() {}
SingerWaiter(const std::string& s, long n, int p = 0, int v = other) : Worker(s, n), Waiter(s, n, p), Singer(s, n, v){}
SingerWaiter(const Worker& wk, int p = 0, int v = other) : Worker(wk), Waier(wk, p), Singer(wk, v){}
// 使用基类指针指向派生类对象,且使用复制构造函数与另一个派生类的构造函数
SingerWaiter(const Waiter& wt, int v = other) : Worker(wt), Waiter(wt), Singer(wt, p) {}
// 使用基类指针指向派生类对象,且使用复制构造函数与另一个派生类的构造函数
SingerWaiter(const Singer& sg, int p = 0) : Worker(sg), Waiter(sg, p), Singer(sg) {}
void set();
void show() const;
};
#endif
介绍一些与MI有关的问题:
1. 混合使用虚基类和非虚基类
如果基类是虚基类,派生类将包含基类的一个字对象;如果基类不是虚基类,派生类将包含多个子对象。当类通过多条虚路径和多条非虚路径继承某个特定的基类时,该类将包含一个表示所有虚路径的基类子对象和分别表示各条非虚路径的多个基类子对象。
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派生而来的。另一方面,任何一个omg()定义都不优于其他omg()定义,因为E和C都不是对方的基类。所以,在F中使用非限定的omg()将导致二义性。
虚二义性与访问规则无关,也就是说,即使E::omg()是私有的,不能在F类中直接访问,但是用omg()仍将导致二义性。同样,即使C::q()是私有的,它也将优于B::q()。
14.3.3 MI小结
如果一个类从两个不同的类哪里继承了两个同门的成员,则需要在派生类中使用类限定符来区分它们。
在派生类使用virtual指定派生时,基类就成为了虚基类。主要变化是,从虚基类的一个或多个实例派生而来的类只继承了一个基类对象。为实现该要求,必须满足以下要求:
有间接虚基类的派生类包含直接调用间接基类的构造函数的构造函数,这对于间接非虚基类来说是非法的;
通过优先规则解决名称二义性;
需要注意的是,在必要时对继承的名称进行限定。
14.4 类模板
容器类设计用来存储其他对象或数据类型。
相比于模板而言,使用typedef有两种缺点,首先,每次修改类型时都需要编辑头文件;其次,在每个程序中只能使用这种技术生产一种类型,而不能让typedef同时代表两种不同的类型。
C++的类模板为生成通用的类声明提供了一种更好的方法。模板提供参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。
14.4.1 定义类模板
模板类的定义如下;
template <class Type>
class ClassName
{
...
};
关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
这里使用的class并不意味Type必须是一个类;而只是表面Type是一个通用的类型说明符,在使用模板时,将使用实际的类型来替换它。
Type items[MAX];
template<class Type>
bool Stack<Type>::push(const Type& item)
{
...
}
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
知道这些模板不是类和成员函数定义至关重要。它们是C++编译器指令,说明了如何生成类和成员函数定义。模板的具体实现--如何来处理string对象的栈类--称为实例化(instantiation)或具体化(specialization)。不能将模板成员函数放在独立的实现文件中。
模板必须与特定的模板实例化请求一起使用,为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
// 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;
}
else
return false;
}
template <class Type>
bool Stack<Type>::pop(Type& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else
return false;
}
#endif
14.4.2 使用具体类
仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。
Stack<int> kernels; //
编译器将按Stack<Type>模板来生成一个独立的类声明和一组独立的类方法。类声明Stack<int>将使用int替换模板中所有的Type。
泛型标识符--Type,称为类型参数(type parameter),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型。
注意,必须显式地提供类型,这与常规的函数模板有所不同,因为编译器可以根据函数的参数类型来确定要生成那种函数。
14.4.3 深入探讨模板类
1. 不正确地使用指针栈
Stack<char*> st;
char* po; // version1
char po[40]; // version2
char* po = new char[40]; // version3
version1版本因为仅仅创建指针,却没有分配存储指针所指对象的内存。编译错误。
version2版本虽然分配了内存,但数组完全和pop()方法的假设相冲突。首先引用变量item(请看pop函数定义)必须引用某种类型的左值,而不是数组名。
version3分配了内存,同时po也是变量,因此与pop()方法兼容。然而,这将遇到最基本的问题:只有一个po变量,该变量总是指向相同的内存单元。具体地说,栈压入或弹出都是在同一个地址。因此没有任何意义。
2. 正确使用栈
使用指针栈方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。
// stacktp1.h -- modified Stack template
#ifndef STACK1TP_H_
#define STACK1TP_H_
template <class Type>
class Stack
{
private:
enum {SIZE = 10};
int stackSie;
Type* items;
int top;
public:
explicit Stack(int ss = SIZE);
Stack(const Stack& st);
~Stack() { delete [] items; }
bool isEmpty() { return top == 0; }
bool isFull() { return top == stackSie; }
bool push(const Type& item);
bool pop(Type& item);
Stack& operator=(const Stack& st);
};
template <class Type>
Stack<Type>::Stack(const Stack& st) // 参数列表的Stack<Type>简写成Stack
{
stackSize = st.stackSize;
top = st.top;
items = new Type[stackSize];
for (int i = 0; i < stackSize; i++)
items[i] = st.items[i];
}
template <class Type>
Stack<Type>::push(const Type& item)
{
if (top < stackSize)
{
items[top++] = item;
return true;
}
else
return false;
}
template <class Type>
bool Stack<Type>::pop(Type& item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else
return flase;
}
template <class Type>
Stack<Type>& Stack<Type>::operator=(const Stack<Type>& st)
{
if (this == &st)
return *this;
delete[] items;
stackSize = st.stackSize;
top = st.top;
items = new Type[stackSize];
for (int i = 0; i < top; i++)
items[i] = st.items[i];
return *this;
}
#endif
原型将赋值运算符函数的返回类型声明为Stack引用,而实际的模板函数定义将;类型定义为Stack<Type>。前者是后者的缩写,但只能在类中使用。及可以在模板声明或模板函数定义内使用Stack,但在类外面,即指定返回类型或使用作用域解析符时,必须使用完整的Stack<Type>。
14.4.4 数组模板实例和非类型参数
模板常用作容器类,这是因为模板参数的概念非常适合于将相同的存储方法用于不同的类型。为容器类提供可重用代码是引入模板的主要动机。
一个允许指定数组大小的简单数组模板。一种方法是在类中使用动态数组和构造函数参数来提供元素数目,上述Stack类模板采用此种方法。另一种方法是使用模板参数来提供常规数组的大小。
// arraytp.h -- Array Template
#ifndef
#define
template <class T, int n>
class ArrayTP
{
private:
T ar[n];
public:
ArrayTP() {};
explicit ArrayTP(const T& v);
virtual T& operator[](int i);
virtual T operator[](int i) const;
};
template <class T, int n>
ArrayTP<T, n>::ArrayTP(const T& v)
{
for (int i = 0; u < n; i++)
ar[i] = v;
}
template <class T, int n>
T& Array<T, n>::operator[](int i)
{
if (i < 0 || i >=n)
{
std::cerr << "Error in array limits: " << i << " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
template <class T, int n>
T Array<T, n>::operator[](int i) const
{
if (i < 0 || i >=n)
{
std::cerr << "Error in array limits: " << i << " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}
#endif
关键字class(typename)指出T是类型参数,int指出n的类型为int,n为非类型或者表达式参数。
表达式参数有一些限制。表达式参数可以是整型、枚举、引用或指针。所以,double是不合法的,double*是合法的。另外,模板代码不能修改参数的值,也不能使用参数的地址。所以,在ArrayTP模板中不能使用诸如n++或&n等表达式。另外,实例化模板时,用作表达式参数的值必须是常量表达式。
与Stack中构造函数方法相比,使用模板参数有一个优点。构造函数方法使用的是通过new和delete管理的堆内存,而模板参数是使用的为自动变量维护的内存栈。这样,执行速度更快,尤其在使用了很多小型数组时。但模板参数的主要缺点是,每种数组大小都将生成自己的模板。
// 两个独立的类声明
ArrayTP<double, 12> egg;
ArrayTP<double, 13> donuts;
// 一个独立的类声明,并将数组大小信息传递给构造函数
Stack<double> egg(12);
Stack<double> donut(13);
另一区别在于,构造函数更通用,这是因为数组大小作为类成员(而不是硬编码)存储在定义中,这样可以一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
14.4.5 模板多功能性
模板类可用作基类,也可以用作构造类,还可以用作其他模板的类型参数。
template <typename T>
class Array
{
private:
T entry;
...
};
template <typename T>
class GrowArray : public Array<Type> {...}; // inheritance
template <typename T>
class Stack
{
private:
Array<T> ar; // use Array<> as a component
...
};
Array< Stack<int> > asi; // an array of stacks of int
在最后一个语句中,C++98要求使用至少一个空白字符将两个>符号分来,避免与>>混淆。C++11不要求这样做。
1. 递归模板
另一个模板多功能性例子是,可以递归使用模板。
Array< Array<int, 5>, 10> twodee;
// 上式等价声明如下:
int twodee[10][5];
这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。请注意,在模板语法中,维的顺序与等价的二维数组相反。
2. 使用多个模板参数
模板可以包含多个类型参数。
template <class T1, class T2>
class Pair
{
private:
T1 a;
T2 b;
public:
...
};
3. 默认类型参数
类模板的另一项新特性是,可以为类型参数提供默认值。
Template <class T1, class T2 = int> class Topo {...};
Topo<double, double> m1;
Topo<double> m2;
虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
14.4.6 模板的具体化
类模板与函数模板很相似,因为可以有隐式实例化,显式实例化和显式具体化,他们统称为具体化(specialization)。
1. 隐式实例化
它们声明一个或多个对象,指出所需的类型,而编译器试用通用模板提供的处方生成具体的类定义。
Array<int, 100> stuff; // implicit instantialtion
编译器在需要对象之前,不会生成类的隐式实例化。
Array<int, 10>* pt; // a pointer, no object needed yet
pt = new Array<int, 30>; // now an object is needed
2. 显式实例化
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化(explicit instantiation)。声明必须位于模板定义所在的名称空间中。
template class Array<string, 100>; // generate Array<string, 100> class
在这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明。
3. 显式具体化
显式具体化(explicit specilization)是特定类型(用来替换模板中的泛型)的定义。有时候,可能需要在位特殊类型实例化时,对模板参数进行修改,使其行为不同。在这种情况下,可以创建显式具体化。
例如,假设模板使用>运算符对值进行比较。字符类比较与数值类比较两种比较方式的。所以可以提供一个显式模板具体化,而不是为泛型定义的模板。当具体化模板和通用模板都与实例化请求匹配时,编译器将采用具体化版本。
具体化类模板定义的格式如下:
template <> class ClassName<specializied-type-name> {...};
// 例如
template <> class ClassName<const char*> {...};
4. 部分具体化
C++还允许部分具体化(partial specilization),即限制部分模板的通用性。例如:
// general template
template <class T1, class T2> class Pair {...};
// partial tempalte
template <class T1> class Pair<T1, int> {...};
关键字template后面的<>声明的是没有具体化的类型参数。因此,上述第二个将T2具体化为int,但T1保持不变,注意,如果指定所有类型,则<>内将为空,这将导致显式具体化。
如果多个模板可供选择,编译器将使用具体化程度最高的模板。例如:
Pair<double, double> p1; // use general template
Pair<double, int> p2; // use Pair<T1, int> partial specialization
Pair<int, int> p3; // use Pair<int, int> explicit specialization
部分具体化特性使得能够设置各种限制。
// general template
template <class T1, class T2, class T3> class Trio {...};
// specialization with T3 set to T2
template <class T1, class T2> class Trio<T1, T2, T2> {...};
// specialization with T3 and T2 set to T1*
template <class T1, class T2> class Trio<T1, T1*, T1*> {...};
14.4.7 成员模板
模板可作为结构、类或模板类的成员。
// template.cpp -- template members
#include<iostream>
using std::cout;
using std::endl;
template <class T>
class Beta
{
private:
template <class V>
class Hold
{
private:
V val;
public:
Hold(V v = 0) : val(v) {}
void show() const { cout << val << endl; }
V value() const { return val; }
};
Hold<T> q;
Hold<int> n;
public:
Beta(T t, int i) : q(t), n(i) {}
template <class U>
U blab(U u, T t) { return (n.value() + q.Value()) * u / t; }
void show() const { q.show(); n.show(); }
};
blab()方法的U类型由该方法被调用时的参数显式确定,T类型由对象的实例化确定。很老的编译器根本不接受模板成员,而另一些编译器接受模板成员,但不接受类外面的定义。然而,如果所用的编译器接受类外面的定义,则在Beta模板之外定义模板成员的代码如下:
template <class T>
class Beta
{
private:
class Hold;
Hold<T> q;
Hold<int> n;
public:
Beta(T t, int i) : q(t), n(i) {}
template <class U>
U blab(U u, T t) { return (n.value() + q.Value()) * u / t; }
void show() const { q.show(); n.show(); }
};
template <class T>
template <class V>
class Hold
{
private:
V val;
public:
Hold(V v = 0) : val(v) {}
void show() const { cout << val << endl; }
V value() const { return val; }
};
template <T>
template <V>
class Hold
{...}
必须使用上述形式,而不能使用template<class T, class V>形式来代替。
14.4.8 将模板用作参数
模板可以包含类型参数和非类型参数。模板本身还可以包含本身就是模板的参数。
template <template <class T> class Thing>
class Crab
{...};
Crab<Stack> nebula; // 声明了对象
模板参数是template <class T> class Thing, 其中template <class T> class是类型,Thing是参数。
14.4.9 模板类和友元
模板类声明也可以有友元。模板的友元分为3种:
非模板友元;
约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元
1. 模板类的非模板友元函数
在模板类中将一个常规函数声明为友元:
template <class T>
class HasFriend
{
public:
friend void show();
...
};
假设要为友元函数提供模板类参数,只有特定的具体化。原因在于不存在HasFriend这样的对象,例如:
template <class T>
class HasFriend
{
public:
friend void report(HasFriend<T> &);// OK! 具体化
friend void report(HasFriend &);// Wrong!
};
也就是说带HasFriend<int>参数的report()将成为HasFriend<int> 类的友元。带HasFriend<double>参数的report()将是report()的一个重载版本。
2. 模板类的约束模板友元函数
使友元函数本身成为模板。具体地说,为约束模板友元做准备,要使类的每一个具体化都获得与友元匹配的具体化。需要三个步骤才能完成;
首先,在类定义的前面声明每个模板函数
template <class T> void counts();
template <class T> void report(T&);
然后,在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:
template <class TT>
class HasFriend
{
...
friend void counts<TT>();
friend void report<>(HasFriend<TT>&);
};
声明中的<>指出这是模板具体化。对于report(),<>可以为空,因为可以从函数参数推断出如下模板参数HasFriend<TT>。然而,也可以使用report< HasFriend<TT> >(HasFriend<TT>&)。但是counts必须使用模板参数语法<TT>来指明其具体化。
第三个步骤是。为友元提供模板定义。
template <class T>
void counts()
{
cout << ...
}
template <class T>
void reprot(T& hf)
{
cout << hf.item << endl;
}
// temp2tmp.cpp -- template friends to a template class
#include<iostream>
using std::cout;
using std::endl;
// template prototype
template <class T> void counts();
template <class T> void report(T&);
// template class
template <class TT>
class HasFriendT
{
private:
TT item;
static int ct;
public:
HasFriendT(const TT& i) : item(i) { ct++; }
~HasFriendT() { ct--; }
//friend
friend void counts<TT>();
friend void reports<>(HasFriendT<TT>&);
};
template <class T>
int HasFriendT<T>::ct = 0;
template <class T>
void counts()
{
cout << ...
}
template <class T>
void reprot(T& hf)
{
cout << hf.item << endl;
}
3. 模板类的非约束模板友元函数
通过类内声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型与模板类类型参数是不同的。
template <class T>
class ManyFriend
{
...
template <class C, class D> friend void show2(C&, D&);
};
// many.cpp -- template friends to a template class
#include<iostream>
using std::cout;
using std::endl;
// template class
template <class TT>
class ManyFriend
{
private:
TT item;
public:
ManyFriendT(const TT& i) : item(i) { ct++; }
//friend
template <class C, class D> friend void show2(C&, D&);
};
template <class C, class D>
int HasFriendT<T>::ct = 0;
template <class T>
void show2(C&, D&)
{
cout << ...
}
14.4.10 模板别名(C++11)
如果能为类型指定别名,将很方便。在模板设计中尤其如此。可使用typedef为模板具体化指定别名:
typedef array<double, 12> arrd;
C++11 还新增了一项功能--使用模板提供了一系列别名,如下所示:
template <class T>
using arrtype = std::array<T, 12>;
C++11 还允许将using=用于非模板。其语法跟typedef等价
typedef const char* pc1;
using pc2 = const char*;
C++11 还新增的另一项模板功能是可变参数模板(variadic template),让您能够定义这样的模板类和模板函数。
14.5 总结
私有继承和保护继承也能重用代码,但建立的是has-a关系。
使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;
使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员;
无论使用保护或私有继承,基类的公有成员都将成为派生类的内部接口。这有时候被称为继承实现,但不继承接口。因为派生类对象不能显示地使用基类的接口。因此,不能将派生类对象看成是一种基类对象。即在不进行强制类型转换的情况下,基类指针或引用不能指向派生类对象。
还可以通过包含对象成员的类来重用代码。这种方法称为包含、层次化或组合,它建立的也是has-a关系,与私有或保护继承相比,包含更容易实现和使用,所以通常优先采取这种方法实现has-a关系。但继承允许派生类访问基类的保护成员;还允许派生类重新定义从基类那里继承的虚函数。另一方面,如果需要使用某个类的几个对象,则用包含合适。
多重继承(MI)使得在类设计中重用多个类的代码、私有MI或保护MI建立has-a关系,而公有MI建立is-a关系。MI会带来一些问题,即多次定义同一个名称,继承多个基类对象。可以使用类限定符来解决名称二义性的问题,使用虚基类来避免继承多个基类对象的问题。但使用虚基类后,就需要为编写构造函数初始化列表以及解决二义性问题引入新的规则。
类模板是的能够创建通用的类设计,其中类型(通常是成员类型)有类型参数表示。典型的模板如下:
template <class T>
class Ic
{
private:
T v;
...
public:
Ic(const T& val) : v(val) {}
...
};
其中,T是类型参数,用作以后将指定的实际类型的占位符。
类定义(实例化)在声明对象并指定特定类型时生成。
class Ic<short> sic; // implicit instantiation
Ic<short>成为模板具体化。具体地说,这是一个隐式实例化。
使用关键字template声明类的特定具体化时,将发生显式实例化:
template class Ic<short>; // explicit instantiation
显式具体化--覆盖模板定义的具体类声明。方法是以template<>打头,然后模板类名称,在加上尖括号(其中包含要具体化的类型)。例子:
template <> class Ic<char*> {...};
类模板可以指定多个泛型,也可以有非类型参数:
template <class T, int n>
class Ic {...};
类模板还可以包含本身就是模板的参数:
template <template<class T1> class T2, class T2>
class Tro {...};
模板也可以部分具体化:
template <class T> Pals<T, T, 10> {...};
模板类可用作其他类、结构和模板的成员。
所有的这些机制的目的都是为了让程序员能够重用经过测试的代码,而不是手工的复制它们。这样可以简化编程工作,提供程序的可靠性。