十四、继承与组合(Inheritance & Composition)
引言
- C++最引人注目的特性之一是代码复用。
- 组合:在新类中创建已有类的对象。
- 继承:将新类作为已有类的一个类型来创建。
14.1 组合的语法
Useful.h
cpp
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{
int i;
public:
X(){i = 0;}
void set(int ii){i = ii;}
int read() const {return i;}
int permute(){return i = i * 47;}
};
#endif
Composition,cpp
cpp
#include "Useful.h"
class Y{
int i;
public:
X x;//嵌入对象,子对象
Y(){ i = 0;}
void f(int ii) {i = ii;}
int g() cosnt{return i;}
};
void main()
{
Y y;
y.f(47);
y.x.set(37);
}
这里
Y y;
语句执行的时候,y
里面的x
是利用构造函数进行初始化的
14.2 继承的语法
Useful.h
cpp
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{
int i;
public:
X (){i = 1;}
void set(int ii){i = ii;}
int read() const{return i;}
int permute() {return i = i*47;}
};
#endif
Inheritance.cpp
cpp
//C14:Inheritance.cpp
#include "Useful.h"
#include <iostream>
using namespace std;
class Y:public X
{
int i;//不是X的i
public:
Y(){i = 2;}
int change()
{
i = permute();//调用不同名称的函数
return i;
}
void set(int ii){
i = ii;
X::set(ii);//调用同名函数,需要加::
}
};
int main(){
cout << "sizeof(X) = " << sizeof(X) << endl;
cout << "sizeof(Y) = " << sizeof(Y) << endl;
Y D;//X::i = 1,Y::i = 2
D.change();//return Y::i = 47(X::i = 47)
D.read(); //X::i = 47
D.permute();//X::i = 47 * 47
D.set(15);//Y::i = 12,X::i = 12
return 0;
}
这里对y里面的x初始化也是通过X的无形参构造函数。
输出
cpp
sizeof(X) = 4
sizeof(Y) = 8
-
Y
继承自X
,这意味着Y
内将包含一个X
类型的子对象,就像在Y
内部直接创建了一个X
成员对象一样。无论是成员对象还是基类所占的存储都称为子对象。 -
Y
是X
的派生类,X
是基类。派生类继承基类的属性,这种关系称为继承。 -
X
的所有私有成员在Y
中仍然是私有的(因此Y
里面不能访问X
的私有成员,只能通过X
的函数)。通过public
继承,基类的所有公有成员 在派生类也保持公有(后面还会有private
继承、protected
继承),也就是说public
继承,X
中的私有在Y
中仍私有,公有仍公有,protected
仍protected
。protected
是指派生类可以访问,外部代码不可以访问。
-
将一个类用作基类 相当于声明了一个该类的(未命名)对象。因此。必须先定义这个类才能将其用作基类。
cppclass X; class Y:public X{ ............ };
14.3 构造函数初始化列表
- 在组合和继承中,确保**子对象被正确初始化**非常重要。
- 构造函数和析构函数不会被继承 (赋值运算符也不会被继承)。因此派生类的构造函数无法直接初始化基类的成员。
- 新类的构造函数无法访问子对象的私有数据元素。
- 如果不使用默认构造函数,该如何初始化子对象的私有数据元素。
解决方法
- 在构造函数初始化列表中调用子对象的构造函数。
- 构造函数初始化列表允许显式调用成员对象的构造函数。其原理是:在进入新类构造函数的函数体之前,所有成员对象的构造函数都会被调用。
- 内置类型的变量也可以在构造函数初始化列表 中初始化。而且初始化列表会自动帮忙初始化(避免垃圾值)。
注意 :在非虚继承(普通继承) 中,派生类只需要构造它的直接基类 ,间接基类会自动由中间类网上构造,构造链自动完成。
示例1
cpp
#include <iostream>
using namespace std;
class X{
int a;
public:
X(int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y{
int b;
public:
Y(int i,int j):b(i),x(j){cout << "Constructor Y:" << b << endl;}
X x;
};
int main(){
Y y(1,2);
return 0;
}
该实例中
x
是y
的成员
输出
cpp
Constructor X:2
Constructor Y:1
示例2
cpp
#include <iostream>
using namespace std;
class X {
int a;
public:
X(int i = 7) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y {
int b;
public:
Y(int i) :b(i) { cout << "Constructor Y:" << b << endl; }
X x;
};
int main() {
Y y(1);
return 0;
}
该实例中
x
是y
的成员
输出
cpp
Constructor X:7
Constructor Y:1
示例3
cpp
#include <iostream>
using namespace std;
class X{
int a;
public:
X (int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X
{
int b;
public:
Y(int i,int j):b(i),X(j){cout << "Constructor Y:" << b << endl;}
}
int main(){
Y y(1,2);
return 0;
}
输出
cpp
Constructor X:2
Constructor Y:1
示例4
cpp
#include <iostream>
using namespace std;
class X {
int a;
public:
X(int i = 9) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y :public X
{
int b;
public:
Y(int i, int j) :b(i){ cout << "Constructor Y:" << b << endl; }
};
int main() {
Y y(1, 2);
return 0;
}
输出
cpp
Constructor X:9
Constructor Y:1
14.4 组成与继承结合
- 当创建一个派生类对象 时,可能会创建以下对象:基类对象、成员对象和派生类对象本身。构造顺序自下而上:首先构造基类、然后构造成员对象,最后构造派生类自身。
- 基类或成员对象构造函数的调用顺序以它们在派生类中声明的顺序为准,而不是它们在初始化列表中出现的顺序。
- 默认构造函数可以被隐式调用。
示例
cpp
#include <iostream>
using namespace std;
class X{
int a;
public:
X(int i = 0):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X{
int b;
X x1,x2;
public:
Y(int i,int j,int m,int n):b(i),x2(j),x1(m),X(n){cout << "Constructor Y:" << b << endl;}
};
int main(){
Y y(1,2,3,4);
return 0;
}
输出
cpp
Constructor X:4
Constructor X:3
Constructor X:2
Constructor Y:1
14.5 名字隐藏
先介绍下面三种相关机制
- 重载(Overload):发生在同一作用域(类)内
- 重新定义(Redefining):继承关系中的普通成员函数
- 重写(Overriding):继承关系中的虚成员函数
名字隐藏:在继承关系中 ,如果派生类定义了一个与基类中同名的成员(不论是函数还是变量),那么这个基类的同名成员(函数)就会被"隐藏",即使参数不同也无法通过派生类对象直接访问。上面三种机制中,重载并非是名字隐藏,其余两个是名字隐藏。
概念 | 英文术语 | 适用情形 | 是否是名字隐藏 | 说明 |
---|---|---|---|---|
重载 | Overload | 同一个类或者同一作用域 | 否 | 函数名相同,但参数不同(返回值不同不算重载啊) |
重新定义 | Redifining | 继承中的普通函数 | 是 | 派生类中定义了同名函数(或变量),会隐藏基类中所有同名函数(变量),需要显示访问 |
重写 | Overriding | 继承中的虚函数 | 是 | 派生类用virtual 修饰的函数覆盖基类中的虚函数 |
虚函数会在后续的章节继续提到
- 在派生类中,只要重新定义了基类中重载的函数名 (假设基类有重载函数),基类中 该名字的其他版本 在 派生类中都会被自动隐藏。
示例
cpp
//C14:NameHidding.cpp
#include <iostream>
#include <string>
using namespace std;
class Base{
public:
int f() const
{
cout << "Base::f()" << endl;
return 1;
}
int f(string) const {return 1;}
void g(){}
};
class Derived1:public Base
{
public:
//重新定义
void g() const{};
};
class Derived2:public Base{
public:
//重定义
int f() const {
cout << "Derived2::f()" << endl;
return 2;
}
};
class Derived3:public Base{
public:
//改变return的类型
void f() const{
cout << "Derived3::f()" << endl;
}
};
class Derived4:public Base{
public:
//改变return的类型
int f(int) const{
cout << "Derived4::f()" << endl;
return 4;
}
};
void main(){
string s("hello");
Derived1 d1;
int x = d1.f();
d1.f(s);
Derived2 d2;
x = d2.f();
//!d2.f(s);//string版本被隐藏
Derived3 d3;
//!x = d3.f();return int 版本被隐藏
Derived4 d4;
//!x = d4.f();f()版本被隐藏
x = d4.f(1);
}
输出
cpp
Base::f()
Derived2::f()
Derived4::f()
14.6 不会自动继承的函数
以下函数不会被自动继承
- 构造函数
- 析构函数
- 赋值运算符函数
继承和静态成员函数(Inheritance and static member funcitons)
- 静态成员函数可以被继承到派生类中。
- 如果在派生类中重新定义了一个静态成员函数 ,那么基类中所有同名的重载函数也会被隐藏。
- 静态成员函数只能访问静态数据成员。
- 静态成员函数不能是是
virtual
(虚函数)。 - 静态成员函数没有
this
指针。
14.7 选择组合还是继承
- 共同点:组合(Composition)和继承(Inheritance)都会在新类中放置子对象(subojects) 。它们都使用构造函数初始化列表(intializer list) 来构造这些子对象。
- 当我们希望在新类中包含某个已有类的功能 (作文数据成员),但不想继承它的接口 时,通常使用组合。
- 当我们希望新类具有与现有类完全相同的接口 时,使用继承 。这被称为子类型化。
14.8 基类的子类型化
-
如果一个派生类继承自一个
public
的基类 ,那么该派生类就继承了基类的所有成员 ,但只能访问基类中的public
和protected
成员 。派生类就是基类的子类型 ,基类是派生类的超类型从外部使用者的角度 来看,这个派生类就具有与基类相同的
public
接口 (可以再加自己的新接口),因此它可以在需要基类对象的地方替代使用,这就是**子类型(subtyping)**的概念。 -
通过子类型化 ,当我们使用指针或引用 操作派生类对象时,它可以被当作基类对象来处理(即:可以指向或引用基类)
cpp#include <iostream> using namespace std; class Base { public: void speak() { cout << "Base speaking" << endl; } }; class Derived : public Base { public: void shout() { cout << "Derived shouting" << endl; } }; int main() { Derived d; // 子类型化:Base* 指向 Derived 对象 Base* ptr = &d; ptr->speak(); // OK:Base 的函数可调用 // ptr->shout(); // 错误:Base* 看不到 Derived 的接口 // 同样适用于引用 Base& ref = d; ref.speak(); // OK }
这在后续还会提到。
14.9 继承的访问控制
- 访问说明符:
public
private
protected
- 访问说明符 用于控制派生类对基类的成员的访问 ,以及从派生类到基类的指针和引用转换的权限。(无论哪种访问,基类的所有成员都会被继承,派生类的内存里都会有它们,只是能不能访问的差别。)
下面展示三种访问控制的不同
public
继承
如果一个派生类使用public
继承:
- 派生类的成员函数及其类内部可以访问其基类中的
public
和protected
成员。private
不可以访问。 - 派生类的对象可以访问其基类中的
public
成员。
说明:
public
继承会让基类的public
成员在派上类里面仍是public
,protected
成员仍是protected
。- 可以形成"子类型关系 ",即能用
Base*
指向Derived
。
示例
cpp
#include <iostream>
using namespace std;
class employee {
public: void print() {}
protected: short number;
private: string name;
};
class manager :public employee {
public:
void meeting(int n) {
print(); //ok
number = n;//ok
//name = "Jhoo" //error
}
private:
short level;
};
int main() {
employee E;
manager M;
E.print();//ok
//E.number = 3; //error
//M.name = "Jhoo"; //error
//M.number = 3; //error
M.print(); //ok
M.meeting(1); //ok
return 0;
}
可见protected
和 private
的区别就是:
- 派上类里面可以访问
protected
,而不可以访问private
,而派生类的对象两个都不可以访问
private
继承
如果一个类使用private
说明符继承:
- 派生类的成员函数及其类内部可以访问其基类中的
public
和protected
成员。private
不可以访问。 - 派生类的对象不能访问基类中的任何成员。
说明:
private
继承会把基类的public
和protected
成员变成派生类中的private
成员。- 所以 只有派生类内部能访问 基类的
public
和protected
成员,类外(包括派生类对象)都不能访问。 - 同时,不会形成"子类型关系 ",即不能 用
Base*
指向Derived
。
示例
cpp
#include <iostream>
using namespace std;
class employee {
public: void print() {}
protected: short number;
private: string name;
};
class manager :private employee {
public:
void meeting(int n) {
print(); //ok
number = n;//ok
//name = "Jhoo" //error
}
private:
short level;
};
int main() {
employee E;
manager M;
E.print();//ok
//E.number = 3; //error
//M.name = "Jhoo"; //error
//M.number = 3; //error
//M.print(); //error
M.meeting(1); //ok
return 0;
}
protected
继承
如果一个类使用protected
说明符进行继承:
- 派生类的成员函数及其类内部可以访问其基类中的
public
和protected
成员。private
不可以访问。 - 派生类的对象不能访问基类中的任何成员。
说明
protected
继承会将基类的public
和protected
成员变成派生类中的protected
;- 所以,只有派生类的内部能用,类外部(包括对象)都无法访问;
- 同时不形成"子类型关系 ",不能用
Base*
指向Derived
。
protected
继承和private
继承当派生类再次被继承时,会体现出来差别
总结
三种继承方式对比
方面 | public 继承 |
protected 继承 |
private 继承 |
---|---|---|---|
基类的public 成员在派生类中变成 |
public |
protected |
private |
基类的proitected 成员在派生类中变成 |
protected |
protected |
ptivate |
基类的private 成员在派生类中变成 |
不可访问 | 不可访问 | 不可访问 |
派生类成员函数能访问哪些基类成员 | public 和protected |
public 和protected |
public 和protected |
派生类对象能访问哪些基类成员 | public |
无法访问任何成员 | 无法访问热河成员 |
是否支持子类型转换(Base* = new Derived ) |
是 | 不支持 | 不支持 |
适用场景 | 接口继承、支持多态、面向对象设计 | 实现复用,不暴露接口 | 强封装 |
类中成员访问权限对比
访问标识符 | 类中是否可以访问 | 派生类是否可以访问 | 类外部(对象)是否可以访问 | 是否可以继承 |
---|---|---|---|---|
public |
可以 | 可以 | 可以 | 可以 |
protected |
可以 | 可以 | 不可以 | 可以 |
private |
可以 | 不可以 | 不可以 | 可以(但不可见) |
示例
Example1
cpp
#include <iostream>
using namespace std;
class Location {
public:
void InitL(int xx, int yy) {
X = xx;
Y = yy;
}
void Move(int xOff, int yOff) {
X += xOff;
Y += yOff;
}
int GetX() { return X; }
int GetY() { return Y; }
private:
int X, Y;
};
class Rectangle :public Location {
public:
void InitR(int x, int y, int w, int h);
int GetH() { return H; }
int GetW() { return W; }
private:
int W, H;
};
void Rectangle::InitR(int x, int y, int h, int w)
{
InitL(x, y);
W = w;
H = h;
}
int main() {
Rectangle rect;//派生类的对象
rect.InitR(2, 3, 20, 10);
rect.Move(3, 2);
cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;
return 0;
}
输出
cpp
5,5,20,10
Example2
这里在Example1 里面添加一个V
类。
cpp
class Location {
public:
void InitL(int xx, int yy) {
X = xx;
Y = yy;
}
void Move(int xOff, int yOff) {
X += xOff;
Y += yOff;
}
int GetX() { return X; }
int GetY() { return Y; }
private:
int X, Y;
};
class Rectangle :public Location {
public:
void InitR(int x, int y, int w, int h);
int GetH() { return H; }
int GetW() { return W; }
private:
int W, H;
};
void Rectangle::InitR(int x, int y, int h, int w)
{
InitL(x, y);
W = w;
H = h;
}
// 派生类
class V : public Rectangle {
public:
void Function() {
Move(3, 2); // 来自基类的函数
}
};
-
如果这里的
clas V :public Rectangle
继承改为private
继承,那么Move(3,2)
还能用吗?解答:
- 如果
Rectangle
是以public
或protected
方式继承Location
,
那么类V
无论使用哪种继承方式(public
/protected
/private
),都可以访问Move()
函数。 - 如果
Rectangle
是以private
方式继承Location
,
那么类V
无论如何继承Rectangle
,都无法访问Move()
函数。 - 因为:
public
和protected
继承会让基类的public
/protected
成员保留可见性(对类内仍可访问); - 而private
继承会将基类的public
/protected
成员都变为private
,对子类完全不可见。
- 如果
Example3
这里将Example1 的public
继承改为private
继承
cpp
#include <iostream>
using namespace std;
class Location {
public:
void InitL(int xx, int yy) {
X = xx;
Y = yy;
}
void Move(int xOff, int yOff) {
X += xOff;
Y += yOff;
}
int GetX() { return X; }
int GetY() { return Y; }
private:
int X, Y;
};
class Rectangle :private Location {
public:
void InitR(int x, int y, int w, int h);
int GetH() { return H; }
int GetW() { return W; }
private:
int W, H;
};
void Rectangle::InitR(int x, int y, int h, int w)
{
InitL(x, y); //OK
W = w;
H = h;
}
那么Example1里面的主函数需要修改
cpp
int main() {
Rectangle rect;//派生类的对象
rect.InitR(2, 3, 20, 10);
rect.Move(3, 2);
cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;
return 0;
}
rect.GetX()
和 rect.GetY()
以及rect.Move(3,2)
会报错,因为它们在Rectangle
里面已经是private
。所以Rectangle
对象无法访问它们。
所以我们将Exampel1修改成如下
cpp
#include <iostream>
using namespace std;
class Location {
public:
void InitL(int xx, int yy) {
X = xx;
Y = yy;
}
void Move(int xOff, int yOff) {
X += xOff;
Y += yOff;
}
int GetX() { return X; }
int GetY() { return Y; }
private:
int X, Y;
};
class Rectangle :private Location {
public:
void InitR(int x, int y, int w, int h);
void Move(int xOff, int yOff) {
Location::Move(xOff,yOff);
}
int GetX() {return Location::GetX();}
int GetY() {return Location::GetY();}
int GetH() { return H; }
int GetW() { return W; }
private:
int W, H;
};
void Rectangle::InitR(int x, int y, int h, int w)
{
InitL(x, y); //OK
W = w;
H = h;
}
int main() {
Rectangle rect;//派生类的对象
rect.InitR(2, 3, 20, 10);
rect.Move(3, 2);
cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;
return 0;
}
这里在 Rectangle
里面重新定义Move()
、GetX()
、GetY()
。
输出
cpp
5,5,20,10
Example4
cpp
class Base {
public: void f1() {}
protected: void f3() {}
};
class Derived1 : protected Base {};
class Derived2 : public Derived1 {
public:
void fun() {
f1(); //ok
f3(); //ok
}
};
int main() {
Derived1 d;
//d.f1(); //error
//d.f3(); //error
return 0;
}
这里如果将class Derived2 : public Derived1
改为private
或者protected
,那么
cpp
void fun() {
f1(); //ok
f3(); //ok
}
正确吗?
是正确的,因为,private
或者protected
仅仅是改变了Derived1
的成员在Derived1
里面是什么访问权限(public
/ protectecd
/ private
),而基类的public
和 protected
成员在派生类的内部是都可以访问的。
14.10 运算符重载与继承
- 除了赋值运算符(
=
)之外,其他运算符会自动被继承到派生类中。
14.11 多重继承
- 多重继承是指:一个派生类可以拥有多个直接基类。
cpp
class A{
//......
}
class B{
//......
}
class C:access A,access B{};
"access"是占位词,代表public
、 protected
、 private
中任意的一种访问方式。
Base classes: A B
↖ ↗
C ← Derived class
示例
cpp
#include <iostream>
using namespace std;
class B1 {
public:
B1(int i) {
b1 = i;
cout << "Constructor B1:" << b1 << endl;
}
void Print() { cout << b1 << endl; }
private:
int b1;
};
class B2 {
public:
B2(int i)
{
b2 = i;
cout << "Constructor B2:" << b2 << endl;
}
void Print() { cout << b2 << endl; }
private:
int b2;
};
class B3 {
public:
B3(int i) {
b3 = i;
cout << "Constructor B3:" << b3 << endl;
}
int Getb3() { return b3; }
private:
int b3;
};
class A :public B2, public B1//多重继承
{
public:
A(int i, int j, int k, int l);
void Print();
private:
B3 bb;
int a;
};
A::A(int i, int j, int k, int l) :a(l), bb(k), B2(j), B1(i) {
cout << "Constructor A:" << a << endl;
}
void A::Print() {
B1::Print();
B2::Print();
cout << bb.Getb3() << endl << a << endl;
}
int main() {
A aa(1, 2, 3, 4);
aa.Print();
return 0;
}
输出
cpp
Constructor B2:2
Constructor B1:1
Constructor B3:3
Constructor A:4
1
2
3
4
14.12 增量式开发
- 增量式开发:在不破坏已有代码 的前提下添加新代码。
- 继承和组合的一个优点是:它们支持增量式开发。
歧义问题:
- 歧义1:当多个基类中拥有同名成员函数 时(多重继承情况下),可能会发生名字冲突。
- 歧义2:如果一个派生类有两个基类,而这两个基类又都继承自一个类,那么就可能触发歧义(即,一个类在继承链中被"继承了两次")。也就是"菱形继承问题"。
消除歧义1
歧义1:
当多个基类中拥有同名成员函数 时(多重继承情况下),可能会发生名字冲突。
歧义1示例
cpp
class A{
public:
void f(){}
};
class B{
public:
void f(){}
void g(){}
};
class C:public A,public B{
public: void g(){}
};
void main(){
C c;
//c.f(); //error:编译器不知道式A还是B的f()
c.g(); //ok
c.B::g(); //ok
}
解决方法:
- 使用作用域解析运算符
::
(比如c.A::f();
) - 在派生类中定义一个新的函数(以覆盖或隐藏同名函数)
方法一:使用作用域解析运算符::
cpp
class A{
public:
void f(){}
};
class B{
public:
void f(){}
void g(){}
};
class C:public A,public B{
public: void g(){}
};
void main(){
C c;
c.A::f(); //ok
c.g(); //ok
c.B::g(); //ok
}
方法二:在派生类中定义一个新的函数
cpp
class A {
public:
void f() {}
};
class B {
public:
void f() {}
void g() {}
};
class C :public A, public B {
public:
void g() {}
void f() { A::f(); }
};
void main() {
C c;
c.f(); //ok
c.g(); //ok
c.B::g(); //ok
}
上述讲的只是第一种歧义:当多个基类中拥有同名成员函数 时(多重继承情况下),可能会发生名字冲突。
接下来讲述第二种歧义:如果一个派生类又有两个基类,且这两个基类又都继承自同一个类,就可以出现歧义。(即,同一个类被继承了两次)
消除歧义2
歧义2:
如果一个派生类有两个基类,且这两个基类又都继承自同一个类 ,就可能出现歧义。(即,同一个类被继承了两次)
歧义2示例
cpp
class A
{
public:
void f(){};
};
class B:public A{
//......
};
class C:public A{
//......
};
class D:public B,public C{
//......
};
void main(){
D d;//A,B,A,C,D
d.f(); //error
}
A B A C D
解决方法:
- 使用作用域解析运算符
::
- 在派生类中定义一个新函数来隐藏或重写冲突函数
- 使用虚基类(virtual base class) 避免重复继承
方法一:使用作用域解析运算符::
cpp
class A
{
public:
void f(){};
};
class B:public A{
//......
};
class C:public A{
//......
};
class D:public B,public C{
//......
};
void main(){
D d;//A,B,A,C,D
d.B::f(); //ok
}
方法二:在派生类中重新定义一个新的函数
cpp
class A
{
public:
void f(){};
};
class B:public A{
//......
};
class C:public A{
//......
};
class D:public B,public C{
public:
void f(){}
};
void main(){
D d;//A,B,A,C,D
d.f(); //ok
}
方法三:使用虚基类
- 关键字
virtual
只作用于其后紧跟的基类。
cpp
class D:virtual public A,public B,virtual public C{
//............
};
上述中A和C都是虚基类,B不是虚基类
cpp
class A
{
public:
void f(){};
};
class B:virtual public A{
//......
};
class C:virtual public A{
//......
};
class D:public B,public C{
//......
};
void main(){
D d;//A,B,A,C,D
d.f(); //ok
}
采用虚基类后,A,B,C,D的关系就变为
A B C D
虚基类与非虚基类的内存比较
-
虚基类
-
非虚基类
虚基类只存在一份,而非虚基类会重复拷贝。
虚基类的构造函数
-
它只会被调用一次。
-
它是由最底层派生类 的构造函数调用的(可以是显示 也可以是隐式调用)。
-
它会在非基类的构造函数之前被调用。
-
虚基类的构造函数会出现在所有派生类的构造函数的成员初始化列表中。
-
如果没有显示调用,则会自动调用它的默认构造函数。
下面解释显示 和隐式调用
cpp
//显示调用
class A {
public:
A(int x) { cout << "A(" << x << ")\n"; }
};
class B : virtual public A {
public:
B() : A(1) { cout << "B\n"; }
};
cpp
//隐式调用
class A {
public:
A() { /*...*/ } // 默认构造函数
};
class B : virtual public A {
public:
B() { // 隐式调用 A()
}
};
下面解释最底层派生类
最底层派生类:最终的创建那个类对象,那个类就是最底层派生类,也就是最终构造函数调用者。
示例
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int i) { cout << "A" << i << endl; }
};
class B : virtual public A {
public:
B(int i = 1):A(i) { cout << "B\n"; }
};
class C : virtual public A {
public:
C(int i = 2):A(i) { cout << "C\n"; }
};
class D : public B, public C {
public:
D(int i = 3):A(i) { cout << "D\n"; }
};
现在看两种对象的构造
cpp
int main(){
B b;
return 0;
}
此时构造的是 B
, B
是最底层派生类 ,所以它负责构虚基类A
。
输出
cpp
A1
B
cpp
int main(){
D d;
return 0;
}
此时构造的是 D
,D
是最底层派生类 ,所以它负责构造虚基类A
,即使 B
和C
也继承了 A
。
输出
cpp
A3
B
C
D
-
A
只被调用一次,由D
构造; -
B
和C
如果重新定义了A
的初始化,那也会被忽略
依旧是对最底层派生类的解释
假设有如下图的类
A B C D E
最底层派生类:
c++
E e;//E是最底层派生类
D d;//D是最底层派生类
B b;//B是最底层派生类
C c;//C是最底层派生类
构造函数:
cpp
B(......):A(......){......}
C(......):A(......){......}
D(......):B(......),C(......),A(......){......}
E(......):D(......),A(......){......}
E
不需要构造B
和C
,是因为它们不是E
的直接基类 ;但
E
必须构造A
,是因为A
是一个虚基类 ,虚基类总是由"最底层派生类"负责构造,即使它不是直接基类。
示例
cpp
#include <iostream>
using namespace std;
class A {
public:
A(const char* s) { cout << "Class A:" << s << endl; }
~A() {}
};
class B :virtual public A
{
public:
B(const char* s1, const char* s2) :A(s1) {
cout << "Class B:" << s2 << endl;
}
};
class C :virtual public A
{
public:
C(const char* s1, const char* s2) :A(s1) {
cout << "Class C:" << s2 << endl;
}
};
class D :public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2), A(s1) {
cout << "Class D:" << s4 << endl;
}
};
void main() {
D* ptr = new D("str1", "str2", "str3", "str4");
delete ptr;
}
输出
cpp
Class A:str1
Class B:str2
Class C:str3
Class D:str4
这里先构造
B
再C
的原因是class D:public B,public C
,先继承B
再继承C
示例(A
不是虚函数)
cpp
#include <iostream>
using namespace std;
class A {
public:
A(const char* s) { cout << "Class A:" << s << endl; }
~A() {}
};
class B :public A
{
public:
B(const char* s1, const char* s2) :A(s1) {
cout << "Class B:" << s2 << endl;
}
};
class C :public A
{
public:
C(const char* s1, const char* s2) :A(s1) {
cout << "Class C:" << s2 << endl;
}
};
class D :public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2) {
cout << "Class D:" << s4 << endl;
}
};
void main() {
D* ptr = new D("str1", "str2", "str3", "str4");
delete ptr;
}
输出
cpp
Class A:str4
Class B:str2
Class A:str2
Class C:str3
Class D:str4
这里
D
就不需要再构造A
了,简洁基类会自动构造。
14.13 向上转型(Upcasing)
- 继承最重要的方面,并不是它为了新类提供了成员函数。
- 更关键的是:它表达了新类和基类之间的关系。
- 这种关系可以总结为这样的一句话:新类是已有类的一种类型。
向上转型(Upcasting): 将一个 派生类 类型的引用或指针转换为
其 基类 的引用或指针,就叫做"向上转型"。
示例
cpp
class Instrument {
public:
void play() const {}
};
//Wind是Instrument的派生类
class Wind :public Instrument {};
void tune(Instrument& i) { i.play(); }
void main() {
Wind flute;
tune(flute); //向上转型(Upcasting)
Instrument* p = &flute;//Upcasting
Instrument& l = flute;//Upcasting
}
Instrument Wind
- 当通过指针或引用 (指向或引用基类)来操作派生类对象时,派生类对象可以被当作其基类对象来处理。
在第十五章还会接着讲述"Upcasting" 。
14.14 总结
- 继承和组合
- 多重继承
- 访问控制
- 向上转型