第14章 C++中的代码重用
1.包含对象成员的类
(1)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
从中可知,可以创建长度为零的空数组、指定长度的空数组、所有元素度被初始化为指定值的数组、用常规数组中的值进行初始化的数组。在C++11中,也可以使用初始化列表:
valarray<int> v5={20,32,17,9}; //C++11
下面是这个类的一些方法:
1)operator[ ]( ):让您能够访问各个元素
2)size():返回包含的元素数
3)sum():返回所有元素的总和
4)max():返回最大的元素
5)min():返回最小的元素
(2)初始化被包含的对象
Student(const char * str,const double * pd, int n)
:name(str),scores(pd,n) {}
因为该构造函数初始化的是成员对象,而不是继承的对象,所以在初始化列表中使用的是成员名,而不是类名;C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数
(3)使用被包含对象的接口
被包含对象的接口不是公有的,但可以在类方法中使用它。例如,下面的代码说明了如何定义一个返回学生平均分数的函数:
double Student::Average() const
{
if(scores.size()>0)
return scores.sum()/scores.size();
else
return 0;
}
可以定义一个使用string版本的<<运算符的友元函数:
//use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
os<<"Scores for"<<stu.name<<":\n";
...
}
同样,该函数也可以使用valarray的<<实现来进行输出,不幸的是没有这样的实现;因此,Student类定义了一个私有辅助方法来处理这种任务:
//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;
}
//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;
}
辅助函数也可以用作其他用户级输出函数的构建块—如果提供符合条件的函数
2.私有继承
1)包含与私有继承的区别:
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中(包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员);子对象来表示通过继承或包含添加的对象;对于继承类,使用类名而不是成员名来标识构造函数
包含与私有继承的相同点:获得实现,但不获得接口
2)初始化基类组件
对于构造函数,包含将使用这样的构造函数:
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
3)访问基类的方法
包含使用对象来调用方法:
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;
}
4)访问基类对象
例如,Student类的包含版本实现了Name()方法,它返回string对象成员name;但使用私有继承时,该string对象没有名称
为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:
const string & Student::Name() const
{
return (const string &) *this;
}
上述方法返回一个引用,该引用指向用于调用该方法的Student对象中的继承而来的string对象
5)访问基类的友元函数
显式地转换为基类来调用正确的函数,例如,对于下面的友元函数定义:
ostream & operator<<(ostream & os, const Student & stu)
{
os<<"Scores for"<<(const String &) stu<<":\n";
...
}
如果plato是一个Student对象,则下面的语句将调用上述函数,stu将是指向plato的引用,而os将是指向cout的引用:
cout<<plato;
6)使用包含还是私有继承
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承
7)保护继承
保护继承是私有继承的变体,保护继承在列出基类时使用关键字protected:
class Student:protected std::string,
protected std::valarray<double>
{...};
特征 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类得私有成员 |
私有成员变成 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
隐式向上转换(implicit upcasting)意味着无需进行显示类型转换,就可以将基类指针或引用指向派生类对象
8)使用using重新定义访问权限
假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。例如,假设希望Student类能够使用valarray类的sum()方法,可以在Student类的声明中声明一个sum()方法,然后像下面这样定义该方法:
double Student::sum() const //public Student method
{
return std::valarray<double>::sum(); //use privately-inherited method
}
另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如,假设希望通过Student类能够使用valarray的方法min和max(),可以在studenti.h的公有部分加入如下using声明:
class Student:private std::string,private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
};
注意,using声明只使用成员名——没有圆括号、函数特征标和返回类型。例如,为使Student类可以使用valarray的operator[]()方法,只需在Student类声明的公有部分包含下面的using声明:
using std::valarray<double>::operator[];
这将使两个版本(const和非const)都可用,这样,便可以删除Student::operator[]()的原型和定义。using声明只适用于继承,而不适用于包含。
3.多重继承
1)多重继承(multiple inheritance,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关系
2)公有MI
问题:从两个不同的基类继承同名方法;从两个或更多相关基类继承同一个类的多个实例
3)虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字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子对象,所以可以使用多态
4)新的构造函数规则
如果不希望默认构造函数来构造基虚类对象,则需要显式地调用所需的基类构造函数,因此,构造函数应该是这样的:
SingingWaiter(const Worker & wk, int p=0, int v=Singer::other)
:Worker(wk),Waiter(wk,p),Singer(wk,v) {}
注意:这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的;如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数
5)哪个方法
注意:多重继承可能导致函数调用的二义性。例如,BadDude类可能从Gunslinger类和PokerPlayer类那里继承两个完全不同的Draw()方法
一种方法是使用模块化方式,而不是递增方式,即提供一个只显示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();
}
另一种方法是将所有的数据组件都设置为保护的,而不是私有的,不过使用保护方法(而不是保护数据)将可以更严格地控制对数据的访问。
6)C-风格字符串库函数strchr()
while(strchr("wstq",choice)==NULL)
该函数返回参数choice指定的字符在字符串“wstq”中第一次出现的地址,如果没有这样的字符,则返回NULL指针
7)混合使用虚基类和非虚基类
如果基类是虚基类,派生类将包含基类的一个子对象;如果基类不是虚基类,派生类将包含多个子对象。当虚基类和非虚基类混合时,情况将如何呢?例如,假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类,而类M是从C、D、X和Y派生而来的。在这种情况下,类M从虚派生祖先(即类C和D)那里共继承了一个B类子对象,并从每一个非虚派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含了三个B类子对象,当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。
4.类模板
模板提供参数化(parameterized)类型,即能够将类型名作为参数传递给接收方来建立类或函数。例如,将类型名int传递给Queue模板,可以让编译器构造一个对int进行排队的Queue类
1)定义类模板
第10章的Stack类为基础来建立模板,原来的类声明如下:
typedef unsigned long Item;
class Stack
{
private:
enum{MAX=10}; //constant specific to class
Item items[MAX]; //holds stack items
int top; //index for top stack item
public:
Stack();
bool isempty() const;
bool isfull() const;
//push() returns false if stack already is full, true otherwise
bool push(const Item & item); //add item to stack
//pop() returns false if stack already is empty, true otherwise
bool pop(Item & item); //pop top into item
};
类模板:
#ifndef STACKTP_H_
#define STACKTP_H_
template <class Type>
class Stack
{
private:
enum {MAX=10}; //constant specific to class
Type items[MAX]; //holds stack items
int top; //index for top stack item
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type & item); //add item to stack
bool pop(Type & item); //pop top into 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
2)使用模板类
template <class T>
void simple(T t) {cout<<t<<'\n';}
...
simple(2); //generate void simple(int)
simple("two"); //generate void simple(const char *)
3)正确使用指针栈
使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串,把这些指针放在栈中是有意义的,因为每个指针都指向不同的字符串。注意,创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针
4)数组模板示例和非类型参数
指定数组大小的简单数组模板:一种方法是在类中使用动态数组和构造函数参数来提供元素数目,另一种方法是使用模板参数来提供常规数组的大小(C++11新增的模板array)
template<class T, int n>
关键字class指出T为类型参数,int指出n的类型为int,这种参数(指定特殊的类型而不是用作泛型名)称为非类型或表达式参数;表达式参数可以是整型、枚举、引用或指针;模板代码不能修改参数的值,也不能使用参数的地址;另外,实例化模板时,用作表达式参数的值必须是常量表达式。
表达式参数方法优点:构造函数方法使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的内存栈
表达式参数方法缺点:每种数组大小都将生成自己的模板,也就是说,下面的声明将生成两个独立的类声明:
ArrayTP<double,12> eggweights;
ArrayTP<double,13> donuts;
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
Stack<int> eggs(12);
Stack<int> dunkers(13);
另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员(而不是硬编码)存储在定义中,这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类
5)模板多功能性
模板类可用作基类,也可用作组件类,还可以用作其他模板的类型参数
递归使用模板:
ArrayTP<ArrayTP<int,5>,10> twodee;
这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组,与之等价的常规数组声明如下:
int twodee[10][5];
请注意,在模板语法中,维的顺序与等价的二维数组相反。
使用多个类型参数:
模板可以包含多个类型参数,注意,在main()函数中必须使用Pair<string,int>来调用构造函数,因为类名为Pair<string,int>,而不是Pair,另外,Pair<char *,double>是另一个完全不同的类的名称
默认类型模板参数:
类模板的另一项特性是,可以为类型参数提供默认值:
template<class T1, class T2=int> class Topo{...};
这样,如果省略T2的值,编译器将使用int:
Topo<double,double> m1; //T1 is double, T2 is double
Topo<double> m2; //T1 IS double, T2 is int
注意,可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值;对于非类型参数提供默认值,对于类模板和函数模板都是适用的
6)模板的具体化
模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明
隐式实例化:
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:
ArrayTP<int,100> stuff; //implicit instantiation
编译器在需要对象之前,不会生成类的隐式实例化:
ArrayTP<double,30> * pt; //a pointer, no object needed yet
pt=new ArrayTP<double,30>; //now an object is needed
第二条语句导致编译器生成类定义,并根据该定义创建一个对象
显式实例化:
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明位于模板定义所在的名称空间中。例如,下面的声明将ArrayTP<string,100>声明为一个类:
template class ArrayTP<string,100>; //generate ArrayTP<string,100> class
在这种情况下,虽然没有创建或者提及类对象,编译器也将生成类声明(包括方法定义)
显式具体化:
显示具体化是特定类型(用于替换模板中的泛型)的定义,当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本
一个专供const char *类型使用的SortedArray模板:
template<> class SortedArray<const char *>
{
...//detailed omitted
};
SortedArray<int> scores; //use general definition
SortedArray<const char *> dates; //use specialized definition
部分具体化:
部分限制模板的通用性,例如,部分具体化可以给类型参数之一指定具体的类型:
//general template
template<class T1, class T2> class Pair{...};
//specialization with T2 set to int
template<class T1> class Pair<T1,int>{...};
注意:关键字template后面的<>声明的是没有被具体化的类型参数,如果指定所有的类型,则<>内将为空,这将导致显式具体化:
//specialization with T1 and T2 set to int
template<> class Pair<int,int> {...};
如果有多个模板可供选择,编译器将使用具体化程度最高的模板:
Pair<double,double> p1; //use general Pair 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 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*>
7)成员模板
模板可用作结构、类或模板类的成员,要完全实现STL设计,必须使用这项特性
template<typename T>
class beta
{
private:
template<typename V> //nested template class member
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; //template object
hold<int> n; //template object
public:
beta(T t, int i) :q(t), n(i) {}
template<typename U> //template method
U blab(U u, T t) { return (n.Value() + q.Value())*u / t; }
void Show() const { q.show(); n.show(); }
};
如果所用的编译器接受类外面的定义,则在beta模板之外定义模板方法的代码如下:
template<typename T>
class beta
{
private:
template<typename V> //declaration
class hold;
hold<T> q;
hold<int> n;
public:
beta(T t, int i): q(t), n(i) {}
template<typename U> //declaration
U blab(U u, T t);
void Show() const {q.show(); n.show();}
};
//member definition
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;}
};
//member definition
template <typename T>
template <typename U>
U beta<T>::blab(U u, T t)
{
return {n.Value()+q.Value())*u/t;
};
8)将模板用作参数
模板可以包含类型参数(如typename T)和非类型参数(如int n),模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL
可以混合使用模板参数和常规参数,例如,Crab类的声明可以像下面这样打头:
template<template <typename T> class Thing, typename U, typename V>
class Crab
{
private:
Thing<U> s1;
Thing<V> s2;
...
模板参数T表示一种模板类型,而类型参数U和V表示非模板类型
9)模板类和友元
模板类声明也可以有友元,模板的友元分3类:
非模板友元;
约束模板友元:友元的类型取决于类被实例化时的类型;
非约束模板友元:友元的所有具体化都是类的每一个具体化的友元
模板类的非模板友元函数:
在模板类中将一个常规函数声明为友元:
template<class T>
class HasFriend
{
public:
friend void counts(); //friend to all HasFriend instantiations
...
};
模板类的约束模板友元函数:
首先,在类定义的前面声明每个模板函数:
template<typename T>void counts();
template<typename T>void report(T &);
其次,在函数中将模板声明为友元,这些语句根据类模板参数的类型声明具体化:
template<typename TT>
class HasFriendT
{
...
friend void counts<TT>();
friend void report<>(HasFriend<TT> &);
};
最后,还需为友元提供模板定义
模板类的非约束模板友元函数:
通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的:
template<typename T>
class ManyFriend
{
...
template<typename C, typename D> friend void show2(C &, D &);
};
10)模板别名(C++11)
template<typename T>
using arrtype=std::array<T,12>; //template to create multiple aliases
arrtype<double> gallons; //gallons is type std::array<double,12>
arrtype<int> days; //days is type std::array<int,12>
arrtype<std::string>months; //months is type std::array<std::string,12>
C++11允许将using=用于非模板。用于非模板时,这种语法与常规typedef等价:
typedef const char * pc1; //typedef syntax
using pc2=const char *; //using= syntax
typedef const int *(*pa1)[10]; //typedef syntax
using pa2=const int *(*)[10]; //using=syntax