第14章 C++中的代码重用
1.包含对象成员的类
包含其他类对象的类,即has-a关系。用于建立has-a关系的C++技术是组合(包含):
class Student
{
private:
typedef std::valarray<double> ArrayDb;
std::string name;
ArrayDb scores;
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(cconst 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;
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, const Student & stu);
};
使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
2.私有继承
C++还有另一种实现has-a关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。使用术语子对象来表示通过继承或包含添加的对象。
要进行私有继承,将使用关键字private而不是public来定义类(实际上,private是默认值,因此省略访问限定符也将导致私有继承):
class Student : private std::string, private std::valarray<double>
{
public:
//...
};
使用多个基类的继承被称为多重继承。
包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。
隐式地继承组件而不是成员对象将影响代码的编写,因为不能使用name和scores来描述对象。新版本的构造函数将使用雷鸣而不是成员名来标识构造函数:
Student(const char * str, const double double * pd, int n) : std::string(str), ArrayDb(pd, n){}
在私有继承中使用类名和作用域解析运算符来调用基类的方法:
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;
}
在私有继承中,如果要使用基类的友元函数,可以通过显式地转换为基类来调用正确的函数:
ostream & operator<<(ostream & os, const Student & stu)
{
os << “Scores for ” << (const string &) stu << “:\n”;
//...
}
引用stu不会自动转换为string引用。根本原因在于,在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。
包含和私有继承都可以建立has-a关系。大多数C++程序员倾向于使用包含。这是由于包含有以下优点:
- 包含易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。
- 继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。而使用包含不太可能遇到这样的问题。
- 包含能够包括多个同类的子对象。而继承则只能使用一个这样的对象。
然而,私有继承所提供的特性确实比包含多:
- 如果使用包含将含有保护成员的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。
- 通过继承得到的派生类可以重新定义虚函数,但包含类不能。
总之,通常情况下应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
3.保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:
class Student : protected std::string, protected std::valarray<double>
{
//...
};
使用保护继承时,基类的公有成员和保护成员都将称为派生类的保护成员。使用私有继承和保护继承的区别在于,当从派生类派生出另一个类时,私有继承的第三代派生类将不能使用基类的接口。而保护继承的派生类可以使用它们。
表14.1总结了公有、私有和保护继承的特征。
特征 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有成员 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
4.用using重新定义访问权限
如果想要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可用使用特定的基类成员,即使采用的是私有派生:
class Student : private std::string, private std::valarry<double>
{
//...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
//...
};
上述using声明使得valarray::min()和valarray::max()可用,就像它们是Student的公有方法一样。
注意,using声明只使用成员名,而没有圆括号、函数特征标和返回类型。
5.多重继承(MI)
多重继承可能会给程序员带来很多新问题。下面是一个例子。
class Worker
{
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;
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), panachar(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];
int voice;
public:
Singer() : Worker(), void(othetr) {}
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;
};
现在,从Singer和Waiter公有派生出SingingWaiter:
class SingingWaiter : public Singer, public Waiter {...};
这将产生第一个问题:由于Singer和Waiter都继承了Worker,因此SingingWaiter将包含两个Worker组件。
首先,通常可以将派生类对象的地址赋给基类指针,但现在将出现二义性:
SingingWaiter ed;
Worker * pw = &ed;
ed中包含两个Worker对象,有两个地址可供选择,有二义性。应该使用类型转换来指定对象:
Worker * pw1 = (Waiter *) &ed;
Worker * pw2 = (Singer *) &ed;
其次,SingingWorker中应该只需要一个fullname和id,但是现在有两个。为解决这种问题,C++引入了一种新技术——虚基类。
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。通过在类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关紧要):
class Singer : virtual public Worker {...};
class Waiter : public virtual Worker {...};
使用虚基类,需要对类构造函数采用一种新的方法。在下面这个构造函数中:
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Waiter(wk, p), Singer(wk, v) {}
在自动传递信息时,将通过两条不同的途径(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) {}
在非虚基类的情况下,这种调用是非法的。但对于虚基类,是合法的,且必须这样做。
所以,如果类有间接虚基类,则除非只需要使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
第二个问题是,下面的调用将产生二义性:
SingingWaiter newhire(“Elise Hawks”, 2005, 6, soprano);
newhire.Show();
一种办法是使用作用域解析运算符来解决这个问题:
newhire.Singer::Show();
更好的办法是在SingingWaiter中重新定义Show(),并指出要使用哪个Show():
void SingingWaiter::Show()
{
Singer::Show();
Waiter::Show();
}
但是这会显示姓名和ID两次,因为Singer::Show()和Waiter::Show()都调用了Worker::Show()。
一种办法是,使用模块化方式,即提供一个只显式Worker组件的方法和一个只显式Waiter组件或Singer组件的保护方法。然后,在SingingWaiter::Show()方法中将组件组合起来。
另一种办法是将所有的数据组件都设置为保护的,而不是私有的。
第三个问题是,混合使用虚基类和非虚基类时,派生类中基类子对象的个数。
当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
第四个问题,虚基类和支配。
使用虚基类将改变C++解析二义性的方式。使用非虚基类时,如果类从不同的类继承了两个或更多的同名成员,则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于其他所有名称,则使用它时,即使不适用限定符,也不会导致二义性。
派生类的名称优先于直接或简介祖先类中的相同名称。
6.类模板
定义类模板时,模板类以下面的代码开头:
template <class Type>
或
template <typename Type>
可以使用自己的泛型名代替Type,其命名规则与其他标识符相同。当前流行的包括T和Type。
除此之外,模板类和普通类的其他区别是在定义类外定义成员函数时,每个函数头都将以相同的模板声明打头,并在限定符后加上<Type>。例如:
templete <class Type>
bool Stack<Type>::push(const Type & item)
{
//...
}
仅在程序中包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名:
Stack<int> kernels;
Stack<string> colonels;
泛型标识符称为类型参数,这意味着它们类似于变量,但是赋给它们的不能是数字,而只能是变量。
切忌盲目使用模板。如使用指针栈时会带来一些问题。正确使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。
模板不是类和成员函数定义!不能将模板成员函数放在独立的实现文件中。为此,最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
返回类对象的成员函数在声明时的返回对象可以用简写,即只有类名。但是在类的外面定义时必须使用完整的方式:
template <typename Type>
class Stack
{
...
public:
Stack & operator=(const Stack & st);
};
template <typename Type>
Stack<Type> & Stack<Type>::operator=(const Stack<Type> & st) {...}
假设需要实现一个可以指定数组大小的模板类。一种方法是在类中使用动态数组和构造函数参数来提供元素数目。另一种方法是使用模板参数来提供常规数组的大小:
template <typename T, int n>
上述模板头中的int指出n的类型为int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型或表达式参数。
表达式参数有下列限制:
- 表达式参数只可以是整型、枚举、引用或指针。
- 模板代码不能修改参数的值,也不能使用参数的地址。
- 实例化模板时,用作表达式参数的值必须是常量表达式。
表达式参数方法与构造函数方法相比,有一个优点。构造函数方法使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈。这样,执行速度将更快,尤其是在使用了很多小型数组时。
表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板:
ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> donuts;
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
Stack<int> eggs(12);
Stack<int> dunkers(13);
另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
可以递归使用模板类:
ArrayTP< ArrayTP<int, 5>, 10> twodee;
这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。
模板类可以使用多个类型参数:
template <typename T1, typename T2>
class Pair
{
//...
};
模板类可以为类型参数提供默认值:
template <typename T1, typename T2 = int> class Topo {...};
类模板与函数模板很相似,因为可以有隐式实例化、显式实例化和显式具体化,它们统称为具体化。
隐式实例化声明一个或多个对象,指出所需类型,而编译器使用通用模板提供的处方生成具体的类定义:
ArrayTP<double, 30> stuff;
编译器在需要对象之前,不会生成类的隐式实例化:
ArrayTP<double, 30> * pt;
pt = new ArrayTP<double, 30>;
第二条语句导致编译器进行隐式实例化。
当使用关键字并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中:
template class ArrayTP<string, 100>;
显式具体化是特定类型(用于替换模板中的泛型)的定义。当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。
具体化类模板定义的格式如下:
template <> class Classname<specialized-type-name> {...};
C++还允许部分具体化,即部分限制模板的通用性。例如,部分具体化可以给类型参数之一制定具体的类型:
template <typename T1, typename T2> class Pair {...};
template <typename T1> class Pair<T1, int> {...};
关键字template后面的<>声明的是没有被具体化的参数。注意,如果指定了所有的类型,则<>将为空,这将导致显式具体化。
也可以通过为指针提供特殊版本来部分具体化现有的模板:
template<typename T> class Feeb {...};
template<typename T*> class Feeb {...};
部分具体化特性使得能够设置各种限制。例如:
// 原模板
template <class T1, class T2, class T3> class Trio {...};
// 部分具体化
template <class T1, class T2> class Trio<T1, T2, T2> {...};
template <class T1> class Trio<T1, T1*, T1*> {...};
模板也可以用作结构、类或模板类的成员:
template <typename T>
class beta
{
private:
template <typename V>
class hold;
hold<T> q;
hold<int> n;
public;
beta(T t, int i) : q(t), n(i) {}
template<typename U>
U blab(U u, T t);
void Show() const { q.show(); n.show(); }
};
//...
其中,blah()方法的U类型由该方法被调用时的参数值显式确定。
模板也可以用作参数:
template <template <typename T>class Thing,typename U,typename V>
class Crab {...};
7.模板类和友元
模板类声明也可以有友元。模板类的友元分3类:
- 非模板友元
template <typename T>
class HasFriend
{
private:
T item;
static int ct;
public:
...
friend void counts();
friend void reports(HasFriend<T> &);
};
//...
void counts() {...}
void reports(HasFriend<int> & hf) {...}
void reports(HasFriend<double> & hf) {...}
其中,counts()将称为HasFriend类所有具体化的友元。带HasFriend参数的reports()将称为hasFriend类的友元,带HasFriend参数的reports()将称为hasFriend类的友元。
- 约束模板友元,即友元的类型取决于类被实例化时的类型
template <typename T> void counts();
template <typename T> void report(T &);
template <typename TT>
class HasFriendT
{
private:
TT item;
static int ct;
public:
//...
friend void counts<TT> ();
friend void report<> (HasFriendT<TT> &);
};
//...
template <typename T> void counts() {...}
template <typename T> void report(T & hf) {...}
类的每一个具体化都获得了与友元匹配的具体化。
- 非约束模板友元,即友元的所有具体化都是类的每一个具体化的友元
约束模板友元是在类外面声明的模板具体化。int类具体化获得int函数具体化,以此类推。而通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的。
template <typename T>
class MantFriend
{
private:
T iteml
public:
//...
template <typename C, typename D> friend void show2(C &, D &);
};
template <typename C, typename D> void show2(C & c, D & d) {...}
该模板函数是所有ManyFriend类的友元。
8.模板别名(C++11)
可以使用typedef为模板具体化指定别名:
typedef std::array<double, 12> arrd;
arrd gallons;
C++新增了一项功能,使用using为模板提供别名:
template<typename T> using arrtype = std::array<T, 12>;
arrtype<doublke> gallons;
arrtype<int> days;
arrtype<std::string> months;
总之,arrtype表示类型std::array<T, 12>
C++还允许将using=语法用于非模板。用于非模板时,这种语法与常规typedef等价:
typedef const char * pc1;
using pc2 = const char *;