1.概论
多重继承是否有必要吗?
这个问题显然是一个哲学问题,正确的解答方式是根据情况来看,有时候需要,有时候不需要,这显然是一句废话,有点像上马克思主义哲学或者中庸思。
但是这个问题和那些思想一样,被一知半解的人所误解和扭曲,最后变成了和稀泥的借口。下面来谈一谈我的个人看法。
假设有两个类A,B,A类提供了一种我们需要的容器,B类提供了我们需要的算法,这两个类是不同途径提供而来,现在创造一种满足这两个要求的类,就可以使用继承
类c可以同时继承两个类,获得他们的特征和行为。
但是,c++中的模板提供了新的思路,它可以通过自动识别来使用对应的方法,这样就不需要使用多重继承才能使用相关的功能了。
在c++标准库中iostream类,有一个多重继承
ios
istream ostream
iostream
这种继承类似于代码分解机制,ios是istream和ostream的共有类,他们继承了ios之后各种扩展了功能,然后再由iostream继承。
当我们使用继承的时候,无论是公有继承还是保护继承。或者是私有继承,派生类都继承了基类的全部成员,没有例外。
2.接口继承
这是一种只继承接口的继承,基类没有成员变量,也没用实现方法,只有一个接口声明,这样的类也被称为虚基类。就像下面这个类
class Base{
public:
virtual ~Base(){}
virtual void interface() = 0;
};
假设有三个这样的类,他们各种声名了不同的功能接口,让一个类来继承他们,实现他们的接口,最后可以通过不同的基类指针或者引用调用不同的接口。
class Base1{
public:
virtual ~Base1(){}
virtual void interface1() = 0;
};
class Base2{
public:
virtual ~Base2(){}
virtual void interface2() = 0;
};
class Base3{
public:
virtual ~Base3(){}
virtual void interface3() = 0;
};
class deriver: public Base1, public Base2, public Base3{
public:
实现三个基类的接口
}
void interface1(const Base1& b1){
b1.interface1
}
void interface2(const Base2& b2){
b2.interface2
}
void interface3(const Base3& b3){
b3.interface3
}
当我们只需要基类的每一个功能时,我们只需要知道基类是谁,而不需要了解派生类的实现。
但是这种继承,可以通过模板来化解,写出来的代码也简单很多。
只需要通过一个基类,写出三种方法,然后通过泛型编程调用不同的函数
class Base{
public:
void interface1(){}
void interface2(){}
void interface3(){}
};
template<class Base1>
void interface1(const Base1& b1){
b1.interface1();
}
template<class Base2>
void interface2(const Base2& b2){
b2.interface2();
}
template<class Base3>
void interface3(const Base3& b3){
b3.interface3();
}
两种方式有何不同?
第一种通过继承,使得对象更为明确了,有一种各司其职的感觉。
第二种是一种集合程度较高的写法,类型较弱。
但是两种方式没有高下之别,适用于不同的场景。
3.实现继承
c++的实现继承意味着所有内容都来自于基类,这样就不用实现继承来的方法了,直接调用即可,多重继承的一个用途包括混入类,混入类的存在是为了通过继承来增加
其他类的功能,它自己不能实例化自己,而是通过其他类来实例化。以数据库操作为例:
//数据库异常类
struct DatabaseError:std::runtime_error{
DatabaseError(const std::string& msg){}
};
//数据库类,连接数据库和关闭数据库,还有一些其他功能,比如检索,删除。。。这里不写出来
class Database{
std::string dbid;
public:
Database(const std::string* dbStr) : dbid(dbStr){}
virtual ~Database(){}
void open() throw(DatabaseError){
std::cout << "connected to" << dbid << std::endl;
}
void close(){
std::cout << dbid << "close" <<std::endl;
}
};
在一个服务器=客户端的模式下,客户拥有多个对象,这些对象分享一个连接的数据库,只有当所有客户都断开连接的时候,数据库才调用close关闭自己。
为了记录连接数据库的客户数量,创建一个新的特殊的类,也就是混入类
class Countable{
long count;
protected:
Countable(){count = 0;}
virtual ~Countable(){assert(count == 0);}
public:
long attach(){return count++;}
long detach(){
return --count > 0 ? count : (delete this, 0);
}
long refcount() const{
return count;
}
};
这个类的构造函数和析构函数都是保护成员,所有它无法自己生成,必须有一个友元类或者派生类来使用它;
析构函数这一点很重要,因为只有detach使用delete它的时候才会被正确的销毁
下面这个类会继承上面的两个类
class DBConnection : public Database, public Countable{
private:
DBConnection(const DBConnection&);
DBConnection& operator=()(const DBConnection&);//不允许赋值或者赋值
protected:
//构造函数--打开数据库
DBConnection(const string& dbStr)throw(DatabaseError) : Database(dbStr){
open();//
}
//析构函数,关闭数据库--无法从外面关闭
~DBConnection(){ close();}
public:
//静态方法
static DBConnection* create(const string& dbStr)throw(DatabaseError){
DBConnection* con = new DBConnection(dbStr);
}
con->attach();
assert(con->refCount() == 1);
return con;
};
这个类只能通过静态成员创建自己,在创建自己的同时,另外两个类也调用了自己的构造函数
不用修改Database类就做到了记录连接数据库的数量,然后根据数量来调用 DBConnection的析构函数关闭数据库
class DBClient{
DBConnection* db;
public:
DBClient (DBConnection* dbCon){
db = dbCon;
db->attach();
}
~DBClient(){ db->detach();}
};
客户端使用RAII(the Resource Acquisition Is Initation)的方法实现数据库的打开和关闭。
int main(){
DBConnection* db = DBConnection::create("database");
assert(db->refCount()==1);
return 0;
}
整合上面的代码,并具体运行
//整合上面的代码
#include <iostream>
#include<stdexcept>
#include<string>
#include<cassert>
using namespace std;
//数据库异常类
struct DatabaseError:std::runtime_error{
DatabaseError(const std::string& msg) : std::runtime_error(msg){}
};
//数据库类,连接数据库和关闭数据库,还有一些其他功能,比如检索,删除。。。这里不写出来
class Database{
std::string dbid;
public:
Database(const std::string& dbStr) : dbid(dbStr){}
virtual ~Database(){}
void open() throw(DatabaseError){
std::cout << "connected to" << dbid << std::endl;
}
void close(){
std::cout << dbid << "close" <<std::endl;
}
};
// 在一个服务器=客户端的模式下,客户拥有多个对象,这些对象分享一个连接的数据库,只有当所有客户都断开连接的时候,数据库才调用close关闭自己。
// 为了记录连接数据库的客户数量,创建一个新的特殊的类,也就是混入类
class Countable{
long count;
protected:
Countable(){count = 0;}
virtual ~Countable(){assert(count == 0);}
public:
long attach(){return count++;}
long detach(){
return --count > 0 ? count : (delete this, 0);
}
long refCount() const{
return count;
}
};
// 这个类的构造函数和析构函数都是保护成员,所有它无法自己生成,必须有一个友元类或者派生类来使用它;
// 析构函数这一点很重要,因为只有detach使用delete它的时候才会被正确的销毁
//
// 下面这个类会继承上面的两个类
class DBConnection : public Database, public Countable{
private:
DBConnection(const DBConnection&);
DBConnection& operator=(const DBConnection&);//不允许赋值或者赋值
protected:
//构造函数--打开数据库
DBConnection(const string& dbStr)throw(DatabaseError) : Database(dbStr){
open();//
}
//析构函数,关闭数据库--无法从外面关闭
~DBConnection(){ close();}
public:
//静态方法
static DBConnection* create(const string& dbStr)throw(DatabaseError){
DBConnection* con = new DBConnection(dbStr);
con->attach();
assert(con->refCount() == 1);
return con;
}
};
// 这个类只能通过静态成员创建自己,在创建自己的同时,另外两个类也调用了自己的构造函数
//
// 不用修改Database类就做到了记录连接数据库的数量,然后根据数量来调用 DBConnection的析构函数关闭数据库
class DBClient{
DBConnection* db;
public:
DBClient (DBConnection* dbCon){
db = dbCon;
db->attach();
}
~DBClient(){ db->detach();}
};
// 客户端使用RAII(the Resource Acquisition Is Initation)的方法实现数据库的打开和关闭。
int main(){
//创建数据库
DBConnection* db = DBConnection::create("database");
assert(db->refCount()==1);
DBClient c1(db);
assert(db->refCount()==2);
DBClient c2(db);
assert(db->refCount()==3);
DBClient c3(db);
assert(db->refCount()==4);
db->detach();
assert(db->refCount() == 3);
return 0;
}
在c++模板我曾经介绍过一种使用模板来计数的方式,这里将混入类设定模板,可以指定混入类的类型
template<class Counter>
class DBConnection : public Database, public Counter{
private:
DBConnection(const DBConnection&);
DBConnection& operator=(const DBConnection&);//不允许赋值或者赋值
protected:
//构造函数--打开数据库
DBConnection(const string& dbStr)throw(DatabaseError) : Database(dbStr){
open();//
}
//析构函数,关闭数据库--无法从外面关闭
~DBConnection(){ close();}
public:
//静态方法
static DBConnection* create(const string& dbStr)throw(DatabaseError){
DBConnection* con = new DBConnection(dbStr);
con->attach();
assert(con->refCount() == 1);
return con;
}
};
这里只有一个改变,就是加入了template<class Counter>,也可以把数据库作为模板类,这样就允许使用不同的数据库,如果这个类的混入类足够多,则可以指定更多的模板类。
4.重复子对象
当派生类继承基类的时候,它会继承基类所有的成员,下面程序说明了多个基类子对象在内存中的布局情况。
#include <iostream>
#include<stdexcept>
#include<string>
#include<cassert>
using namespace std;
class A{ int x;};
class B{int y;};
class C : public A, public B{ int z;};
int main(){
cout << "sizeof(A) = " << sizeof(A) << endl;
cout << "sizeof(B) = " << sizeof(B) << endl;
cout << "sizeof(C) = " << sizeof(C) << endl;
C c;
cout << "&c == " << &c << endl;
A* ap = &c;
B* bp = &c;
cout << "ap == " << static_cast<void*>(ap) << endl;
cout << "bp == " << static_cast<void*>(bp) << endl;
C* cp = static_cast<C*>(bp);//强制向下转换类型
cout << "cp == " << static_cast<void*>(cp) << endl;
return 0;
}
//输出
sizeof(A) = 4
sizeof(B) = 4
sizeof(C) = 12
&c == 0x6ffde0
ap == 0x6ffde0
bp == 0x6ffde4
cp == 0x6ffde0
bp == cp true
0
从程序输出来看,对象C的布局如下
A的数据
B的数据
C自己新增的数据
以对象A开头,依次向下偏移四个字节。
ap指向了对象C开头的位置,所以它的输出和&a一样
bp必须指向B对应的位置,所以它必须偏移四个字节,到达B的位置
而把bp向下转为C*的时候,它就需要后退四个字节,来到对象C的初始位置,但是如果bp指向一个独立的B对象,这种转化就是不合法了。
当使用bp == cp时,cp隐式转化为bp了,这种转化说明,向上转总是允许的
可以得出一个结论,子对象和完整类型间来回转换,要用到适当的偏移量
下面这个程序有点像菱形继承,但是其实不是,根据继承,子类会继承基类的全部成员,因此Left和Right都有一份Top,而Bottom则有两份Top了
#include <iostream>
#include<string>
#include<cassert>
using namespace std;
class Top{
public:
int x{10};//4 bit
public:
Top(int n): x(n){
}
};
class Left : public Top{
public:
int y{20};//4
public:
Left(int n, int m):Top(n),y(m){
}
};
class Right : public Top{
public:
int z{30};// 4
public:
Right(int n, int m):Top(n), z(m){
}
};
class Bottem: public Left, public Right{
public:
int w;//4
public:
Bottem(int i, int j, int k, int m):Left(i,k), Right(j,k), w(m){
}
};
int main(){
Bottem b(1,2,3,4);//Top + Top + Right + Left + int w
cout << sizeof(b) << endl;
Top* ptr = &b;//错误,因为存在两个Top因此产生二义性
cout << ptr
return 0;
}
5. 虚基类
想要真正实现菱形继承,就要用到虚函数了,让Left和Right共享一份Top
另外对象的空间大小是由非静态成员变量和支持虚函数所产生的虚函数表所共同决定的,还有不要忘了字节对齐,至于虚函数表占据多大的空间,要看编译器的实现
#include <iostream>
#include<string>
#include<cassert>
using namespace std;
class Top{
public:
int x; //4字节
public:
virtual ~Top(){}//8字节
Top(int n): x(n){
}
friend ostream& operator<<(ostream& os, const Top& t){
return cout << t.x;
}
};
//按照字节对其,Top占16字节
//继承父类的虚指针和数据成员16 + 4字节
class Left :virtual public Top{
public:
int y;
public:
Left(int n, int m):Top(n),y(m){
}
};
class Right :virtual public Top{
public:
int z;
public:
Right(int n, int m):Top(n), z(m){
}
};
//继承了Left和Right的虚指针以及数据 32
class Bottem: public Left, public Right{
public:
int w;
public:
Bottem(int i, int j, int k, int m):Left(i,j), Right(j,k),Top(m),w(m){
}
friend ostream& operator<<(ostream& os, const Bottem& b){
return cout << b.x << " " << b.y << " " << b.z << " " << b.w << endl;
}
};
class A{
int a;
};
int main(){
cout << sizeof(Top) << endl;//16
cout << sizeof(Left) << endl;//32
cout << sizeof(Right) << endl;//32
cout << sizeof(Bottem) << endl;//48
Bottem b(1,2,3,4);
cout << sizeof(b) << endl;
cout << b << endl;
cout <<static_cast<void*>(&b) << endl;
Top* p = static_cast<Top*>(&b);
cout << *p << endl;
cout << static_cast<void*>(p) << endl;
cout << dynamic_cast<void*>(p) << endl;
return 0;
}
虚继承这方面的事情讲得有点多了,感觉没必要一一详细列举,只需要说一个大概的情况即可
首先,使用虚继承可以保证基类在后续的直接多重继承中只被继承一个副本。
其次,Left和Right还有孙子类Bottem都继承了基类Top,且都有对Top的初始化,如果都起作用了,就会导致二义性,那么对继承的基类进行初始化
的任务就交给了Bottem,这时Left和Right都将会失去对Top初始化的作用,他们对Top的初始化会被忽略,只有最高派生类Bottem的初始化才有效
最后是关于虚继承的虚指针与虚函数表,编译器会给每一个派生类创建一个虚指针,它会指向一张虚函数表,里面有各种数据成员或者方法,在后面需要使用的时候,
它会根据这张虚函数表调用不同的函数,来实现多态。这一点很多文章都有分析,这里只是将一个大概的。以后有空再详细重复吧。
还有一个需要谈到的问题,子类对父类的对象的调用,用下面这个程序作为总结,子类会重复调用父类,最好的解决办法是调用一个特殊的处理,避免重复的工作
#include <iostream>
#include<string>
#include<cassert>
using namespace std;
class Top{
private:
int x; //4字节
public:
virtual ~Top(){}//8字节
Top(int n): x(n){
}
friend ostream& operator<<(ostream& os, const Top& t){
return cout << t.x;
}
};
//按照字节对其,Top占16字节
//继承父类的虚指针和数据成员16 + 4字节
class Left :virtual public Top{
private:
int y;
public:
Left(int n, int m):Top(n),y(m){
}
friend ostream& operator<<(ostream& os, const Left& l){
return cout <<static_cast<const Top&>(l) << " " << l.y;
}
};
class Right :virtual public Top{
private:
int z;
public:
Right(int n, int m):Top(n), z(m){
}
friend ostream& operator<<(ostream& os, const Right& r){
return cout <<static_cast<const Top&>(r) << " " << r.z;
}
};
//继承了Left和Right的虚指针以及数据 32
class Bottem: public Left, public Right{
private:
int w;
public:
Bottem(int i, int j, int k, int m):Left(i,i), Right(j,j),Top(k),w(m){
}
friend ostream& operator<<(ostream& os, const Bottem& b){
return cout
<< static_cast<const Left&>(b) << " "
<< static_cast<const Right&>(b) << " "
<< b.w;
}
};
class A{
int a;
};
int main(){
Bottem b(1,2,3,4);//3 1 3 2 4
cout << b << endl;
return 0;
}
问题在于Left和Right都调用了Top的输出,最好导致重复输出Top
所以,可以再Left和Right的保护成员中添加一个打印函数,这个函数就只会让派生类调用。
总结初始化的顺序:
(1) 所有虚基类子对象,按照他们在类定义中出现的位置,从上往下,从左往右初始化。
(2)然后非虚基类按通常顺序初始化
(3)所有的成员对象按声名的顺序初始化
(4)完整的对象的构造函数执行
#include <iostream>
#include<string>
#include<cassert>
using namespace std;
class M{
public:
M(const string& s){
cout << " M " << s << endl;
}
};
class A{
private:
M m;
public:
A(const string& s) : m("in A"){
cout << " A " << s << endl;
}
virtual ~A(){}
};
class B{
M m;
public:
B(const string& s) : m("in B"){
cout << " B " << s << endl;
}
virtual ~B(){}
};
class C{
M m;
public:
C(const string& s) : m("in C"){
cout << " C " << s << endl;
}
virtual ~C(){}
};
class D{
M m;
public:
D(const string& s) : m("in D"){
cout << " D " << s << endl;
}
virtual ~D(){}
};
class E: public A, virtual public B, virtual public C{
M m;
public:
E(const string& s): A("from E"), B("from E"),C("from E"), m("in E"){
cout << " E " << s << endl;
}
};
class F: virtual public B, virtual public C, public D{
M m;
public:
F(const string& s): B("from F"),C("from F"),D("from F"),m("in F"){
cout << " F " << s << endl;
}
};
class G: public E, public F{
M m;
public:
G(const string& s):B("from G"), C("from G"), E("from G"), F("from G"), m("in G"){
cout << " G " << s << endl;
}
};
int main(){
G g("main start");
return 0;
}
总体来看,先对E,再对F,然后G自己
细分下去:先初始化虚基类B C,然后是A
然后是E本身
接下来的F也是如此,初始化自己的虚基类,这个任务是G 完成,而且已经完成,也就是B C的初始化,所以就初始化D
最后是G自己,最后输出了m in G
B::m
B
C::m
C
A::m
A
E::m
E
D::m
D
F::m
F
G::m
G
//输出
M in B
B from G
M in C
C from G
M in A
A from E
M in E
E from G
M in D
D from F
M in F
F from G
M in G
G main start
6.名字查找问题
如果一个派生类同时继承了多个类,其中有的类有两个相同名字的函数,在调用这个函数,则会出错。当然,这几个类需要在统一层次的继承的。
#include <iostream>
#include<string>
#include<cassert>
using namespace std;
class Top{
public:
virtual ~Top(){}
};
class Left :virtual public Top{
public:
void fun(){
}
};
class Right : virtual public Top{
public:
void fun(){
}
};
class Bottem : public Left, public Right{
};
int main(){
Bottem b;
b.fun();//错误,产生了二义性
}
修改的方法是用基类名称来限定
#include <iostream>
#include<string>
#include<cassert>
using namespace std;
class Top{
public:
virtual ~Top(){}
};
class Left :virtual public Top{
public:
void fun(){
cout << "Left::fun()" << endl;
}
};
class Right : virtual public Top{
public:
void fun(){
cout << "Right::fun()" << endl;
}
};
class Bottem : public Left, public Right{
public:
using Right::fun;
};
int main(){
Bottem b;
b.fun();
}
正如上面提到的,必须是同一层次的继承才会有名字冲突,如果是垂直继承下来的,就不会有二义性了。
#include <iostream>
#include<string>
#include<cassert>
using namespace std;
class Top{
public:
virtual ~Top(){}
virtual void fun(){
cout << "Top::fun()" << endl;
}
};
class Left :virtual public Top{
public:
void fun(){
cout << "Left::fun()" << endl;
}
};
class Right : virtual public Top{};
class Bottem : public Left, public Right{};
int main(){
Bottem b;
b.fun();
}
虽然Bottem继承了Left和Right还有Top,其中Top和Left都有fun函数,但是b调用fun的时候,会直接调用Left的fun而不是基类Top的fun
这说明,继承中调用同名函数的时候,会优先调用派生级别高一些的。
7.避免使用多重继承
多重继承是一个很复杂的东西,我们应该尽量避免
如果下面两个条件有一个不满足,就不要多重继承
(1)是否需要通过一个新的派生类来显示接口?
(2)需要向上类型转化为基类吗?
尽量使用组合,而不是继承
8. 使用多重继承的一个案例:扩充一个接口
假设有这么一个库,只有一些头文件和接口,具体实现方法都看不见,这个库是一个带有虚函数的类层次接口,并且有全局函数,函数的参数是基类的引用,它利用这个类继承的多态
现在程序员需要用到一些功能,需要这些类的函数是虚函数,但是提供的库并不是,现在怎么办?
//1.提供的头文件和类声名
//Base.h文件
class Base{
public:
virtual void v() const;
void f() const;//假设我们想要这个函数是虚函数
~Base();
};
class Base1 : public Base{
public:
void v() const;
void f() const;
~Base1();
};
void fun1(const Base&);//全局函数,调用类中的某些接口
void fun2(const Base&);//同上
//上面这些d东西已经固定了,我们无法更改,现在我们想要添加一个功能g做某些事情,还想让Base1里的函数是虚函数,
//我们也不能令全局函数的功能发生变化
//为了解决问题可以使用多重继承
class MyProject{
public:
virtual ~MyProject(){
cout << "~MyProject()" << endl;
}
virtual void f() const = 0;//令这些函数全都为虚函数
virtual void v() const = 0;
virtual void g() const = 0;//我们需要增加的功能
};
class MyWay: public MyProject, public Base1{
public:
~MyWay(){
cout << "~MyWay()" << endl;
}
void f()const{
cout << "MyWay::f()" << endl;
Base1::f();
}
void v()const{
cout << "MyWay::v()" << endl;
Base1::v();
}
void g()const{
cout << "MyWay::g()\n";
}
};
//2下面的文件对用户不可见,这里写出来是为了更好的说明
//Base.cpp文件
void Base::f()const {
cout << "Base::fun()" << endl;
}
void Base::v()const{
cout << "Base::v()" << endl;
}
Base::~Base(){
cout << "Base::~Base()" << endl;
}
void Base1::f()const{
cout << "Base1::fun()" << endl;
}
void Base1::v()const{
cout << "Base1::v()" << endl;
}
Base1::~Base1(){
cout << "Base1::~Base1()" << endl;
}
void fun1(const Base& b){
b.f();
b.v();
}
void fun2(const Base& b){
b.f();
b.v();
}
int main(){
MyWay& pw = *new MyWay;
cout << "____________\n";
pw.f();
cout << "____________\n";
pw.v();
cout << "____________\n";
pw.g();
cout << "____________\n";
fun1(pw);
cout << "____________\n";
fun2(pw);
cout << "____________\n";
delete &pw;
return 0;
}
我们通过多重继承解决了接口问题,而且也没用改变原来的库的性质。