由于公有继承是 is - a 的关系,即班长首先是一个学生,因此班长类可以从学生类中继承而来。但很多情况下需要 has - a 的关系,即学生有姓名和成绩组成,而姓名和成绩可以是两个不同的类。此时可以让学生类中包含姓名类和成绩类的对象来实现,这种方式叫做包含:
class Student {
private:string name; // 姓名类的对象valarray scores; // 成绩类的对象
};
由于声明为私有成员,此时对于姓名和成绩的管理只能通过成员函数利用姓名类和成绩类的接口来实现,注意,此时学生类并没有获得姓名类和成绩类的接口,而只是通过对象调用他们。因此对于 has - a 关系来说,类兑现不能自动获得被包含对象的接口。从实现上来说,两个姓名类之间进行加减拼接操作是没有意义的,因此也不应该继承接口。
但是使用姓名来排序是一件有意义的事情,因此没有继承接口的情况下就需要编写这个成员函数,在函数中通过姓名对象调用它的接口来实现。
由于构造函数用于初始化对象,如果类中存在对象成员,那么是否使用初始化列表就有了区别:
Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {}
name
和 scores
来说将分别使用 String(const char *)
和 ArrayDb(const double *, int)
构造函数来初始化;name
和 scores
来说将先使用默认的构造函数来生成对象,然后在构造函数中根据相应的代码对其进行赋值。除了上一种实现 has - a 关系的方法,还可以采用私有继承来实现,这样基类中的公有成员和保护成员都将变为派生类的私有成员,这意味着基类方法将不会成为派生类对象公有接口的一部分,但是派生类的成员函数却可以使用它们:
class Student : private String, private valarray {
public:...
};
使用多个基类的继承被称为多重继承(multiple inheritance, MI),对于这个继承类,因为提供了两个无名称的子对象成员,所以需要采用新的构造函数:
Student(const char * str, const double * pd, int n) : String(str), ArrayDb(pd, n) {}
类定义与上一个版本的区别在于省略了显式对象名称,并在内联构造函数中使用了类名,而不是成员名:
#include
#include
#include
using namespace std;
class Student : private String, private valarray {
private:typedef valarray ArrayDb;ostream & arr_out(ostream & os) const;
public:Student() : string("Null Student"), ArrayDb() {}explicit Student(const char * s) : String(s), ArrayDb() {}explicit Student(int n) : String("Nully"), ArrayDb(n) {}Student(const char * str, int n) : String(str), ArrayDb(n) {}Student(const char * str, const ArrayDb & a) : String(str), ArrayDb(a) {}Student(const char * str, const double * pd, int n) : String(str), ArrayDb(pd, n) {}~Student() {}double Average() const;double & operator[](int i);double operator[](int i) const;const string & Name() const;// friendsfriend istream & operator>>(istream & is, Student & stu);friend istream & getline(istream & is, Student & stu);friend ostream & operator<<(istream & os, const Student & stu);
};
如果用上一个版本写一个求均值的方法:
double Student::Average() const {if(scores.size() > 0)return scores.sum() / scores.size();return 0;
}
但是私有继承版本的实现如下,即通过在派生类方法中用作用域解析符号来实现:
double Student::Average() const {if(ArrayDb::size() > 0)return ArrayDb::sum() / ArrayDb::size();return 0;
}
若要访问基类对象(姓名 string),由于基类对象没有名称,因此需要通过强制类型转换来将 Student 对象转换为 string 对象:
const string & Student::Name() const {return (const string &) *this;
}
用类名显式地限定函数名不适合友元函数,这是因为友元不属于类。然而,可以通过显示地转换为基类来调用正确的函数:
cout << stu; // 通过派生类友元函数输出
os << "Scores for" << (const string &) stu << ":\n"; // 显式地将stu转换为string对象引用,进而调用operator<<(ostream &, const string &)
引用 stu 不会自动转换为 string 对象引用,这是因为在私有继承中,在不显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针;第二个原因则是这个类使用多重继承,编译器无法确定应转换成哪个基类。
不过在这个例子中,即便使用公有继承,第一条语句仍然会与派生类的友元函数匹配,从而导致递归调用。
保护继承是私有继承的变体,基类的公有成员和保护成员都将称为派生类的保护成员。二者的主要区别在于派生类派生出另一个类时:
假设派生类想要使用基类的私有方法,除了可以在成员方法中使用作用域解析符,还可以使用 using 关键字进行声明,这将使(const 和非 const 版本均可用):
class Student : private String, private valarray {
public:using valarray::min;using valarray::max;...
};
该语句只适用于继承,而不适用于包含。
多重继承即子类同时继承多个父类的特性,举个栗子:
class className : public Dad1, public Dad2 {...};
多重继承看起来很美好,但也有它自己的新问题,举个栗子:
class Grandpa {
private:string name;
};
class Dad : public Grandpa {...
};
class Mom : public Grandpa {...
};
class Son : public Dad, public Mom {...
};
上述代码中儿砸继承了它的父亲和母亲,父亲和母亲继承了祖父。那么问题来了,儿砸到底有几个名字?从理论上来看,同时继承父亲和母亲的私有成员变量,那么就是两个名字,但这与显示世界的实际情况不符。
为了解决这个问题,C++引入了一种新技术——虚基类。
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象,举个栗子:
class Dad : virtual public Grandpa { // virtual的先后顺序不影响作用效果...
};
class Mom : public virtual Grandpa {...
};
class Son : public Dad, public Mom {...
};
此时儿砸就只有一个名字了!
使用虚基类会对构造函数的初始化列表产生影响:
Son(const Grandpa & grandpa, int a = 0, int b = 1) : Dad(grandpa, a), Mom(grandpa, b) {}
由于 grandpa 会从 Dad 和 Mom 两条路径上进行传递,因此出现了冲突,为避免这种冲突,虚基类会禁止中间类传递信息给基类,此时只有 a 和 b 两个变量会被传递并用于初始化。但是要想创建派生类,首先必须先创建基类,因此虚基类会调用默认的构造函数来对 grandpa 进行初始化。要想选择特定的构造函数对 grandpa 进行初始化,需要显式的指定:
Son(const Grandpa & grandpa, int a = 0, int b = 1) : Grandpa(grandpa), Dad(grandpa, a), Mom(grandpa, b) {}
注意,虚基类必须这样做,但是对于非虚基类则是非法的。
再来看另一种情况,加入 son 调用了一个自己没有重新定义的函数,而此时 dad 和 mom 中均有该函数的定义,此时调用是二义性的(单继承会调用最近祖先的同名方法)。为解决这个问题可以使用作用域解析运算符来表示:
Son son;
son.show(); // 二义性
son.Dad::show(); // allow
当然,更好的办法是在类中重新定义 show 方法:
void Son::show() {Dad::show();
}
像 Stack 这样的类来说,内部变量类型可能是多样化的,而为此定义多个相同功能的类显然过于冗余,因此可以使用类模板:
template // 早期版本,因为class易混淆,所以新版本换为了typaname
template // 新版本class Stack {...Item items[Max]; // 老版本,Item表示数据类型,即typedef unsigned long Item;Type items[Max]; // 模板化...
};bool Stack::push(const Item & item) {...} // 老版本
bool Stack::push(const Type & item) {...} // 模板化Stack kernels; // 使用方法
Type 为泛型标识符,被称为类型参数(type parameter),这意味着它类似于变量,但赋给它们的不能是数字,而只能是类型,因此最后一句的参数 Type 的值为 int 。