C++继承篇
1.继承的基本概念
被继承的类叫做基类也叫做父类/超类,从其他类继承而来的类叫做派生类也叫做子类。
子类中不仅继承了父类的中的数据成员,也继承了父类的成员函数。
实例化子类时,构造和析构函数的调用:
在子类实例化对象的时候,会先隐式的调用父类的构造函数;在子类对象析构之后,也会隐式的调用父类的析构函数。
举个例子,假如有父类A,子类B,在其构造函数和析构函数中都输出一个标记语句,在这种情况下,如果我们创建一个子类B的对象,那么可以得到以下输出:
// 创建子类B的对象,构造函数和析构函数的调用顺序
A(),This is the constructor
B(),This is the constructor
~B(),This is the destructor
~A(),This is the destructor
2.继承方式-访问限定符
继承时一定别忘了写访问限定符!如果不写,默认是private私有继承。
继承的使用:
分类 | 写法 |
---|---|
公有继承 | class A:public B |
保护继承 | class A:protected B |
私有继承 | class A:private B |
继承方式和对应的访问属性(权限):
继承方式 | 基(父)类成员访问属性 | 派生类访问属性 |
---|---|---|
public | public | public |
protected | protected | |
private | 无法访问 | |
protected | public | protected |
protected | protected | |
private | 无法访问 | |
private | public | private |
protected | private | |
private | 无法访问 |
这个表格我们每一行从左往右看,拿public
继承方法来说明:
- 如果父类中某成员为
public
,子类通过public
方式将其继承,该成员的访问权限不变依然为public
。 - 如果父类中某成员为
protected
,子类通过public
方式将其继承,该成员的访问权限变为protected
。 - 如果父类中某成员为
private
,子类通过public
方式将其继承,该成员的访问权限变为 不可访问。
小总结:
B类从A类派生,那么B类是A类的子类,A类是B类的超类。
B类从A类派生,那么B类中含有A类的所有数据成员。
B类从A类公共派生,那么可以通过B类的对象调用到A类的 所有成员函数 public及protected限定符下的成员函数。
B类从A类公共派生,那么可以在B类中直接使用A的public及protected的数据成员。
B类从A类公共派生,那么A类的公共成员函数成为B类的公共成员函数。
B类从A类公共派生,那么A类的保护成员函数成为B类的保护成员函数。
B类从A类公共派生,那么A类的私有成员函数 成为B类的私有成员函数 不能被B类继承并使用。
B类从A类私有派生,那么A类的公共成员函数成为B类的 公共成员函数 私有成员函数。
B类从A类保护派生,那么A类的公共成员函数成为B类的保护成员函数。
B类从A类保护派生,那么A类的保护成员函数成为B类的保护成员函数。
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
/**
* 定义人的类: Person
* 数据成员姓名: m_strName
* 成员函数: eat()
*/
class Person
{
public:
string m_strName;
void eat()
{
cout << "eat" << endl;
}
};
/**
* 定义士兵类: Soldier
* 士兵类公有继承人类: public
* 数据成员编号: m_strCode
* 成员函数: attack()
*/
class Soldier:public Person
{
public:
string m_strCode;
void attack()
{
cout << "fire!!!" << endl;
}
};
int main(void)
{
// 创建Soldier对象
Soldier so;
// 给对象属性赋值
so.m_strName = "Jim";
so.m_strCode = "592";
// 打印对象属性值
cout << so.m_strName << endl;
cout << so.m_strCode << endl;
// 调用对象方法
so.eat();
so.attack();
return 0;
}
3.继承中同名成员的隐藏(父类与子类的继承关系中)
当父类和子类中出现同名成员时,使用子类的对象调用这个同名成员,你会发现只调用了子类中的,而父类中的同名成员,仿佛被隐藏了一样,你无法直接调用,这就是隐藏。
隐藏不仅适用于成员函数,还适用于其它对象成员。
当然,也是有办法可以访问父类的同名成员的,那就是使用父类名::
,看下面的例子。
例1,访问同名函数的方法:
// Person为Soldier的父类
int main(){
Soldier so;
so.play(); // 调用子类中的play()
so.Person::play(); // 调用父类中的play(),Person是父类的类名
return 0;
}
例2,访问同名属性的方法:
class Persion{
public:
void play();
protected:
string code; // 父类code变量
};
class Soldier:public Person{
public:
void play();
void work();
protected:
int code; // 子类code变量
}
void Soldier::work(){
code = 1234; // 调用子类的code(就是当前的这个类)
Person::code = "5678"; // 调用父类的code(被当前类继承的类)
}
隐藏与重载:
如果需要调用父类中的同名成员,一定要使用父类名::
。当然,你会觉得这很像重载,所以可能会尝试将父子类中的同名函数利用参数进行区分。然而这当然是不行的,隐藏和重载是不同的概念,父子类中的同名成员函数是无法通过函数的参数进行区分的,如果不使用父类名::
,就无法调用父类中的同名成员函数。
4.isA语法-将子类赋值给父类
**子类(派生类)是可以赋值给父类(基类)**的。
如果这个时候对
// 人是父类,士兵类是子类
int main(){
// 正确
Soldier s1;
Person p1 = s1;
Person *p2 = &s1;
// 错误
s1 = p1;
Soldier *s2 = &p1;
return 0;
}
存储结构:
-
如果将子类对象 初始化/赋值 给父类:
子类中属性的数据会赋值给父类的属性,父类没有的属性,会被截断(即丢失),只赋值父类有的属性的值(也就是从父类继承来的那些,非子类独有的部分)。
对于父类来说,它只能接收自己拥有的数据成员。
// 初始化: Soldier so; Person p = so; // 赋值: Soldier so; Person p; p = so;
[外链图片转存失败(img-JOKQMFNu-1563352548404)(./images/muke_C++/003.png)]
-
如果用父类指针来访问子类对象,那么也只能访问到父类所拥有的数据成员,而无法访问到子类拥有的数据成员和成员函数。
[外链图片转存失败(img-ZBHD3rs6-1563352548405)(images/muke_C++/004.png)]
isA的常见用法:
既然子类可以赋给父类,那么我们就可以用父类作为形参来接收子类。
void fun1(Person *p){// 指针
...
}
void fun2(Person &p){// 引用
...
}
int main(){
Person p1;
Soldier s1;
fun1(&p1); fun2(p1);
fun1(&s1); fun2(s1);
return 0;
}
需要使用 虚析构函数 的情况:
当存在继承关系,我们使用父类的指针,去指向堆中的子类的对象,并且我们还想使用父类的指针去释放这块内存,这个时候我们就需要 虚析构函数。
为什么?因为在这种情况下之后调用父类的析构函数,而子类的析构函数不会被释放,这样就会造成内容泄露的问题(在return 0;
前打断点才能看出,因为程序一旦return退出,所有的内存都会释放)。
这种情况的具体表现请看下面的代码:
类声明:
// Person类声明
class Person {
public:
Person(string name = "Person001");// 构造函数赋初值
~Person();// 析构函数
void play();
protected:
string m_strName;
};
// Soldier类声明
class Soldier: public Person {
public:
Soldier(string name = "soldier001",int age = 20);// 构造函数赋初值
~Soldier();// 析构函数
void work();
protected:
int m_iAge;
};
类实现:
// Person类实现
Person::Person(string name){
m_strName = name;
cout << "Person()" << endl;
}
Person::~Person(){
cout << "~Person()" << endl;
}
void Person::play(){
cout << "Person -- play()" << endl;
cout << "m_strName = " << m_strName << endl;
}
// Soldier类实现
Soldier::Soldier(string name, int age){
// 将初始值赋值给对象
m_strName = name;
m_iAge = age;
cout << "Solidier()" << endl;
}
Soldier::~Soldier(){
cout << "~Solidier()" << endl;
}
void Soldier::work(){
cout << "m_strName = " << m_strName << endl;
cout << "m_iAge = " << m_iAge << endl;
cout << "Soldier -- work()" << endl;
}
mian.c:
int main() {
Soldier so;
Person *p = &so;
cout<< "==="<< endl;
p->play();
cout<< "==="<< endl;
delete p;
p = nullptr;
return 0;
}
/** 输出:
Person()
Solidier()
===
Person -- play()
m_strName = soldier001
===
~Person()
*/
这里我们使用指针(堆,需要手动释放)来进行资源的释放,可以观察到,最后并没有调用~Solidier()
,这样会有内存泄露的问题。
如何解决这个问题,就是使用虚析构函数,这里要使用修饰符virtual
。
虚析构函数是可以继承的,父类中写了,即使子类中的不写,也是虚析构函数。但是建议全写上,起提示作用,这很重要!
// 拿本例来说明
// 在Person和Solidier类的声明中,在析构函数前面加virtual
// 在实现部分不需要加virtual
转载自:virtual 在哪些情况要用?
个人总结,virtual当前出现的三种地方:
虚析构函数:当父类指针指向子类对象时,释放内存时,若不定义virtual,则仅释放父类内存。
虚继承:防止多继承和多重继承时,一个父类被继承多次,造成内存空间的浪费。
虚函数:当父类指针指向子类对象时,父类指针可以指向子类方法。
附加例子,在实验时发现的,关于子类对象相互赋值与拷贝构造函数的调用:
如果在子类写一个没有赋值操作的拷贝构造函数,然后用子类对象01给子类对象02赋值,会发现对象02中的属性如果是继承于父类的,属性的值就是父类中赋的初始值,如果是子类中添加的,则值为空。代码如下:
关于拷贝构造函数的内容,在笔记第2篇“封装篇(上)”。
类声明:
// Person类
class Person {
public:
// 构造函数赋初值
Person(string name = "Person001");
Person(const Person& pe);
protected:
string m_strName;
};
// Soldier类
class Soldier: public Person {
public:
// 构造函数赋初值
Soldier(string name = "soldier001",int age = 20);
Soldier(const Soldier& soso);
protected:
int m_iAge;
};
类实现:
// Person类
Person::Person(string name){
m_strName = name;
cout << "Person()" << endl;
}
Person::Person(const Person& pe){
cout << "Copy Person()" << endl;
}
// Soldier类
Soldier::Soldier(string name, int age){
// 将初始值赋值给对象
m_strName = name;
m_iAge = age;
cout << "Solidier()" << endl;
}
Soldier::Soldier(const Soldier& soso){
cout << "Copy Solidier()" << endl;
}
void Soldier::work(){
cout << "m_strName = " << m_strName << endl;
cout << "m_iAge = " << m_iAge << endl;
cout << "Soldier -- work()" << endl;
}
main.c
int main() {
Soldier so;
Soldier so1 = so;
cout << "====" << endl;
so1.work();
cout << "====" << endl;
return 0;
}
/* 输出:
Person()
Solidier()
Person()
Copy Solidier()
====
m_strName = Person001
m_iAge = 0
Soldier -- work()
====
*/
这里来看下输出:
Person()
和Solidier()
是因为so
这个对象 的创建,在调用子类的构造函数前会先调用父类的构造函数。- 之后的
Person()
和Copy Solidier()
是因为so1
的创建,但是这里Person这个类并没有调用拷贝构造函数,而是调用了构造函数,也就是在这里赋上了初始值。Solidier类则是调用了拷贝构造函数。 m_strName = Person001
和m_iAge = 0
,因为我们手写了Solidier类的拷贝构造函数,其中又没有赋值操作,所以这里m_strName
的值是调用Person构造函数时的初始值,而m_iAge
是Solidier类中的属性,所以没有被赋值,输出就是0
。
如果这里用自动生成的拷贝构造函数(没有手写拷贝构造函数的时候就会自动生成),则so对象中属性的值是会赋值到so1对象的属性中的。
5.多重继承和多继承
多重继承:
当B类从A类派生,C类从B类派生,形成一条继承链,此时成为多重继承。
举个多重继承的例子:
步兵类继承士兵类,士兵类继承人类。
[外链图片转存失败(img-PFOCt8QK-1563352548407)(images/muke_C++/005.png)]
class Person{
};
class Soldier:public Person{
};
class Infantryman:public Soldier{
};
多继承:
多继承是指一个子类继承多个父类。多继承对父类的个数没有限制,继承方式可以是公共继承、保护继承和私有继承。
再举个多继承的例子:
农民工同时继承工人类和农民类,工人类和农民类是平行关系。
[外链图片转存失败(img-nE3H2F6A-1563352548408)(images/muke_C++/006.png)]
[外链图片转存失败(img-F3mlAChm-1563352548410)(images/muke_C++/007.png)]
class Worker{
};
class Farmer{
};
class MigrantWorker:public Worker,public Farmer{
};
多继承 下的调用初始化列表使用:
这里的继承关系同上,这里只列出构造函数的代码:
// 类的实现部分代码,这里分别是Farmer、Worker和MigrantWorker的构造函数
Farmer::Farmer(string name){
m_strName = name;
}
Worker::Worker(string code){
m_strName = code;
}
// 通过MigrantWorker的构造函数,调用Farmer和Worker的构造函数,同时将name和code的值传递过去
MigrantWorker::MigrantWorker(string name,string code):Farmer(name),Worker(code){
}
6.虚继承与菱形继承(环状继承)
虚继承的使用:
在继承的访问限定符之前添加virtual
修饰符。
菱形继承
在菱形继承中,类A是类B的父类,类A是类C的父类,类B和类C都是类D的父类,is-A的关系如下:
[外链图片转存失败(img-BddrsI6o-1563352548411)(images/muke_C++/008.png)]
可见,菱形继承中是存在 多重继承 和 多继承 的。
(多重继承:类D继承类B,类B继承类A;多继承:类D继承类B,也继承类C)
菱形继承的数据冗余问题:
这样的继承中会存在类D中存在有2份类A的数据,我们需要使用虚继承来解决,看下面的例子:
[外链图片转存失败(img-hFtdMrvw-1563352548412)(images/muke_C++/009.png)]
// 在类的声明中:
// 之后会被农民工继承,所以这个类是虚基类(父类)
class Worker:virtual public Person{
}
class Farmer:virtual public Person{
}
// 继承上面2个虚基类
class MigrantWorker:public Worker, public Farmer{
}
解决类的重定义问题:
在菱形继承中,类的重定义是一定会发生的,我们可以在发生重定义的内部写一段宏来避免重定义。
#ifndef XXX
#define XXX
// 在这里写:类的声明
#endif
本篇为视频教程笔记,视频如下: