概念
继承与派生是同一过程从不同的角度看
- 保持已有类的特性而构造新类的过程成为继承
- 在已有类的基础上新增自己的特性而产生新类的过程称为派生
被继承的已有类称为基类(父类)
派生出的新类称为派生类(子类)
直接参与派生出某类的基类称为直接基类
基类的基类甚至更高的基类称为间接基类
继承与派生的目的
- 继承的目的:实现设计与代码的重用
- 派生的目的:当新问题出现原有程序无法解决(或不完全解决)时,需要对原有程序进行改造
派生类的构成
- 吸收基类成员
派生类实际就包含它的全部基类中除了构造和析构函数之外的所有成员- 改造基类成员
如果派生类声明了一个和某基类成员同名的新成员,派生的新成员就隐藏或覆盖了外层童名成员- 添加新的成员
派生类增加新成员使派生类在功能上有所发展
继承方式及类成员的访问控制
C++ 允许多继承,派生类可以从多个不同的类继承
class Devrived: 继承方式 Base1, 继承方式 Base2, ...
继承方式:
- 公有继承(public)
- 私有继承(private)
- 保护继承(protected)
无论那种继承,Base 类的 private 成员都是不可直接访问的,只能通过 Base 的共有接口访问
公有继承(public)
大部分时间都是用此方式,不需要刻意制造后续两种方式的场景
- 基类的 public 和 protected 成员的访问属性在派生类保持不变,基类的 private 成员不可直接访问
- 派生类的成员函数可以直接访问基类的 public 和 protected 成员,基类的 private成员除外
- 派生类对象访问基类继承的成员,只能访问 public 成员
- 基类提供的对外服务接口,派生类都提供(可以直接用基类的对外服务接口)
// 公有继承示例
// Point.h
#ifndef _POINT_H
#define _POINT_H
class Point { // 基类 Point 类的定义
public:
void initPoint(float x = 0, float y = 0) {
this->x = x;
this->y = y;
}
void move(float offX, float offY) {
x += offX;
y += offY;
}
float getX() const { return x; }
float getY() const { return y; }
private:
float x, y;
};
#endif // _POINT_H
// Rectangle.h
#ifndef _RECTANGLE_H
#define _RECTANGLE_H
#include "Point.h"
class Rectangle: public Point { // 公有继承 Point 类
public: // 新增公有函数成员
void initRectangle(float x, float y, float w, float h) {
initPoint(x, y); // 调用基类共有成员函数,不能直接操作基类的私有成员
this->w = w;
this->h = h;
}
float getW() const { return w; }
float getH() const { return h; }
private:
float w, h;
};
#endif // _RECTANGLE_H
#include <iostream>
#include <cmath>
using namespace std;
int main() {
Rectangle rect; // 定义 Rectangle 类的对象
// 设置矩形的数据
rect.initRectangle(2, 3, 20, 10);
rect.move(3, 2); // 派生类对象直接调用基类的 publc 成员函数
cout << "The data of rect(x, y, w, h): " << endl;
// 输出矩形的特征参数
cout << rect.getX() << "," // 派生类对象直接调用基类的 publc 成员函数
<< rect.getY() << "," // 派生类对象直接调用基类的 publc 成员函数
<< rect.getW() << "<"
<< rect.getH() << endl;
return 0;
}
私有继承(private)
- 基类的 public 和 protected 成员都以 private 身份出现在派生类中,基类的 private 成员不可直接访问
- 派生类的成员函数可以直接访问基类的 public 和 protected 成员,基类的 private成员除外
- 通过派生类的对象不能直接访问从基类继承的任何成员
- 只是把基类提供的公有接口变为私有工具,不对外继续服务(只能在派生类类使用,类外不可用)
// 私有继承示例
// 与 public 继承的差异代码
class Rectangle: private Point { // 私有继承
public:
void initRectangle(...) {...};
// 因为派生类和基类有同名函数,所以带有限定符,如果不同名则不用,直接调用即可
void move(float offX, float offY) {
Point::move(offX, offY); // 基类的公有成员函数不能在类外使用,只能在派生类类内使用
}
// 基类的公有成员函数不能在类外使用,只能在派生类类内使用
float getX() const { return Point::getX(); }
float getY() const { return Point::getY(); }
...
};
保护继承(protected)
- 基类的 public 和 protected 成员都以 protected 身份出现在派生类中,基类的 private 成员不可直接访问
- 派生类的成员函数可以直接访问基类的 public 和 protected 成员,基类的 private成员除外
- 通过派生类的对象不能直接访问从基类继承的任何成员
proteced 成员的特点和作用
- 对建立其所在类对象的模块来说,它与 private 成员的性质相同
- 对于其派生类来说,它与 public 成员的性质相同
- 即实现了数据隐藏,又方便继承,实现代码重用
纵向看:派生类看基类的 protected 成员,像 public 一样
横向看:整个类之外看基类的 protected 成员,像 private 一样
// 保护成员示例1
class A {
portected:
int x;
};
int main() {
A a;
a.x = 5; // 错误,横向看类外不能直接访问 private 一样
}
// 保护成员示例2
class A {
protected:
int x;
};
class B:publicA {
public:
void function();
};
void B::function() {
x = 5; // 正确,派生类的成员函数可以直接访问基类的 proteced 成员
}
公有继承是最基础的类重用需求,基类与派生类之间存在一种上下位概念
私有继承看作基类里有很多基础模块,派生类代码来讲这些是不错的基础工具,可以调用这些模块来做事情,但是打造的新类与基类完全要提供不同的对外服务接口。从基类继承来的这些工具不再具有继续派生的能力
保护继承与私有继承有同样的考虑,但是这些工具虽然不对外提供服务,但是派生类的子类也许有用,那么这些工具在子孙还可以继续使用
// 多继承示例
class A {
public:
void setA(int);
void showA() const;
private:
int a;
};
void A::setA(int x) {
a = x;
}
class B {
public:
void setB(int);
void showB() const;
private:
int b;
};
void B::setB(int x) {
b = x;
}
class C:public A, private B {
public:
void setC(int, int, int);
void showC() const;
};
void C::set(int x, int y, int z) {
setA(x); // 派生类成员直接访问基类的公有成员
setB(y); // 派生类成员直接访问基类的公有成员
c = z;
}
// 其余函数略
int main() {
C obj;
obj.setA(5);
obj.showA();
obj.setC(6, 7, 9);
obj.showC();
// 错误
obj.setB(6); // 私有继承对象不能访问基类公有成员
obj.showB(); // 私有继承对象不能访问基类公有成员
return 0;
}
向上转型
其概念是多态的支撑
- 一个公有派生类的对象在使用上可以被当做基类的对象,反之不行
派生类的对象可以隐含转换为基类对象
派生类的对象可以初始化基类的引用
派生类的指针可以隐含转换为基类的指针- 通过基类对象名,指针只能使用基类继承的成员
先来看一个示例:
预想通过一个通用接口,依据传递对象的指针来调用对象实际的用例
当然目前结果来看是不行的,因为编译时已经静态绑定实际的调用接口,后续通过 虚函数 概念的引入才能解决
// 类型转换规则示例
class Base1 {
public:
void display() const { // 基类 Base1
cout << "Base1:: display()" << endl;
};
class Base2:public Base1 { // 派生类 Base2
public:
void display() const {
cout << "Base2:: display()" << endl;
};
class Derived:public Base2 { // 派生类 Derived
public:
void display() const {
cout << "Derived:: display()" << endl;
};
// 通用接口,参数为指向基类对象的指针
void fun(Base1 *ptr) {
ptr->display();
}
int main() {
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1); // 用 Base1 对象的指针调用 fun 函数
fun(&base2); // 用 Base2 对象的指针调用 fun 函数
fun(&derived); // 用 Derived 对象的指针调用 fun 函数
return 0;
}
/* 输出结果
Base1:: display()
Base1:: display()
Base1:: display()
*/
继承时的构造函数
- 默认情况下基类的构造函数不被继承,派生类需要定义自己的构造函数
- 定义构造函数时,只需要对本类新增成员进行初始化,对继承来的基类成员的初始化是自动调用基类构造函数完成的
- 派生类的构造函数需要给基类的构造函数传递参数
- C++ 11 规定可以用 using 语句继承基类构造函数,使之成为派生类的构造函数,但是只能初始化基类继承的成员
using Base::Base;
这个不难理解,毕竟祖先到底是谁这条线谁能说的清楚呢,做好自己的就行了,剩余交给系统
- 当基类中声明有默认构造函数或未声明构造函数时,派生类构造函数可以不响基类构造函数传递参数,也可以不声明构造函数
- 构造派生类对象时,基类的默认构造函数将被调用
- 当需要执行基类中带形参的构造函数来初始化基类数据时,派生类构造函数应在初始化列表中为基类构造函数提供参数
单继承时的构造函数
派生类名 :: 派生类名(基类所需的形参,派生类所需的形参): 基类名(参数表),本类成员初始化列表
{
其他初始化
}
class B {
public:
B();
B(int i);
...
};
...
class C:public B {
public:
C();
C(int i, int j);
...
};
C::C(int i, int j):B(i), C(j) { ... }
多继承时的构造函数
雷同单继承时的构造函数,扩展一点点
派生类名 :: 派生类名(基类所需的形参,派生类所需的形参): 基类名1(参数表),... 基类名n(参数表),本类成员初始化列表
{
其他初始化
}
当还存有 组合 的情况时
派生类名 :: 派生类名(基类所需的形参,派生类所需的形参): 基类名1(参数表),... 基类名n(参数表),对象成员初始化列表,基本类型成员初始化列表
{
其他初始化
}
构造函数的执行顺序
- 调用基类构造函数,调用顺序按照它们被继承时声明的顺序
- 对初始化列表中的对象成员和基本类型成员进行初始化,初始化顺序按照它们在类中声明的顺序。对象成员初始化是自动调用对象所属类的构造函数完成的
- 执行派生类的构造函数体中的内容
class Derived:public Base2, public Base1, public Base3 {
public:
Derived(int a, int b, int c, int d):Base1(a), member2(d), member1(c), Base2(b) {} // 此处的次序与构造函数的执行次序无关,与继承的顺序相关,实际编程不建议这样写容易引起错觉
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
/* 运行结果
依序调用继承的 Base2, Base1, Base3 构造函数
再调用组合里的 Base1, Base2, Base3 构造函数
*/
继承时的析构函数
- 析构函数不被继承,派生类如果需要,要自行声明析构函数
- 声明方法一般(无继承关系时)类的析构函数相同
- 不需要显示调用基类的析构函数,系统会自动隐式调用
- 析构函数的调用次序与构造函数相反
继承时的复制构造函数
- 若建立派生类对象时没有编写复制构造函数,编译器会生成一个隐含的复制构造函数,该函数先调用基类的复制构造函数,再为派生类新增的成员对象执行复制
- 若派生类的复制构造函数,一般都要为基类的复制构造函数传递参数
- 派生类的复制构造函数只能接受一个参数,此参数不仅用来初始化派生类定义的成员,也将被传递给基类的复制构造函数
- 基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用
C::C(const C &c1): B(c1) {..} // 向上转型
作用域限定
- 当派生类与基类有相同成员时
若未特别限定,则通过派生类对象使用的是派生类中的同名成员,除非用基类名和作用域符 :: 来限定int main() { Derived d; Derived *p = &d; // 访问派生类成员 d.fun(); // 访问基类成员 d.Base1::fun(); p->Base2::fun(); return 0; }
二义性问题
如果多继承时各个基类有同名成员,但是派生类没有该同名成员,那么无法区隔调用哪个基类的成员
clase Base0 {
public:
int var0;
void fun0() { ... }
};
class Base1:public Base0 {
public:
int var1;
};
class Base2:public Base0 {
public:
int var2;
};
class Derived:public Base1, public Base2 {
public:
int var;
void fun() { ... };
};
int main() {
Derived d;
d.Base1::var0 = 2;
d.Base1::fun0();
d.Base2::var0 = 3;
d.Base2::fun0();
return 0;
}
尽管可以排除二义性,但是产生了代码的冗余,下面的方法可以避免冗余
虚基类
- 以 virtual 说明基类继承方式
- 用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
- 为最远的派生类提供唯一的基类成员,而不重复产生多次复制
- 在第一级继承时就要将共同基类设计为虚基类
class Base0 {
public:
int var0;
void fun0() { ... }
};
class Base1:virtual public Base0 {
public:
int var1;
};
class Base2:virtual public Base0 {
public:
int var2;
};
...
int main() {
Derived d;
d.var0 = 2; // 直接访问虚基类的数据成员
d.fun0(); // 直接访问虚基类的成员函数
return 0;
}
虚基类及其派生类构造函数
- 建立对象时所指定的类称为 最远派生类
- 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的
- 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中为虚基类的构造函数列出产生。如果未列出则表示调用该虚基类的默认构造函数
- 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略
// 有虚基类时的构造函数示例
class Base0 {
public:
Base0(int var):var0(var) {}
int var0;
void fun0() { ... }
};
class Base1:vitual public Base0 {
public:
Base1(int var):Base0(var) {} // 所有派生类都必须在构造函数初始化表中列出 基类的构造函数参数
int var1;
}
class Base2:vitual public Base0 {
public:
Base2(int var):Base0(var) {} // 所有派生类都必须在构造函数初始化表中列出 基类的构造函数参数
int var2;
}
class Derived:public Base1, public Base2 {
public:
Derived(int var):Base0(var), Base1(var), Base2(var) {} // 所有派生类都必须在构造函数初始化表中列出 基类的构造函数参数
int var;
void fun() { ... }
};
int main() {
Derived d(1); // 只会由该最远派生类的构造函数调用 Base0,Base1,Base2的构造函数不会去初始化Base0
d.var0 = 2;
d.fun0();
return 0;
}