本博客将记录:类的相关知识点的第8节的笔记!
(这个在学习C++基础课程时已经学习过一次了,这里再次简单地回顾一下而已)
今天总结的知识分为以下5个点:
一、基类(父类)指针、派生类(子类)指针
二、虚函数(并补充:override 和 final关键字)
三、多态性
四、纯虚函数
五、多态基类的析构函数一般写成虚函数(虚析构函数) very Important !!!
这个第五点其实就算:为多态基类声明virtual析构函数。(From Effective C++ Term 07)
一、基类(父类)指针、派生类(子类)指针:
我们平时创建对象时,不只是可以创建对应的对象实体,还可以在堆区创建指向该对象的指针。
请看以下代码:(仍然以3.7小节中我所写的Human类和Man类作为我举代码的例子)
Human* phuman = new Human();//调用Human类的无参构造函数创建父类指针
Man* pman = new Man();//调用Man类的无参构造函数创建子类指针
(当然,由于Man继承自Human,因此会先调用Human类的构造函数创建子类对象中的父类成分,再调用子类的构造函数创建子类对象中的子类成分!)
运行结果:
下面,我将介绍一种新的"玩法",叫做多态。
多态:用父类指针new一个子类的对象。
请看以下代码:
Human* p = new Man();//父类指针可以指向子类对象
/*Man* pp = new Human;*/ //子类指针无法指向父类的对象
当父类指针new一个子类对象时,有2个注意事项:
①类的多态性是在继承性的基础上去do事情的!
(你首先得do个继承的操作对吧?不然你没有继承的话何来父类指针?又何来的子类对象呢对吧?学习的时候多问问为什么?多钻研一些小细节,学习C++这种细节知识点非常非常多的主流语言时更要如此。)
②该父类指针可以调用父类的成员函数,不可以调用子类的成员函数
(既然你父类指针没办法访问到子类的成员函数,那你为什么还要让父类的指针去new一个子类的对象呢?这有啥用呢?不要着急!对于这个问题的解答将在下面虚函数的讲解中一一解开哈!)
(因为你是父类的指针而不是子类的指针)
//在Human父类中添加成员函数funcHuman()
void funcHuman() { cout << "this is funcHuman()函数的调用!" << endl; }
//在Man子类中添加成员函数funcMan()
void funcMan() { cout << "this is funcMan()函数的调用!" << endl; }
//在main.cpp中尝试用指向子类对象的父类指针调用父类的成员函数以及子类的成员函数
p->funcHuman();//✔!用父类指针调用父类Human的成员函数
p->funcMan();//❌!因为用父类指针是不可以调用子类Man的成员函数的,因为没有访问权限!
运行结果:
而把 p->funcMan();这行代码注释掉就可以成功编译通过了!
运行接结果:
二、虚函数(并补充:override 和 final关键字):
所谓虚函数,就是在想要让子类重写父类中的同名的函数的声明前加virtual关键字的函数。本质作用:就是借助virtual关键字,让子类去重写与父类中同名的函数,这样的函数就叫做是虚函数。以使得当用该指针调用该同名函数时会指向子类的重写之后的同名函数而不是指向父类中的同名函数。
请看以下代码:
Human.h(为了学习方便起见,删去上面这个父类定义中不必要的代码)
#ifndef __HUMAN_H__//这是头文件的防卫声明
#define __HUMAN_H__
#include
#include
using namespace std;
//声明基类/父类/超类
class Human {
public:
Human(){ cout << "调用默认的Human无参构造函数!" << endl;}
~Human() { cout << "调用默认的~Human析构函数!" << endl;}
void eat() { cout << "人类要吃饭!" << endl; }
};
#endif __HUMAN_H__
Woman.h
#ifndef __WOMAN_H__
#define __WOMAN_H__
#include
#include"Human.h"
using namespace std;
//子类/派生类
class Woman : public Human {
public:
Woman() { cout << "调用Woman类的构造函数!" << endl; }
~Woman() { cout << "调用Woman类的析构函数!" << endl; }
void eat() { cout << "女性也要吃饭!" << endl;}
};
#endif __WOMAN_H__
main.cpp
Human* p = new Woman();//用父类指针指向子类对象
p->eat();
delete p;
运行结果:
我们从这里就可以看出来,虽然说父类和子类的函数是一样的,但是父类指针始终只可以调用父类的eat()成员函数,而不能调用子类的成员函数,这就是在上面我讲解多态这个知识点时给你留下来的小疑问。那么现在我就来给出正确的答案:当你使用多态时,要想让该指向子类对象的父类指针能够调用子类的成员函数or成员变量的话,就必须使用virtual关键字!
废话不多说,用virtual关键字让子类重写其与父类中的同名函数,以达到上述目的!
请看以下代码:
//将Human父类中的void eat() 函数改写为:
virtual Human::void eat() { cout << "人类要吃饭!" << endl;}
//将Woman子类中的void eat() 函数改写为:
virtual Woman::void eat() { cout << "女性也要吃饭!" << endl;}
//其实也就是分别在子类和父类的重名函数前加上virtual关键字
运行结果:
从这里我们就可以看出,此时的父类指针能够调用子类的成员函数了!这也就是多态的目的!
虚函数的注意事项:
①一旦父类中的某个函数被声明为虚函数时,那么继承该父类的所有的子类中的该函数就都变成虚函数了!
(虽然说你只需要在父类中的该函数声明前加上virtual就可以do到上述的目的,而不需要all的子类都在该函数声明前加virtual,但为了方便他人和自己维护所写的代码,你写虚函数的话就最好在父类及其all的子类中把该函数都声明virtual的)
②注意昂,我们是在需要重写的同名函数的声明前加virtual,在定义处就不需要加了
(若你把所写的类都做分文件编写的话,也即分为.h头文件和.cpp源文件来编写时,就按照我这么说的处理就不会出错了!)
请看以下代码:
Human.h
#ifndef __HUMAN_H__//这是头文件的防卫声明
#define __HUMAN_H__
#include
#include
using namespace std;
//声明基类/父类/超类
class Human {
public:
Human();
~Human();
//声明时加virtual
virtual void eat();
};
#endif __HUMAN_H__
Human.cpp
#include"Human.h"
Human::Human() { cout << "调用默认的Human无参构造函数!" << endl;}
Human::~Human() { cout << "调用默认的~Human析构函数!" << endl; }
//定义(具体实现)时就不需要加virtual了
void Human::eat() { cout << "人类要吃饭!" << endl; }
Woman.h
#ifndef __WOMAN_H__
#define __WOMAN_H__
#include
#include"Human.h"
using namespace std;
class Woman : public Human {
public:
Woman();{ cout << "调用Woman类的构造函数!" << endl; }
~Woman(); { cout << "调用Woman类的析构函数!" << endl; }
//声明时需要加virtual了
virtual void eat();
(当然,由于在父类中已经加了virtual把eat函数声明为虚函数了,子类你也可以不加virtual)
(但是本质上因为你继承了刚才的父类,因此你这个函数其实也是一个虚函数,只不过是隐式的虚函数而已)
};
#endif __WOMAN_H__
Woman.cpp
#include"Woman.h"
Woman::Woman();{ cout << "调用Woman类的构造函数!" << endl; }
Woman::~Woman(); { cout << "调用Woman类的析构函数!" << endl; }
//定义时就不需要加virtual了
void Woman::eat() { cout << "女性也要吃饭!" << endl; }
Man.h
#ifndef __MAN_H__//这是头文件的防卫声明
#define __MAN_H__
#include
#include"Human.h"
using namespace std;
//声明子类/派生类
class Man : public Human {//public公有继承自Human
public:
Man();
~Man();
virtual void eat();
};
#endif __MAN_H__
Man.cpp
#include"Man.h"
Man::Man() { cout << "调用默认的Man无参构造函数!" << endl; }
Man::~Man() { cout << "调用默认的~Man析构函数!" << endl; }
void Man::eat() { cout << "男性也要吃饭!" << endl;}
main.cpp
Human* p1 = new Man(); //调用的是Man类的eat虚函数 显示:男性也要吃饭!
p1->eat();
//当然,你就算用了虚函数来do事情,我依然有办法访问到父类的eat()函数
p1->Human::eat();
delete p1;
Human* p2 = new Woman(); //调用的是Woman类的eat虚函数 显示:女性也要吃饭!
p2->eat();
//当然,你就算用了虚函数来do事情,我依然有办法访问到父类的eat()函数
p2->Human::eat();
delete p2;
Human* p3 = new Human();//调用的是Human类的eat虚函数 显示:人类要吃饭!
p3->eat();
delete p3;
运行结果:
③我们需要在子类中重写的virtual同名函数必须与其在父类中的该函数的原型声明完全一样(除了函数体,因为父类中该虚函数的实现大多数时候与子类中该函数的实现是不一样的,并且,我们使用多态和虚函数这种技术就是为了调用子类重写的虚函数do事情。因此后面我们会引入到纯虚函数,用纯虚函数实现的父类和子类才是标准的继承多态代码的写法。)包括返回值类型、函数参数以及函数名都得一毛一样!(override关键字的引入)
请看以下代码:
//Human父类中
virtual void eat();
//Woman子类中
virtual void eat();
//父类和子类中的虚函数的声明是完全一致的!
如果不一致,比如:
//Human父类中
virtual void eat();
//Woman子类中
virtual void eat(int abc);
那么后面你在main.cpp中写下:
Human * p = new Woman;
p->eat();//这个还是只会调用回父类中的 void eat()函数
p->eat(1);//这个才会调用子类中的 virtual void eat(int abc)函数
//你可以自己写几行代码验证一下,这里我就不多赘述了!
我们在父类和子类中写虚函数时,一般都要求父类中虚函数是怎样声明的,那么你子类中的虚函数就要怎样去声明。那么为了防止你在写虚函数时出现如上的错误代码,C++11中引入了override关键字!
override:顾名思义,就是让你子类重写的虚函数凌驾于父类之上,完全覆盖掉父类中的该虚函数的实现的意思!
注意实现:
①override关键字专用于虚函数的声明中!
②override关键字就是来给你写的虚函数纠错用的!
(你的虚函数一旦写地与父类的不同,override就可以给你提示报错!)
以上面的代码为例子:一旦你写虚函数时用了这个override关键字,那么编译器就会认为你子类中的eat()是覆盖掉了父类中的eat()虚函数的!
废话不多说,请看以下代码:
//在上述的Woman.cpp中写下:
virtual void eat() override;//这样就可以防止你把要声明为虚函数的函数的声明在子类中弄错了!
与之相对地,C++11中也引入了final关键字!
final:顾名思义,就是让你的父类中的虚函数变成最后一个虚函数,也即没有任何的子类可以重写该声明为final的虚函数。
废话不多说,请看以下代码:
//一旦我在上述的Human.h中写
virtual void eat()final;
//用final关键字来声明这个我要在其子类中进行重写的虚函数时
//此时其子类Man和Woman中重写的该eat()虚函数就会报错!
//Man
virtual void eat() override;//❌!
//Woman
virtual void eat() override;//❌!
运行结果:
阐述了这么多,相信你也有点头晕,这里我再总结升华一下让你对这个知识点感到清晰一点。
调用虚函数执行的是“动态绑定”,这也是多态的精髓所在!
动态:表示的是在我们程序运行时才能够知道调用了哪一个子类中的eat()虚函数。
绑定:表示的是父类的指针p到底动态地绑定到Man类,还是Woman类,取决于你new的是一个Man类对象还是一个Woman类对象。
动态绑定:程序运行时才决定你的父类指针p所调用的函数该绑定到哪一个类中去
当然,虚函数你要用父类指针指向子类对象来进行操作,你不能直接用子类对象来操作。因为如果直接用某个类的对象来调用其虚函数,那就和你用该类对象调用其普通成员函数没有任何区别了!(此时你就根本没必要再用多态来写代码了!)
Man man;
man.eat();//调用的肯定是Man类的eat()函数
Woman woman;
woman.eat();//调用的肯定是Woman类的eat()函数
Human human;
human.eat();//调用的肯定是Human类的eat()函数
三、多态性:多态性是面试时经常考察的一个知识点。
多态性:只是针对虚函数来说的,面向对象编程的思想里就有一个“多态性”。就体现在具有继承关系的父类和子类之间,子类重新定义(重写)父类的成员函数eat(),同时父类把这个eat()函数声明为了virtual虚函数。此时,当程序运行时,编译器就会通过父类的指针,找到动态绑定到父类指针上的对象,这个对象既可能为某个子类对象,也可能为父类对象。然后,编译器的内部实际上是会去查询一个叫做虚函数表的virtualTable,从而找到函数eat()的入口地址,进而去调用父类or子类的eat()函数,这就是程序运行时期的多态性。
具体的多态性代码在第二个点我详解虚函数时就已经给出,如果你看到这里还不熟悉的话大可以翻阅我上面的内容,这里就不多赘述了!
四、纯虚函数:
所谓的纯虚函数,也是在基类中声明的虚函数。只不过声明为纯虚函数的话就等价于直接让基类的虚函数变成空实现的意思。
你其实还可以理解为:纯虚函数是基类中没有具体实现代码的虚函数,只是一个声明而已,但是,一旦你这么声明后,继承基类的all的子类与基类中同名的函数都必须具有他们自己这些虚函数的具体函数实现。
声明成员函数为纯虚函数的格式:(直接在虚函数的声明最后加一个 = 0;)
virtual 函数返回值类型 函数名(参数表) = 0;
请看以下代码:
//把上述的Human基类中的eat函数声明为纯虚函数,且删除掉Human.cpp中的eat的函数定义
virtual void eat() = 0;//纯虚函数,没有定义,只有声明!
//这其实就相当于==>
virtual void eat() {/*空实现*/}//虚函数,有定义(即便定义为空实现也要有{}括号括着),有声明!
main.cpp
Human* p1 = new Man();
p1->eat();
delete p1;
Human* p2 = new Woman();
p2->eat();
delete p2;
//Human* p3 = new Human();//报错!
//因为一旦把基类中的函数声明为纯虚函数后,该基类就变成了抽象类
//而抽象类,顾名思义就算很抽象的,是失去一般化的,不是给我们随意用的,一般都只适合让基类do为抽象类
//这样我们就不能创建抽象类的对象ornew一个抽象类的指针了!
//p3->eat();
//delete p3;
运行结果:
当然,纯虚函数只需要记住两个注意事项即可:
①一旦某个基类中的某个成员函数声明为纯虚函数之后,那么这个基类就会变成一个抽象类。而在C++中是不允许你自己去创建抽象类的实体的(包括直接创建抽象类的对象以及new一个该抽象类的指针)。(含有纯虚成员函数的类叫做抽象类,不能用来生成该类的对象,主要是拿来当作基类统一管理or生成子类 用的!也即写子类用的!)
Human *pHuman = new Human;//❌!new一个抽象类的指针 不合法!
Human human;//❌!直接创建一个抽象类的对象 不合法!
②子类中必须要实现该基类中声明的纯虚函数,否则就会报错说:无法解析的外部命令
五、多态基类的析构函数一般写成虚函数(虚析构函数)
也即:为多态基类声明virtual析构函数。(From Effective C++ Term 07)
为什么我们要将多态了的基类的析构函数声明为虚析构呢?下面我就通过代码来带大家学习这个很重要的知识点!
请看以下代码:(仍然是以上述写的Human类,Woman类,Man类作为例子)
①把main.cpp中的test()函数中的代码改成:
#include
#include"Human.h"
#include"Man.h"
#include"Woman.h"
using namespace std;
void test() {
Human hm;
Man man;
Woman woman;
}
int main(){
test();
system("pause");
return 0;
}
运行结果:
② 再把test()函数中的代码改成:
void test() {
Human* hm = new Human;
delete hm;
cout << "------------------------" << endl;
Man* man = new Man;
delete man;
cout << "------------------------" << endl;
Woman* woman = new Woman;
delete woman;
}
运行结果:
③再把test()函数中的代码改成:
void test() {
Human* p1 = new Man;//多态 基类指针指向子类对象
delete p1;
cout << "------------------------" << endl;
Human* p2 = new Woman;//多态 基类指针指向子类对象
delete p2;
}
运行结果:
相信大家看到这第③份代码时,已经看出来有点不对劲了!至于怎么不对劲呢?
大家有没有发现,现在我们用多态写new的代码时,执行程序后是不是少了一次调用子类的析构函数的代码呢?也即没有执行子类的析构函数的意思,这样就坏事了!
从上述的运行结果,我们可以看出,这里在销毁指针时,不论是p1还是p2都只是执行了一半的工作,只是销毁了这2个指针中的基类成分,而没有销毁其子类成分,只销毁了一半的工作量,那这样必然是会造成内存的泄露的!
对,这就是我们要引入在多态基类中将析构函数声明为虚析构函数这个重要的代码规范的原因!
为了deal上述这个内存泄露的大问题,现在,我们就直接将Human这个基类中的析构函数声明为虚析构函数!这样在多态时,基类指针可以去调用子类中的析构函数,进而销毁其子类成分了!
//只需要在Human.h中将其析构函数声明之前加一个virtual关键字即可将其变成虚析构函数
virtual ~Human();
此时,当我再次运行以上第③份代码案例时,就会得出非常符合我们要求(不造成内存的泄露!)的运行结果:
其实这个知识点本质上还是考察前面我详细讲解的虚函数的概念,因为当你用多态这个知识点去写子类时,如果说你基类的析构函数不是虚函数的话,那么子类中就没法去覆盖(重写)基类中的析构函数,那当程序运行时,将基类指针动态绑定到子类对象上并且在该指针需要释放时,也就无法去调用到子类的析构函数,将子类的成分释放掉了!这一定是会造成内存上的泄露的!(万一你子类在heap堆区申请了很多空间的话,那肯定是会造成非常多内存泄露的问题,因为你基类不是虚析构的话,没法在多态代码使用完delete对应的父类指针时释放这些子类申请的heap空间!)因此,我们以后但凡是写多态的代码,就必须要将基类的析构函数写为虚析构函数!
(简单的加一个小小的virtual关键字,就可以做到避免内存泄漏的大问题!)
一句话,如果一个类想要做基类的话,我们务必要把这个类的析构函数写为virtual析构函数!这句话务必要牢记!!!
(否则你不这样写的代码必然是会发生内存泄漏的!长时间运行必然导致程序的崩溃!但只要基类的析构函数是虚函数,就能够保证我们在使用完多态后,delete基类指针时能运行正确的析构函数版本)
当然,对于一个普通的类(不做基类的类),我们可以不把其析构函数写为virtual的!
总结一下:
①在public继承(因为我们大多数时候都是用的public继承的)中,基类对子类及其对象的操作,只能影响到那些从基类继承下来的成员。
②如果想要在多态时,用基类的指针对子类的成员函数做(针对子类成员本身的)一些读写操作时,就必须要把该对应的函数写为虚函数(也即加上virtual关键字)。特别的,如果说基类的析构函数是虚析构的话,那么子类继承自基类后,子类的析构函数也会变成虚函数!
③其实,基类中的析构函数的虚属性也会被继承给子类,so子类中的析构函数也自然而言地成为了虚函数,虽然子类的析构函数名与基类的不一样,但是也不妨碍其继承基类后也变成一个虚的析构函数。
④虚函数在我们写多态时的有诸多好处,但也有其弊端。就算写虚函数会增加内存的开销。也即你在类中写virtual的函数,编译器就会给这个类增加一个虚函数表virtualTable,这个表中就存放了指向虚函数的指针。
好,那么以上就是这一3.8小节我所回顾的内容的学习笔记,希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~