C++ 多继承
1. 多继承的概念
定义:一个派生类,同时继承多个基类
语法格式:
class 派生类名 : 访问修饰符 基类1, 访问修饰符 基类2, ...
{
// 派生类成员
};
2. 多继承时构造/析构顺序
规则:
- 构造顺序:从左到右,先构造基类,再构造派生类
- 析构顺序:从右到左,先析构派生类,再析构基类
例子:
#include <iostream>
//阅读能力
class ReadAbility
{
public:
ReadAbility()
{
std::cout << "ReadAbility" << std::endl;
}
~ReadAbility()
{
std::cout << "~ReadAbility" << std::endl;
}
};
//写作能力
class WriteAbility
{
public:
WriteAbility()
{
std::cout << "WriteAbility" << std::endl;
}
~WriteAbility()
{
std::cout << "~WriteAbility" << std::endl;
}
};
//人类具备阅读和写作能力
class Human : public ReadAbility, public WriteAbility
{
public:
Human()
{
std::cout << "Human" << std::endl;
}
~Human()
{
std::cout << "~Human" << std::endl;
}
};
int main()
{
Human human;
return 0;
}
运行上面的例子,输出结果如下:
ReadAbility
WriteAbility
Human
~Human
~WriteAbility
~ReadAbility
3. 多继承中的一些陷阱
3.1 二义性问题
class ReadAbility
{
public:
void executeAbility()
{
std::cout << "doRead" << std::endl;
}
};
class WriteAbility
{
public:
void executeAbility()
{
std::cout << "doWrite" << std::endl;
}
};
class Human : public ReadAbility, public WriteAbility
{
};
int main()
{
Human human;
// human.executeAbility(); 错误,有二义性,2个基类中如果出现同名成员,这样写不知道到底调用哪个
//=============================================================================
// 这种写法叫做"作用域解析符调用"或"显式基类成员访问"
// 使用基类名::成员名的方式来明确指定调用哪个基类的成员函数
// 用于解决多重继承中的二义性问题
human.ReadAbility::executeAbility();
return 0;
}
当两个基类中出现同名成员且都可访问时,派生类对象访问该成员时就会出现二义性问题。它无法确定应该调用哪个基类的成员函数。除非使用作用域解析符调用,否则编译器会报错。
不过在单继承中会出现类似现象,只是它不会引起编译问题,而是默认访问的是自己的成员。例如:
class Base
{
public:
int a;
};
class Derived : public Base
{
public:
int a;
};
int main()
{
Derived d;
d.a = 1; // 派生类的a
d.Base::a = 2; // 显式基类成员访问的写法,访问基类的a
return 0;
}
3.2 菱形继承问题
所谓菱形继承,是指孙子类以多继承的方式,继承了两个或两个以上的父类,而这2个父类又同时继承了一个祖先类,示意图如下:
Top
/ \
/ \
Left Right
\ /
\ /
Bottom
3.2.1 二义性问题
class Top
{
public:
int a;
};
class Left : public Top
{
};
class Right : public Top
{
};
class Bottom : public Left, public Right
{
public:
void foo()
{
// a = 1; 错误,二义性问题,无法确定是Left继承自基类Top的a还是Right继承自基类Top的a
// 解决方法:使用作用域解析符调用
Left::a = 1;
Right::a = 2;
}
};
二义性引起编译错误,只能通过作用域解析符调用成员加以区分。
3.2.2 内存数据冗余
通过作用域解析符调用成员虽然能够解决编译问题,但是菱形继承还有个致命缺陷,就是继承基类对象冗余。还是上面的例子,如果在Top类的构造中加入调试打印:
class Top
{
public:
Top()
{
std::cout << "Top" << std::endl;
}
~Top()
{
std::cout << "~Top" << std::endl;
}
int a;
};
在它的孙子类构造/析构时,会分别出现两次构造/析构调用。这是因为在菱形继承的情况下,它的孙子类的内存布局里面会出现两个祖先类对象,分别继承自它的两个父类。虽然依然可以通过作用域解析符调用,避免二义性,但是内存浪费了,而且要小心的使用同一套作用域解析符。
为了解决这个问题,C++引入了虚继承。所谓虚继承,就是如果多个父类拥有共同祖先,并且他们会被一个子类进行多继承,那么在父类继承祖先类时,使用virtual关键字修饰继承。
示例:
#include <iostream>
class Top
{
public:
int a;
};
class Left : virtual public Top
{
};
class Right : virtual public Top
{
};
class Bottom : public Left, public Right
{
public:
void foo()
{
a = 1;
}
};
int main()
{
Bottom b;
std::cout << &b.Left::a << std::endl;
std::cout << &b.Right::a << std::endl;
return 0;
}
运行程序,可以看到a地址相同,也就是他们是同一个,这样Bottom对象里面就只有一个a了,不再有内存数据冗余,也就不再有二义性。那虚继承是怎么实现的呢?它是通过一个叫虚基类表(vbtable)实现的。这里不对详细的实现做深入的阐述,后面文章再深入分析。只要知道它是通过一个指向虚基类表的指针,这个指针指向的内存布局遵守约定的规则,特别是包含一些访问成员的偏移量和基类成员的起始地址等。
4. 虚继承对构造/析构函数顺序的影响
虚继承下的构造/析构顺序:
- 虚基类(如 Top)由最派生类(如 Bottom)直接初始化,且只构造一次。
- 顺序可简述为:先虚基类(从左到右),再非虚基类,最后派生类;析构相反。

浙公网安备 33010602011771号