C++的一个主要目标是实现代码重用。公有继承是实现这种目标的机制之一,还有其它机制分别为组合和私有或保护继承。
组合、私有或保护继承可以用于实现has-a关系;多重继承能够从两个或更多的基类中派生出新的类;类模板能使用通用的术语定义类,然后使用模板来创建对特定类型定义的特殊类。
1.组合
valarray
类简介
该类用于处理数值,它支持将数组中所有元素相加以及在数组中找出最大最小值的操作。
模板特性意味着声明对象时,必须指定具体的数据类型。
class Student
{
private:
string name;
valarray<double> scores;
...
}
在Student
类中,可以使用string
和valarray<double>
的公有接口来访问和修改name
和scores
对象;但是在类的外面,只能通过Student
类的公有接口来访问和修改name
和scores
对象。
这被称为Student
类继承了其成员对象的实现,但是没有继承接口。
而在公有继承中,类可以继承接口,可能还有实现。
class Student
{
private:
//将typedef放在类的私有部分,保证只有在类的实现中可以使用
typedef valarray<double> ArrayDb;
string name;
ArrayDb scores;
public:
//成员列表初始化包含的对象
Student():name("Null Student"), scores() {}
//使用explicit避免显式转换
explicit Student(const string & s):name(s),scores() {}
}
对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数;对于成员对象,则使用成员名。
C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
在初始化列表中,项目被初始化的顺序为被声明的顺序(如上文中必须先name
再scores
),而不是它们在初始化列表中的顺序。
2.私有继承
使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。
派生类将继承实现,但不会继承接口。
包含将对象作为一个命名的成员对象添加到类中,私有继承将对象作为一个未命名的继承对象添加到类中。
class Student : private string, private valarray<double>
{
}
使用多个基类的继承被称为多重继承。
初始化基类组件:
使用私有继承,构造函数必须使用类名而不是成员名调用基类构造函数:
Student(const string & s):string(s),ArrayDb() {}
访问基类方法:
使用类名和作用域解析运算符(如ArrayDb::sum()
)来调用。
访问基类对象:强制类型转换
const string & Student::Name() const
{
return (const string &) *this;
}
访问基类的友元函数:强制类型转换
cout << (const string &) stu << endl;
在私有继承中,引用stu
不会自动转换成string
类型引用。这是因为,在私有继承中,未经过显式类型转换的派生类指针或引用,无法赋值给基类指针或引用。
3.使用组合还是私有继承
组合更易于理解;继承容易引起许多问题,尤其是需要同时继承多个基类时;
组合可以包括多个同类的子对象,继承只能使用一个这样的对象;
组合不能访问类的保护成员,继承能。
组合不能重新定义虚函数,继承能,但是私有继承重新定义的方法不是公有的,只能在类中使用。
通常,使用组合建立has-a关系,如果需要访问原有类的保护成员或者重新定义虚函数,则使用私有继承。
4.保护继承
保护继承,基类的公有成员和保护成员将称为派生类的保护成员。
基类的接口在派生类中可用,在继承层次外不可用。
在私有继承和保护继承中,使用using
声明重新定义访问权限,使得派生类能够使用基类的接口:
public:
using valarray<double>::min;
5.多重继承
公有多重继承表示的也是is-a关系:
//由于默认是私有派生,所以必须用public限定每一个基类
class SingingWaiter : public Waiter, public Singer {...}
多重继承的问题:
从两个不同的基类中继承同名的方法;
从两个或更多相关基类那里继承同一个类的多个实例。
Worker
是一个抽象基类,Singer
和Waiter
是它的两个派生类,通过多重继承从Singer
和Waiter
中派生出SingingWaiter
。
1.多个Worker
类对象
此时SingingWaiter
中有两个Worker
类对象,在将SingingWaiter
类对象地址赋值给Worker
类对象指针时,就会出现二义性问题,需要通过类型转换指向特定的对象。
SingingWaiter ed;
Worker *pw1 = (Waiter *) &ed;
Worker *pw2 = (Singer *) &ed;
2.虚基类
虚基类使得从多个类(其基类相同)派生出的对象只继承一个基类对象。
虚基类需要在类声明中使用关键字virtual
:
class Singer : virtual public Worker {...}
//public和virtual的顺序可以任意
class Waiter : public virtual Worker {...}
class SingingWaiter : public Waiter, public Singer {...}
这使得继承的Singer
和Waiter
对象共享一个Worker
对象,SingingWaiter
对象中只包含了一个Worker
对象。
3.新的构造函数规则
为了避免冲突,C++在基类是虚的时候时,禁止信息通过中间类自动传递给基类。
可以显式的调用基类的构造函数:
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other) : Worker(wk), Waiter(wk,p), Singer(wk,v) {}
如果不显式的调用基类的构造函数,就会调用基类的默认构造函数。
4.哪个方法
多重继承可能导致函数调用的二义性。
可以使用作用域解析运算符来确定究竟是哪个方法:
SingingWaiter newhire();
newhire.Singer::Show();
也可以在SingingWaiter
中重新定义Show()
:
void SingingWaiter::Show()
{
Singer::Show();
}
6.类模板
如果使用typedef
生成通用的类声明,则需要在每次修改类型时编辑头文件。
C++的类模板为生成通用的类声明提供了一种更好的方法,模板提供参数化类型,能够将类型名作为参数传递给接收方来建立类或函数。
1.定义类模板
template <class Type>
是模板类的开头,关键字template
告诉编译器要定义一个模板。尖括号的内容相当于函数的参数列表,class
看作是变量的类型名,Type
看作是变量的名称。
在模板定义中,使用泛型名Type
代替声明中的具体类型。
成员函数模板:
使用模板成员函数替换原有类的方法,每个函数头都应该以相同的模板声明打头。
需要将类的限定符从Stack::
变成Stack<Type>::
。
如果在类声明中定义了方法(内联定义),可以省略模板声明和类限定符。
类模板和成员函数模板并不是类和成员函数的定义。模板的具体实现,被称为实例化或具体化。
不能将成员函数模板放在独立的实现文件中,因为成员函数模板不是成员函数定义,不能单独编译。
应当将所有模板信息放到一个头文件中,在要使用这些模板的文件中包含该头文件。
//版本类以该句为开头
template <class Type>
class Stack
{
private:
enum {MAX = 10}; //类中的符号常量
Type items[MAX]; //Type类型的数据
int top;
public:
Stack();
bool isempty();
bool isfull();
bool push(const Type & item); //函数参数为Type类型
bool pop(Type & item);
};
//每个函数头都应该以相同的模板声明打头
template <class Type>
Stack<Type>::Stack() //修改类的限定符
{
top = 0;
}
2.使用模板类
使用所需的具体类型名替代泛型名,来声明一个类型为模板类的对象。
对于下面的声明,编译器将按照模板生成两个独立的类声明和两组独立的类方法。
Stack<int> kernels;
Stack<string> colonels;
3.使用指针栈
为了使pop
函数能够正常工作,应当使调用程序提供给一个指针数组,其中每个指针都指向不同的字符串。
这样每次pop
时,字符串本身并不会移动,只是指向字符串的指针被新建加入栈中。
栈的析构函数也不会对字符串有任何影响,栈的构造函数使用new
创建了一个用于保存指针的数组,析构函数只是删除这个数组,而不是删除字符串。
template <class Type>
class Stack
{
private:
enum {SIZE = 10}; //默认栈大小
int stacksize;
Type *items; //存储Type类型的数据
int top;
public:
explicit Stack(int ss=SIZE);
Stack(const Stack & st); //复制构造函数
~Stack() {delete [] items;} //析构函数
bool isempty() {return top == 0;}
bool isfull() {return top == stacksize;}
bool push(const Type & item);
bool pop(Type & item);
Stack & operator=(const Stack & st); //赋值运算符
};
template <class Type>
Stack<Type>::Stack(int ss) :: stacksize(ss),top(0)
{
items = new Type[stacksize]; //new创建动态数组
}
//Stack 只能在模板声明和模板函数定义内使用
Stack<Type> & Stack<Type>::operator=(const Stack & st)
{
}
使用时:
Stack<const char *>st(stacksize);