第十四章 C++中的代码重用
引言
C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。
本章将介绍其他方法,其中之一是使用这样的类成员:本身是另一个类的对象。这种方法称为包含,组合或层次化。另一种方法是使用私有或保护继承。
通常,包含,私有继承和保护继承用于实现has a关系,即新的类将包含另一个类的对象。多重继承使得能够使用两个或更多的基类派生出新的类,将新基类的功能组合在一起。
14.1 valarray类简介
- valarray类是由头文件varlarray支持的。用于处理数值,支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值操作。
- valarray被定义为一个模板类,以便能够处理不同的数据类型。
- 模板特性意味着声明对象,必须指定具体的数据类型。因此,使用valarray类来声明一个对象时,需要在标识符valarray后面加上一对尖括号,并在其中包含所需的数据类型。
valarray<int> q_values;// int数组
valarray<double> weights;//double数组
类特性意味着要使用valarray对象,需要了解这个类的构造函数和其它用法。下面是几个使用其构造函数的例子:
double gpa[5]={3.1,3.5,3.8,2.9,3.3};
valarray<double>v1;
valarray<int> v2(8);
valarray<int> v3(10,8);//8个int数组;每个设置为10
valarray<double>v4(gpa,4);
//由4个元素组成的数组
//初始化为gpa的前4个元素
由上面可知,可以创建长度为0的空数组,指定长度的空数组,所有元素被初始化为指定值的数组,用常规数组中的值进行初始化的数组。在c++11中,也可使用初始化列表:
valarray<int> v5={20,32,17,9};//c++11
下面是这个类的一些方法:
-
operator[]()
:让您能够访问各个元素。 -
size()
:返回包含的元素数。 -
sum()
:返回所有元素的总和 -
max()
:返回最大的元素 -
min()
:返回最小的元素
14.1.1 接口和实现
使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a(public继承)关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a(protetced/private继承)关系的组成部分。
14.1.2 C++和约束
C++包含让程序员能够限制程序结构的特性——使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
14.1.3 初始化顺序
当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。
14.2 has a
14.2.1 组合
使用组合实现has a关系:假设需要设计一个student类:如果使用组合的方式则需要使用一个string对象来表示姓名,使用一个valarray来表示考试分数。
class Student
{
private:
string name; // use a string object for name
valarray<double> scores; // use a valarray<double> object for scores
...
};
14.2.2 私有继承
除了组合,C++还有另一种实现has-a关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
使用公有继承,基类的公有方法将成为派生类的公有方法,派生类将继承基类的接口;这是is-a关系的一部分;
使用私有继承,基类的公有方法将成为派生类的私有方法,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。
因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系
14.2.1 私有继承的使用
要进行私有继承,请使用关键字private而不是public来定义类(实际上,private是默认值,因此省略访问限定符也将导致私有继承)。使用多个基类的继承被称为多重继承(multiple inheritance,MI)。
class Student : private std::string,private std::valarray<double>
{
public:...
}
组合版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个主要区别。
对于组合:
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {} // use object names for containment
对于继承:
Student(const char * str, const double * pd, int n)
: std::string(str), ArrayDb(pd, n) {} // use class names for inheritance
在这里,ArrayDb是std::valarray的别名。成员初始化列表使用std::string(str),而不是name(str)。这是包含和私有继承之间的第二个主要区别。
与组合版本唯一不同的地方是,省略了显式对象名称,并在内联构造函数中使用了类名,而不是成员名。
// 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
14.2.2.1 访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。这时可以对该方法进行一个包装。
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;
}
14.2.2.2 访问基类对象
使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢?例如,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对象。
14.2.2.3 访问基类的友元函数
用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &) stu << ":n";
...
}
用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:
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 &)。
引用stu不会自动转换为string引用。根本原因在于,在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。
然而,即使这个例子使用的是公有继承,也必须使用显式类型转换。原因之一是,如果不使用类型转换,下述代码将与友元函数原型匹配,从而导致递归调用:
os << stu;
14.2.3 使用组合还是私有继承?
由于既可以使用包含,也可以使用私有继承来建立has a关系,那么应该使用哪种方式呢?
首先,大多数程序员倾向于使用包含,因为它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。
其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。
总之,使用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要3个string对象,可以使用包含声明3个独立的string成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。
另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
因此通常情况下,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
14.3 保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。
- 使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;
- 使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
14.4 如何访问基类的私有方法?
我们知道使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。那么,假设要让基类的方法在派生类外面可用,有什么方法呢?
**方法一:**定义一个使用该基类方法的派生类方法;
double Student::sum() const // public Student method
{
return std::valarray<double>::sum(); // use privately-inherited method
}
**方法二:**将函数调用包装在另一个函数调用中,使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。
class Student : private std::string, private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
};
注意,using声明只使用成员名——没有圆括号、函数特征标和返回类型。using声明只适用于继承,而不适用于包含。
14.5 多重继承
MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系。例如,可以从Waiter类和Singer类派生出SingingWaiter类:
class SingingWaiter : public Waiter, public Singer {...};
请注意,必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生:
class SingingWaiter : public Waiter, Singer {...}; // Singer is a private base
正如本章前面讨论的,私有MI和保护MI可以表示has-a关系。
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
// must implement virtual destructor, even if pure
Worker::~Worker() {}
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: waitern";
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: singern";
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;
}
这种设计看起来是可行的:使用Waiter指针来调用Waiter::Show()和Waiter::Set();使用Singer指针来调用Singer::Show()和Singer::Set()。然后,如果添加一个从Singer和Waiter类派生出的SingingWaiter类后,将带来一些问题。
具体地说,将出现以下问题:
- 有多少Worker?
- 哪个方法?
有多少worker?
class SingingWaiter: public Singer, public Waiter {...};
因为Singer和Waiter都继承了一个Worker组件,因此SingingWaiter将包含两个Worker组件(参见图14.4)。
正如预期的,这将引起问题。例如,通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:
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成为可能。
14.5.1 虚基类
14.5.1.1 虚基类介绍
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字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对象副本(参见图14.5)。因为SingingWaiter现在只包含了一个Worker子对象,所以可以使用多态。
您可能会有这样的疑问:
- 为什么使用术语“虚”?
- 为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为多MI的准则呢?
- 是否存在麻烦呢?
首先,为什么使用术语虚?毕竟,在虚函数和虚基类之间并不存在明显的联系。C++用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。
其次,为什么不抛弃将基类声明为虚的这种方式,而使虚行为成为MI的准则呢?第一,在一些情况下,可能需要基类的多个拷贝;第二,将基类作为虚的要求程序完成额外的计算,为不需要的工具付出代价是不应当的;第三,这样做有其缺点,将在下一段介绍。
最后,是否存在麻烦?是的。为使虚基类能够工作,需要对C++规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码。例如,将SingingWaiter类添加到Worker集成层次中时,需要在Singer和Waiter类中添加关键字virtual。
14.5.1.2 新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数即是基类构造函数。但这些构造函数可能需要将信息传递给其基类。例如,可能有下面一组构造函数:
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) {} // flawed
存在的问题是,自动传递信息时,将通过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 &)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
14.5.1.3 调用哪个方法
除了修改类构造函数规则外,MI通常还要求调整其他代码。假设要在SingingWaiter类中扩展Show( )方法。因为SingingWaiter对象没有新的数据成员,所以可能会认为它只需使用继承的方法即可。这引出了第一个问题。假设没有在SingingWaiter类中重新定义Show( )方法,并试图使用SingingWaiter对象调用继承的Show( )方法,这将引起二义性。
对于单继承,如果没有重新定义Show( ),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个Show( )函数,因此这使得上述调用是二义性的。
**警告:**多重继承可能导致函数调用的二义性。例如,BadDude类可能从Gunslinger类和PokerPlayer类
那里继承两个完全不同的Draw( )方法。
可以使用作用域解析运算符来澄清编程者的意图:
SingingWaiter newhire("Elise Hawks",2005,6,soprano);
newhire.Singer::Show();//use Singer version
然而,更好的方法是在SingingWaiter中重新定义Show( ),并指出要使用哪个Show( )。例如,如果希望SingingWaiter对象使用Singer版本的Show( ),则可以这样做:
void SingingWaiter::Show()
{
Singer::Show();
}
对于单继承来说,让派生方法调用基类的方法是可以的。例如,假设HeadWaiter类是从Waiter类派生而来的,则可以使用下面的定义序列,其中每个派生类使用其基类显示信息,并添加自己的信息:
void Worker::Show()const
{
cout<<"Name: "<<fullname<<endl;
cout<<"Employee ID:"<<id<<endl;
}
void Waiter::Show() const
{
Worker::show();
cout<<"Panache rating: "<<panache<<"\n";
}
void HeadWaiter::Show() const
{
Worker::show();
cout<<"Presence rating: "<<presence<<"\n";
}
然而,这种递增的方式对SingingWaiter示例无效。下面的方法将无效,因为它忽略了Waiter组件:
void SingingWaiter::Show()
{
Singer::Show();
Waiter::Show();
}
然而,这将显示姓名和ID两次,因为Singer::Show()和Waiter::Show()都调用了Worker::Show()。
14.5.1.4 如何解决这种现象
一种办法是使用模块化方式,而不是递增方式,即提供一个只显示Worker组件的方法和一个只显示Waiter组件或Singer组件(而不是Waiter和Worker组件)的方法。然后,在SingingWaiter::Show()方法中将组件组合起来。采用这种方式,对象仍可使用Show( )方法。而Data( )方法只在类内部可用,作为协助公有接口的辅助方法。
另一种办法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。
在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写它们。
while(strchr("wstq",choice) == NULL)
该函数返回参数choice指定的字符在字符串“wstq”中第一次出现的地址,如果没有这样的字符,则返回NULL指针。使用这种检测比使用if语句将choice指定的字符同每个字符进行比较简单。
14.5.2 有关多重继承的其他问题
14.5.2.1 混合使用虚基类和非虚基类
再来看一下通过多种途径继承一个基类的派生类的情况。如果基类是虚基类,派生类将包含基类的一个子对象;如果基类不是虚基类,派生类将包含多个子对象。当虚基类和非虚基类混合时,情况将如何呢?
例如,假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类,而类M是从C、D、X和Y派生而来的。在这种情况下,类M从虚派生祖先(即类C和D)那里共继承了一个B类子对象,并从每一个非虚派生祖先(即类XY)分别继承了一个B类子对象。因此,它包含三个B类子对象。当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
14.5.2.2 虚基类和支配
使用虚基类将改变C++解析二义性的方式。使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于(dominates)其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。
那么,一个成员名如何优先于另一个成员名呢?派生类中的名称优先于直接或间接祖先类中的相同名称。虚二义性规则与访问规则(public,private,protected)无关。
14.5.2 MI小结
首先复习一下不使用虚基类的MI。这种形式的MI不会引入新的规则。然而,如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。即在从GunSlinger和PokerPlayer派生而来的BadDude类中,将分别使用Gunslinger::draw( )和PokerPlayer::draw( )来区分从这两个类那里继承的draw( )方法。否则,编译器将指出二义性。
如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分别继承非虚基类的一个实例。在某些情况下,这可能正是所希望的,但通常情况下,多个基类实例都是问题。
接下来看一看使用虚基类的MI。当派生类使用关键字virtual来指示派生时,基类就成为虚基类:
class marketing : public virtual reality{...}
主要变化(同时也是使用虚基类的原因)是,从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其他要求:
- 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
- 通过优先规则解决名称二义性。
正如您看到的,MI会增加编程的复杂程度。然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的。避免这种情况后,唯一需要注意的是,在必要时对继承的名称进行限定。
14.6 类模板
C++的类模板为生成通用的类声明提供了一种更好的方法。模板提供参数化类型,能够将类型名作为参数传递给接收方来建立类或函数。
**容器类:**容器类设计用来存储其他对象或数据类型。
和模板函数一样,模板类以下面这样的代码开头:
template <typename Type>
关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字typename看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
当模板被调用时,Type将被具体的类型值取代,执行文本替换。此时类限定符从Stack::改为Stack<Type>::
。
如果成员函数是在类中定义的,则正常写就行,可以省略模板前缀和类型限定符。而如果是在类外定义的,则需加上一些限定符:
template<typename T>
bool stack<Type>::push(const Type& item)
{
}
类模板和函数模板并不是具体的类和成员函数,它们是C++编译器指令,是类的蓝图,说明了如何生成类和成员函数定义。而模板的具体实现被称为实例化或具体化。
**不能将模板成员函数放在独立的实现文件中。**由于模板不是函数,它们不能单独编译,模板必须与特定的模板实例化请求一起使用,为此,最简单的方法是将所有模板信息全都放在同一个头文件中,并在要使用这些模板的时候引入相应的头文件。
14.6.1 使用模板类
仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。
Stack<int> kernels;
注意,必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数。
14.6.2 数组模板示例和非类型参数
模板常用作容器类,这是因为类型参数的概念非常适合于将相同的存储方案用于不同的类型。下面将探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族。
首先介绍一个允许指定数组大小的简单数组模板:
- 一种方法是在类中使用动态数组和构造函数参数来提供元素数目。
- 另一种方法是使用模板参数(表达式参数)来提供常规数组的大小,C++11新增的模板array就是这样做的。
- 表达式参数可以是整型、枚举、引用或指针。 如double m是不合法的,但double* rm是合法的。
- 模板代码不能修改参数的值,也不能使用参数的地址。 如n++, &n
- 实例化模板时,用作表达式参数的值必须是常量表达式。
template <class T, int n> // T为类型参数,n为int类型(非类型/表达式参数)
class ArrayTP
{
...
};
// 定义一个名为ArrayTP<double, 12>的类,并创建一个类型为ArrayTP<double, 12>的eggweight对象。
ArrayTP<double, 12> eggweights;
构造函数方法和表达式参数方法的区别:
- 构造函数方法使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。
- 表达式参数的优点:执行速度将更快,尤其是在使用了很多小型数组时。
- 表达式参数的缺点:每种数组大小都将生成自己的模板。
- 构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
// 生成两个独立的类声明
ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> donuts;
// 只生成一个类声明,并将数组大小信息传递给类的构造函数
Stack<int> eggs(12);
Stack<int> dunkers(13);
14.6.3 模板多功能性
模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。
template <typename T>
class Array
{
private:
T entry;
...
};
template <typename Type>
class GrowArray : public Array<Type> {...}; // inheritance
template <typename Tp>
class Stack
{
Array<Tp> ar; // use an Array<> as a component
...
};
...
Array < Stack<int> > asi; // an array of stacks of int
// 上面一行代码C++98要求>>之间有空格,C++11不要求
14.6.3.1 递归使用模板
在模板语法中,维的顺序与等价的二维数组相反。
// 以下两条命令等价
ArrayTP< ArrayTP<int,5>, 10> twodee;
int twodee[10][5];
twod.cpp
// twod.cpp -- making a 2-d array
#include <iostream>
#include "arraytp.h"
int main(void)
{
using std::cout;
using std::endl;
ArrayTP<int, 10> sums;
ArrayTP<double, 10> aves;
ArrayTP< ArrayTP<int,5>, 10> twodee;
int i, j;
for (i = 0; i < 10; i++)
{
sums[i] = 0;
for (j = 0; j < 5; j++)
{
twodee[i][j] = (i + 1) * (j + 1);
sums[i] += twodee[i][j];
}
aves[i] = (double) sums[i] / 10;
}
for (i = 0; i < 10; i++)
{
for (j = 0; j < 5; j++)
{
cout.width(2);
cout << twodee[i][j] << ' ';
}
cout << ": sum = ";
cout.width(3);
cout << sums[i] << ", average = " << aves[i] << endl;
}
cout << "Done.\n";
// std::cin.get();
return 0;
}
out:
1 2 3 4 5 : sum = 15, average = 1.5
2 4 6 8 10 : sum = 30, average = 3
3 6 9 12 15 : sum = 45, average = 4.5
4 8 12 16 20 : sum = 60, average = 6
5 10 15 20 25 : sum = 75, average = 7.5
6 12 18 24 30 : sum = 90, average = 9
7 14 21 28 35 : sum = 105, average = 10.5
8 16 24 32 40 : sum = 120, average = 12
9 18 27 36 45 : sum = 135, average = 13.5
10 20 30 40 50 : sum = 150, average = 15
Done.
14.6.3.2 使用多个类型参数
模板可以包含多个参数类型。假设希望类可以保存两种值,则可以创建并使用Pair模板来保存两个不同的值(标准模板库提供了类似的模板,名为pair)。
// 方法first( ) const和second( ) const报告存储的值,
// 由于这两个方法返回Pair数据成员的引用,因此让您能够通过赋值重新设置存储的值。
// pairs.cpp -- defining and using a Pair template
#include <iostream>
#include <string>
template <class T1, class T2>
class Pair
{
private:
T1 a;
T2 b;
public:
T1 & first();
T2 & second();
T1 first() const { return a; }
T2 second() const { return b; }
Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval) { }
Pair() {}
};
template<class T1, class T2>
T1 & Pair<T1,T2>::first()
{
return a;
}
template<class T1, class T2>
T2 & Pair<T1,T2>::second()
{
return b;
}
int main()
{
using std::cout;
using std::endl;
using std::string;
Pair<string, int> ratings[4] =
{
Pair<string, int>("The Purpled Duck", 5),
Pair<string, int>("Jaquie's Frisco Al Fresco", 4),
Pair<string, int>("Cafe Souffle", 5),
Pair<string, int>("Bertie's Eats", 3)
};
int joints = sizeof(ratings) / sizeof (Pair<string, int>);
cout << "Rating:\t Eatery\n";
for (int i = 0; i < joints; i++)
cout << ratings[i].second() << ":\t "
<< ratings[i].first() << endl;
cout << "Oops! Revised rating:\n";
ratings[3].first() = "Bertie's Fab Eats";
ratings[3].second() = 6;
cout << ratings[3].second() << ":\t "
<< ratings[3].first() << endl;
// std::cin.get();
return 0;
}
out:
Rating: Eatery
5: The Purpled Duck
4: Jaquie's Frisco Al Fresco
5: Cafe Souffle
3: Bertie's Eats
Oops! Revised rating:
6: Bertie's Fab Eats
14.6.3.3 默认类型模板参数
template <class T1, class T2 = int> class Topo {...};
Topo<double, double> m1; // T1 is double, T2 is double
Topo<double> m2; // T1 is double, T2 is int
14.6.4 模板的具体化
类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化(specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
14.6.4.1 隐式实例化(用的时候再准备)
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。
编译器在需要对象之前,不会生成类的隐式实例化:
ArrayTP<int, 100> stuff; // 隐式实例化
ArrayTP<double, 30> * pt; // 此时还没有使用创建对象,没有分配内存,只是创建了一个指针指向这样的对象
pt = new ArrayTP<double, 30>; // 该语句导致编译器生成类定义,并根据该定义创建一个对象。
14.6.4.2 显式实例化(没用的时候就准备)
template class ArrayTP<string, 100>; _// generate ArrayTP<string, 100> class_
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中。虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。更快速,省去运行阶段创建这个类的时间。
14.6.4.3 显示具体化(全部特殊处理)
template <> class Classname<specialized-type-name> { ... };
已经有了类模板的定义,对其中的一些进行修改,重新进行类的实现。有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以提供一个显式模板具体化。当具体化模板和通用模板都与实例化请求匹配时,编译器将优先使用具体化版本。
举例:
假设已经为用于表示排序后数组的类(元素在加入时被排序)定义了一个模板:假设已经为用于表示排序后数组的类(元素在加入时被排序)定义了一个模板:
template<typename T>
class SortedArray
{
}
假设模板使用>运算符来对值进行比较。对于数字,这管用;如果T表示一种类,则只要定义了T::operator>( )方法,这也管用;但如果T是由const char *表示的字符串,这将不管用。
在这种时候,可以提供一个显式模板具体化,这将采用为具体类型定义的模板,而不是为泛型定义的模板。
template<> class SortedArray<const char *>
{
}
其中的实现代码将使用strcmp( )(而不是>)来比较数组值。现在,当请求const char *类型的SortedArray模板时,编译器将使用上述专用的定义,而不是通用的模板定义:
SortedArray<int> scores;
SortedArray<const char*> dates;
14.6.4.4 部分具体化(部分特殊处理)
C++还允许部分具体化(partial specialization),即部分限制模板的通用性。例如,部分具体化可以给类型参数之一指定具体的类型:
//general template
template<class T1,class T2> class Pair{...}
//具体化
template<class T1> class Pair<T1,int>{...}//如果这里指定所有类型,<>就为空,就成为了显式具体化。
template<> class Pair<int,int>{...}
//如果有多个模板可供选择,编译器将使用具体化程度最高的模板。给定上述三个模板,情况如下:
Pair<double,double> p1;//通用
Pair<double,int> p2;//部分具体化
Pair<int,int>p3;//显式具体化
关键字template后面的<>声明的是没有被具体化的类型参数。
部分具体化特性使得能够设置各种限制。例如:
//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 Trio<T1,T1*,T1*>{...};
//给出上述声明,编译器将作出以下选择:
Trio<int,short,char*> t1;//use general template
Trio<int,short> t2;//use Trio<T1,T2,T2>
Trio<char,char*,char*> t3;//use Trio<T1,T1*.T1*>
示例:
#include <iostream>
using namespace std;
template<class T1, class T2>
class A
{
public:
void show();
};
template<class T1, class T2>
void A<T1, T2>::show()
{
cout << "this is a general definition." << endl;
}
// 显式实例化,A<double, double>这个类在内存中已经存在
template class A<double, double>;
// 显示具体化,不再使用通用的方法,所有类型都指定为某一特定的类型
template<>
class A<int, int>
{
public:
void show();
};
// template<> 不用这一行语句
void A<int, int>::show()
{
cout << "explicit specialization." << endl;
}
// 部分具体化
template<class T1> // <>中放没有具体化的,T1泛型,T2指定为某种特殊的类型
class A<T1, int>
{
public:
void show();
};
template<class T1> // 注意这里
void A<T1, int>::show()
{
cout << "partial specialization." << endl;
}
int main(void)
{
A<char, char> a1; // 隐式实例化
a1.show();
A<int, int> a2; // 显示具体化过
a2.show();
A<double, int> a3; // 调用部分具体化
a3.show();
return 0;
}
成员模板
模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。程序清单14.20是一个简短的模板类示例,该模板类将另一个模板类和模板函数作为其成员。
// 模板类和模板函数做成员变量
#include <iostream>
using namespace std;
template <class T>
class beta
{
private:
template <class V> // beta模板类里面的成员变量是一个hold模板类
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; // 基于int类型的hold对象
public:
beta(T t, int i) : q(t), n(i) {}
void Show() const
{
q.show();
n.show();
}
template <class U> // 定义模板函数
U blab(U u, T t)
{
return (q.Value() + n.Value()) * u / t;
}
// 注意q.Value()后面加括号,因为调用的是一个函数。
};
int main(void)
{
beta<double> guy(1.2, 3);
guy.Show();
cout << guy.blab(10, 6.1) << endl; // (f+int)*int/f,返回int类型
cout << guy.blab(10.0, 6.1) << endl; // (f+int)*f/f,返回f类型
return 0;
}
可以在beta模板中声明hold类和blah方法,并在beta模板的外面定义它们。那么如何在类外定义它们?
template<typename T>
template<typename V>
class beta<T>::hold
{
private:
V val;
public:
hold(V v=0):val(v){}
void show()const{std::cout<<val<<std::endl;}
V Value()const{return val;}
}
上述定义将T、V和U用作模板参数。因为模板是嵌套的,因此必须使用下面的语法:
template<typename T>
template<typename V>
而不能用下面的语法:
template<typename T,typename V>
定义还必须指出hold和blab是beta类的成员,这是通过使用作用域解析运算符来完成的。
14.6.5 将模板用作参数
模板可以包含类型参数(如typename T)和非类型参数(int)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL.
template<typename T>//按照我们之前的理解,这里的typename可以理解成类型,T理解成参数;
template<template<typename T> class Thing>//这里可以套用上面的理解:template<typename T> class是类型,Thing是参数,但是回过头来看看类型:template<typename T> class这本身就是个模板类。
// 模板类做参数
#include <iostream>
#include "stacktp.h"
using namespace std;
template <template <class T> class Thing> // 参数Thing是一个模板类型
class Crab
{
private:
Thing<int> s1; // Ting<int>类的对象
Thing<double> s2; // Ting<double>类的对象
public:
Crab() {}
bool push(int a, double x)
{
return s1.push(a) && s2.push(x);
}
bool pop(int& a, double& x)
{
return s1.pop(a) && s2.pop(x);
}
};
int main(void)
{
Crab<Stack> nebula; // 包含两个对象Stack<int>和Stack<double>,包含两个栈
int ni;
double nb;
cout << "Enter int double pairs, such as 4 3.5 (0 to end):\n";
while (cin >> ni && ni > 0 && cin >> nb && nb > 0)
{
if (!nebula.push(ni, nb))
break;
}
while (nebula.pop(ni, nb))
cout << ni << ", " << nb << endl;
cout << "Done.\n";
return 0;
}
也可以混合使用模板参数和常规参数:
template<template<typename T> class Thing,typename U,typename V>
class Crab{
private:
Thing<U> s1;
Thing<V> s2;
}
//现在,成员s1和s2可存储的数据类型为泛型,而不是用硬编码指定的类型。
Crab<Stack,int,double> nebula;
//模板参数T表示一种模板类型,而类型参数U,V表示非模板类型。
14.6.6 模板类和友元
模板类声明也可以有友元。模板的友元分3类:
- 非模板友元;
- 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型;
- 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
14.6.6.1 模板类的非模板友元函数
在模板类中将一个常规函数声明为友元:
template<class T>
class HasPriend
{
public:
friend void counts(HasPriend<T>&);
};
上述声明使counts( )函数成为模板所有实例化的友元。例如,它将是类hasFriend<int>和HasFriend<string>的友元。在这里友元函数counts本身并不是一个模板函数,只是它接受了一个模板类型作为入参,如果想要使用,还必须显式具体化这个函数,定义出对应版本的函数。
14.6.6.2 模板类的约束模板友元函数
可以修改前一个示例,使得友元函数本身成为模板。
- 首先,在类定义的前面声明每个模板函数:
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> &);
};
声明中的<>指出这是模板具体化。对于report( ),<>可以为空,因为可以从函数参数推断出如下模板类型参数:
HasFriendT<TT>
然而,也可以用:
report<HasFriendT<TT>>(HasFriendT<TT> &);
但counts( )函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化。还需要注意的是,TT是HasFriendT类的参数类型。
- 程序必须满足的第三个要求是,为友元提供模板定义;
14.6.6.3 模板类的非约束模板友元函数
前一节中的约束模板友元函数是在类外面声明的模板的具体化。int类具体化获得int函数具体化,依此类推。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
template<typename T>
class ManyFriend
{
template <typename C,typename D> friend void show2(C &,D&);
};
这是一个使用非约束友元的例子。其中,函数调用show2(hfi1,hfi2)与下面的具体化匹配:
void show2<ManyFriend<int>&,ManyFriend<int>&>(ManyFriend<int>&c,ManyFriend<int>& d);
14.6.7 模板别名(C++17)
在谈这一小节之前,你可能会想到用以下方式去定义:
typedef std::array<double,12> arrd;
typedef std::array<int,12> arri;
typedef std::array<std::string,12> arrs;
怎么说呢?对,但是麻烦,C++11提供了新功能,可以使用模板定义一系列别名:
template<typename T>
using arrtype = std::array<T,12>
arrtype<double> gallons;
arrtype<int> days;
arrtype<std::string> months;
可以用arrtype<T>表示类型std::array<T, 12>。
C++11允许将语法using =用于非模板。用于非模板时,这种语法与常规typedef等价:
typedef const char* pc;
using pc = const char *;
习惯这种语法后,您可能发现其可读性更强,因为它让类型名和类型信息更清晰。
总结
C++提供了几种重用代码的手段。第13章介绍的公有继承能够建立is-a关系,这样派生类可以重用基类的代码。私有继承和保护继承也使得能够重用基类的代码,但建立的是has-a关系。使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。无论使用哪种继承,基类的公有接口都将成为派生类的内部接口。这有时候被称为继承实现,但并不继承接口,因为派生类对象不能显式地使用基类的接口。因此,不能将派生对象看作是一种基类对象。由于这个原因,在不进行显式类型转换的情况下,基类指针或引用将不能指向派生类对象。
还可以通过开发包含对象成员的类来重用类代码。这种方法被称为包含、层次化或组合,它建立的也是has-a关系。与私有继承和保护继承相比,包含更容易实现和使用,所以通常优先采用这种方式。然而,私有继承和保护继承比包含有一些不同的功能。例如,继承允许派生类访问基类的保护成员;还允许派生类重新定义从基类那里继承的虚函数。因为包含不是继承,所以通过包含来重用类代码时,不能使用这些功能。另一方面,如果需要使用某个类的几个对象,则用包含更适合。例如,State类可以包含一组County对象。
多重继承(MI)使得能够在类设计中重用多个类的代码。私有MI或保护MI建立has-a关系,而公有MI建立is-a关系。MI会带来一些问题,即多次定义同一个名称,继承多个基类对象。可以使用类限定符来解决名称二义性的问题,使用虚基类来避免继承多个基类对象的问题。但使用虚基类后,就需要为编写构造函数初始化列表以及解决二义性问题引入新的规则。
类模板使得能够创建通用的类设计,其中类型(通常是成员类型)由类型参数表示。典型的模板如下:
template<class T>
class Ic
{
T V;
public:
Ic(const T& val):v(val){}
...
}
其中,T是类型参数,用作以后将指定的实际类型的占位符(这个参数可以是任意有效的C++名称,但通常使用T和Type)。在这种环境下,也可以使用typename代替class。
使用关键字template声明类的特定具体化时,将发生显式实例化:
template class Ic<int>;
在这种情况下,编译器将使用通用模板生成一个int具体化——Ic<int>,虽然尚未请求这个类的对象。
可以提供显式具体化——覆盖模板定义的具体类声明。方法是以template<>打头,然后是模板类名称,再加上尖括号(其中包含要具体化的类型)。文章来源:https://www.toymoban.com/news/detail-496908.html
template<>class Ic<char *>
{
}
//这样,下面这样的声明将为chic使用专用定义,而不是通用模板:
class Ic<char*> chic;
所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。文章来源地址https://www.toymoban.com/news/detail-496908.html
到了这里,关于【C++】第14章: C++中的代码重用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!