面向对象之继承
一、继承的概念
继承的定义
定义
继承就是在原有类的基础上创建新的类,新的类会继承原有类的所有属性和方法。
格式
class Son:public Father
{
//...
};
其中,Son为子类,Father为父类,“:”后为继承方式。
注意事项
- 基类的构造函数与析构函数不能被继承。
- 派生类(子类)会继承所有基类的属性和方法,不能选择性的继承。
- 派生类可以添加新的成员,实现新功能的拓展。
- 一个基类可以派生出多个派生类,一个派生类也可以继承自多个子类。
二、继承方式
public(公有继承)
公有继承时对派生类成员访问权限的影响
基类成员权限 | 在派生类中成员的权限 |
---|---|
public | public |
protected | protected |
private | 不可访问 |
注意
私有成员 | 不可访问成员 |
---|---|
在类内或者类外都不可以访问的成员 | 在类外不可访问,只能通过类的成员访问 |
protected(保护继承)
保护继承时对派生类成员访问权限的影响
基类成员权限 | 在派生类中成员的权限 |
---|---|
public | protected |
protected | protected |
private | 不可访问 |
private(私有继承)
私有继承时对派生类成员访问权限的影响
基类成员权限 | 在派生类中成员的权限 |
---|---|
public | private |
protected | private |
private | 不可访问 |
三种继承方式总结
三、派生类
特点
- 派生类不会继承基类的构造函数与析构函数。
- 当派生类新增的成员函数与从基类继承的成员函数重名时,新成员函数会覆盖基类的成员函数。
派生类的构造函数与析构函数
派生类的构造函数除了要初始化派生类新增的成员外,还要初始化基类的成员。
格式
class Father{
public:
Father(string sing,string dance);
~Father();
private:
string _sing;
string _dance;
};
//Son继承Father类
class Son:public Father{
public:
//子类的构造函数
Son(string rap,string sing,string dance);
~Son();
private:
string _rap;
};
关于派生类构造函数的注意事项
- 派生类构造函数和基类构造函数的调用顺序:先调用基类的构造函数,在调用派生类的构造函数。
- 派生类构造函数的参数需要包含基类成员变量和派生类成员变量的参数值。
- 调用构造函数时,基类构造函数从派生类构造函数的参数中获取实参。
- 若基类定义了有参构造函数,派生类必须定义构造函数。
拓展—成员对象
定义
当一个类的成员是另一个类的对象时,该对象就叫成员对象。
实例
class Small{
public:
Small();
};
class Big{
private:
Small small;
};
含有成员对象时派生类构造函数的初始化
当派生类含有成员对象时,派生类的构造函数除了要初始化基类的成员变量和新增的成员变量外,还要初始化成员对象。
实例
class Adult //创建基类
{
public:
Adult(int height,int weight);
~Adult();
private:
int _height;
int _weight;
};
class Toy //创建Toy类
{
public:
Toy(string type);
~Toy();
private:
string _type;
};
class Child:public Adult //创建派生类
{
private:
Toy toy; //创建成员对象,对象toy为Toy类的成员
public:
//派生类中含有成员对象,初始化过程
Child(int candy,int height,int weight);
~Child();
};
//类外实现派生类构造函数,使用“:”调用基类构造函数,成员对象构造函数
Child::Child(int candy,int height,int weight):
Adult(height,weight),Toy(type)
{
//新增成员初始化...
}
构造函数与析构函数的调用顺序
当创建派生类对象时,构造函数的调用顺序为:
在继承中,析构函数的调用顺序与构造函数的调用顺序相反。
析构函数的的调用顺序为:
调用隐藏的基类成员函数
当派生类的成员函数与基类的成员函数同名时,基类的成员函数会被隐藏,使用派生类对象调用该同名函数时,默认调用的是派生类的成员函数,若想要调用基类的同名成员函数,可在函数前加作用于限定符“::”或通过基类指针指定。
实例
#include<iostream>
using namespace std;
class Adult { //创建基类
public:
void play();
};
//类外实现基类同名函数
void Adult::play() {
cout << "成年人玩游戏" << endl;
}
class Child :public Adult { //创建子类
public:
void play(); //声明同名函数
};
//类外实现派生类同名函数
void Child::play() {
cout << "小孩子玩游戏" << endl;
}
int main() {
Child child;
child.play(); //派生类对象调用同名函数
//通过作用域限定符调用基类同名函数
child.Adult::play();
//通过基类指针调用基类同名函数
Adult *p = &child;
p->play();
return 0;
}
编译运行结果
小孩子玩游戏
成年人玩游戏
成年人玩游戏
四、多继承
多继承的方式
多继承的定义
一个派生类继承自多个基类,这种继承方式称为多继承。
格式
class Father{
//...
};
class Mother{
//...
};
class Child:public Father,public Mother
{
//...
};
多继承中派生类的构造函数与析构函数
调用顺序
构造函数的调用顺序为:
在继承中,析构函数的调用顺序与构造函数的调用顺序相反。
析构函数的的调用顺序为:
#include<iostream>
using namespace std;
class Father{
public:
Father()
{
cout<<"Father类构造函数"<<<endl;
}
void play_f()
{
cout<<"爸爸在玩游戏"<<endl;
}
~Father()
{
cout<<"Father类析构函数"<<<endl;
}
};
class Mother{
public:
Mother()
{
cout<<"Mother类构造函数"<<<endl;
}
void play_m()
{
cout<<"妈妈在玩游戏"<<endl;
}
~Mother()
{
cout<<"Mother类析构函数"<<<endl;
}
};
class Friend{
public:
Friend()
{
cout<<"Friend类的构造函数"<<endl;
}
~Friend()
{
cout<<"Friend类的析构函数"<<endl;
}
};
class Child:public Father,public Mother
{
public:
Child():Father(),Mother()
{
cout<<"Child类构造函数"<<endl;
}
~Child()
{
cout<<"Child类析构函数"<<endl;
}
};
int main()
{
Child child;
child.play_f();
child.play_m();
return 0;
}
编译运行结果
Father类构造函数
Mother类构造函数
Friend类的构造函数
Child类构造函数
爸爸在玩游戏
妈妈在玩游戏
Child类析构函数
Friend类的析构函数
Mother类析构函数
Father类析构函数
多继承中二义性的两种情况
不同基类中有同名成员函数
在多继承中,若多个基类中有同名的成员函数,派生类对象调用同名函数时,运行会出现错误。
间接的基类成员变量在派生类中有多份拷贝
在继承中,若派生类继承的多个基类可能继承自同一个基类,间接基类的成员变量在派生类中会存在多份拷贝,当通过派生类对象访问间接基类成员变量时,就会出现二义性。
为了避免二义性,可以在访问时,通过作用于限定符“::”进行区分。
五、虚继承
虚继承的概念
当有派生类有间接基类时,为了使间接基类的成员变量在派生类中只有一份拷贝,可以通过虚继承达到目的。
格式
class Father{
//...
};
class Son:virtual public Father{
//...
};
其中,被继承的基类通常称为虚基类。
用途实例
解决间接的基类成员变量在派生类中有多份拷贝的问题
#include<iostream>
using namespace std;
class Grandfather { //创建间接基类
public:
Grandfather(string treasure_a);
protected:
string _treasure_a;
};
//类外实现间接基类Grandfather的构造函数
Grandfather::Grandfather(string treasure_a) {
_treasure_a = treasure_a;
}
//Father类,虚继承Grandfather类
class Father :virtual public Grandfather {
public:
Father(string treasure_b, string treasure_a);
protected:
string _treasure_b;
};
//类外实现Father类的构造函数
Father::Father(string treasure_b, string treasure_a) :
Grandfather(treasure_a)
{
_treasure_b = treasure_b;
}
//Mother类,虚继承Grandfather类
class Mother :virtual public Grandfather {
public:
Mother(string treasure_c, string treasure_a);
protected:
string _treasure_c;
};
//类外实现Mother类的构造函数
Mother::Mother(string treasure_c, string treasure_a) :
Grandfather(treasure_a)
{
_treasure_c = treasure_c;
}
//创建派生类,继承Father类和Mother类
class Son:public Father,public Mother {
public:
Son(string treasure_a, string treasure_b, string treasure_c);
void getTreasure();
};
//类外实现Son类的构造函数
Son::Son(string treasure_a, string treasure_b, string treasure_c) :
Father(treasure_b,treasure_a), Mother(treasure_c, treasure_a),Grandfather(treasure_a) {
}
void Son::getTreasure() {
cout << "祖父的珍宝:" << _treasure_a <<endl;
cout << "爸爸的珍宝:" << _treasure_b << endl;
cout << "妈妈的珍宝:" << _treasure_c << endl;
}
int main() {
Son s("古书","武功秘籍","金钗"); //创建派生类对象
s.getTreasure();
return 0;
}
编译运行结果
祖父的珍宝:古书
爸爸的珍宝:武功秘籍
妈妈的珍宝:金钗
虚继承的原理
在虚继承中,每个虚继承的派生类都会在派生类对象的顶部增加一个虚基类指针vbptr,vbptr指针指向一个虚基类表vbtable(不占用类的存储空间),虚基类表中记录了基类成员变量相对于vbprt指针的偏移量,根据偏移量可以找到基类成员变量。当虚基类的派生类被当作当作基类继承时,虚基类指针vbptr也会被继承。