一、父类指针和子类指针
对象是可以new出来的。
Human* phuman = new Human(); //没有问题
Men *pmen = new Men; //没有问题
第十五课学习了子类概念后,又遇到了新的new对象的方法--父类指针可以new一个子类对象:
Human* phuman2 = new Men(); //正常(父类指针new一个子类对象)
但是反过来就不行,子类指针new一个父类对象是不可以的:
Men *pmen2 = new Human;
通过上面的测试说明,父类指针很强大,不仅可以指向父类对象,也可以指向子类对象。
现在再来做一个测试,在Human.h文件的Human父类定义中,增加一个用public修饰的成员函数定义(函数体保持为空即可):
void funchuman(){ }
再在Men.h文件的Men子类定义中,增加一个用public修饰的成员函数定义:
void funcmen(){ }
在main主函数中,增加如下代码:
phuman2->funchuman();//可以,父类类型,可以调用父类的成员函数
phuman2->funcmen(); //不可以,虽然new的是子类对象,但是phuman2是父类指针,无法调用子类成员函数
疑问:既然父类指针没有办法调用子类的成员函数,那为什么还允许父类指针new(指向)一个子类对象,有什么作用?下面来看看这个问题。
二、虚函数
现在,再来定义一个Human类的子类,名字为Women.专门创建Women.h和Women.cpp文件来声明和定义这个类。
在Women.h文件中,内容如下:
#pragma once
#ifndef __WOMEN_H__
#define __WOMEN_H__
#include <iostream>
#include <vector>
#include"Human.h"
class Women :public Human
{
public:
Women();//构造函数声明
};
#endif // !__WOMEN__
Women.cpp文件中,内容如下:
#include "Women.h"
Women::Women()
{
}
现在,在父类Human和两个子类Men、Women的定义中,都加入如下的用public修饰的同名成员函数eat定义(都加在各自定义的.h头文件中):
void eat(){ }
然后完善eat成员函数,增加一些输出语句,当该函数被调用的时候可以输出一些信息。
在Human类(Human.h)中,完善的eat成员函数定义如下:
void eat()
{
cout<<"人类吃各种粮食"<<endl;
}
在Men类(Men.h)中,完善的eat成员函数定义如下:
void eat()
{
std::cout << "男人喜欢吃米饭" << std::endl;
}
在Women类(Women.h)中,完善的eat成员函数定义如下:
void eat()
{
std::cout << "女人喜欢吃面食" << std::endl;
}
在main主函数中,代码定义如下:
Human *phuman = new Human;
phuman->eat();// 调用父类的eat成员函数
从上面这行代码可以看到,调用的是Human类的成员函数eat,因为phuman是Human类指针,而new的时候new的也是Human对象,怎么样才能调用Men和Women类中的eat成员函数。
父类指针能调用子类同名函数的条件:
(1)如果eat函数声明和定义分开写的情况下,在声明父类的eat同名函数时,在函数前面加virtual关键字修饰,将该成员函数声明为虚函数,父类的eat函数定义前面不需要加virtual。如果在Human.h头文件中的父类声明里面直接定义eat函数,则直接在eat函数直接加virtual,如下所示,
virtual void eat()
{
std::cout << "人类吃各种粮食" << std::endl;
}
(2)如果两个子类的eat函数声明和定义分开写的情况下,在声明两个子类(Men和Women)的eat同名函数时,在函数前面加virtual关键字修饰,将该成员函数声明为虚函数,两个子类的eat函数定义前面不需要加virtual。如果在两个子类的.h头文件中直接定义eat成员函数,则直接在eat成员函数之前加virtual(其实子类的eat函数加不加virtual都可以,但是建议加上),如下所示:
virtual void eat()
{
std::cout << "男人喜欢吃米饭" << std::endl;
}
virtual void eat()
{
std::cout << "女人喜欢吃面食" << std::endl;
}
(3)在main主函数创建父类指针,指向子类的对象,如下所示,
Human* phuman = new Human;
phuman->eat();
delete phuman;
phuman = new Men;
phuman->eat();
delete phuman;
phuman = new Women;
phuman->eat();
delete phuman;
运行结果如下所示:
可以看到,
虽然指针是父类指针,但是指针指向哪个类的对象,则调用的就是哪个类的eat成员函数
现在又有一个问题:如果phuman指向一个子类对象时,能否实现phuman调用父类的eat成员函数。答案是可以的,在main主函数中如下定义:
Human* phuman = new Men;
phuman->Human::eat();
delete phuman;
phuman = new Women;
phuman->Human::eat();
delete phuman;
运行结果如下所示:
为了避免在子类中写错虚函数,在C++11中,可以在函数声明所在行的末尾增加一个override关键字,特别注意的是,这个关键字是用在子类中,而且是虚函数专用,修改子类Men.h和Women.h的相关类声明中的eat成员函数(成员函数定义中不需要加):
virtual void eat() override;
override的作用:
这个关键字主要用来说明派生类中的虚函数,用了这个关键字后,编译器就会认为子类中的eat成员函数覆盖了父类中的同名的虚成员函数(virtual),那么编译器就会在父类中找同名同参的虚成员函数,如果父类中没有这个虚成员函数,编译器就会报错。如果不小心在子类中把虚函数写错了名字或者写错了参数,编译器会帮助开发者找出错误,方便开发者的修改。
final的作用:
与override关键字相对的还有一个final关键字,final关键字也是必须用于虚函数和父类中的。如果在父类虚函数声明的末尾加上final关键字,那么在任何子类中尝试覆盖该成员函数的操作都将引发错误。
假如在父类Human.h文件的Human类声明中将eat成员函数的声明这样修改:
virtual void eat() final;
那么在Men类和Women类中的eat成员函数的声明语句都会引发编译错误:
virtual void eat() override;
另外,子类的虚函数返回类型一般也和所要覆盖父类的虚函数返回类型一样,也可以有点小差别:
(1)例如有一个父类Action,它有一个子类名字为Action_Sub。
(2)如果Human父类中有一个虚函数over,返回的类型是Action *,代码如下:
virtual Action *ovr()
{
return NULL;
};
(3)那么子类Men或者Women中,对应的虚函数可以返回Action *类型,也可以返回Action_Sub*类型,如下两种写法的代码都可以:
virtual Action_Sub *ovr()
{
return NULL;
};
virtual Action *ovr()
{
return NULL;
};
virtual关键字修饰虚函数总结:
(1)用父类的指针调 一个虚成员函数时,执行的是动态绑定的eat函数。什么叫动态绑定呢?所谓动态,表示的就是在程序运行的时候(运行到调用 eat 函数这行代码时)才能 知道词=调用了哪个子类的eat函数(虚成员函数))。读者知道,一个函数如果不去调用,编码时可以只写该函数的声明部分不写该函数的定义部分。但是虚函数 ,因为是在程序运行的时候才知道调用了哪个虚函数,所以虚函数必须写它的定义部分(以备编译器随时使用随时就存在) 杏则会编译出错.
可以看到,程序运行的时候,作为父类的指针phuman ,如果 new 的是Men子类对象(也叫实例),那么调用的eat函数就是Men类的虚函数eat,如果 new 的是 Woman 子类对象,那么调用的eat函数就是Woman类的虚函数eat,这就叫动态绑定--运行的时候(根据new的是哪个类的对象)才决定 phuman调用哪个 eat 函数。
(2)如果不是用phman父类类型指针,而是用普通对象来调用虚函数,那虚函数的作用就体现不出来了,因为这就不需要运行时(根据 new 的是哪个类的对象)决定绑定哪个eat函数,而是在编译的时候就能确定。看如下代码:
Men men;
men.eat(); //调用的就是Men的eat函数
Women women;
women.eat();//调用的就是women的eat函数
Human human;
human.eat();//调用的就是human的eat函数
三、多态性
多态分为静态多态和动态多态。静态多态往往是通过函数重载和模版(泛型编程)来实现的,静态多态是指编译期间就可以确定函数的调用地址,并生产代码。动态多态只是针对继承和虚函数说的,非虚函数不存在动态多态的说法。
动态多态的解释:
(1)体现在具有继承关系的父类与子类之间。子类重新定义(覆盖/重写)父类的成员函数,同时父类和子类中又把这个成员函数声明为了virtual虚函数。
(2)通过父类指针,只有到了程序运行时期,根据具体执行到的代码行,才能找到动态绑定到父类指针上的对象(new 的是哪个具体的对象),这个对象有可能是某个子类对象,也有可能是父类对象,而后,系统内部实际上是要查类的“虚函数表”,根据虚函数表找到函数的入口地址,从而调用父类或者子类的虚成员函数,这就是运行时期的多态性。
四、纯虚函数与抽象类
1、纯虚函数
就算是没有子类,也可以使用虚函数,而且,如果子类中不需要自有版本的虚函数,可以不在子类中声明和实现该虚函数。如果子类中没有定义该虚函数,则调用该虚函数时,调用的当然时父类中的虚函数。
纯虚函数是在父类中声明的虚函数,它在父类中没有函数体(仅有声明,没有定义进行实现),要求任何子类都要自己定义该虚函数并实现。父类中实现纯虚函数的方法是在函数原型后面加“=0”,或者可以说成是在该虚函数的函数声明末尾的分号之前增加“=0”。
为了方便演示,需呀做以下两件事:
(1)在Human.h和Human类声明中,在eat虚函数之后加上”=0”,代码如下:
virtual void eat()=0;
(2)然后把Human.cpp中的eat函数实现屏蔽掉。
然后在main函数中,这样创建Human新对象:
Human human1;//报错,含纯虚函数的类不允许创建对象
Human human2 = new Human;//报错,含纯虚函数的类不允许创建对象
说明:一个类中一旦有了纯虚函数,那么就不能生成这个类的对象。
2、抽象类
这种带有纯虚函数的类就叫做抽象类,抽象类不能用来生成对象。主要目的是统一管理子类(或者说建立一些供子类参照的标准或规范)。
需要记住几点:
(1)含有纯虚函数的类就叫做抽象类。抽象类不能用来生成对象,主要当作父类用来生成子类。
(2)子类中必须要实现父类(抽象类)中声明的纯虚函数,否则就没法用该子类创建子类对象--创建对象编译器就会报错。
疑问:为什么需要抽象类,每个子类都实现自己的eat接口不就可以了吗?
答:如果是这样,那就不能实现多态功能。
需要记住:
(1)动态多态的实现是:父类指针指向子类对象。如果没有父类,也就不存在动态多态。
Human *phuman = new Men;
phuman->eat();
(2)纯虚函数也是虚函数,因此是支持动态多态的。
五、父类的析构函数一般写成虚函数
在Human.h文件的Human类定义中已经有了默认构造函数的声明,在Human.cpp中已经有了默认构造函数的实现。
在Human.h文件的Human类中定义增加析构函数的声明:
public:
^Human();
在Human.cpp文件文件中增加析构函数的实现代码:
Human::~Human()
{
cout<<"执行了Human::~Human()析构函数"<<endl;
}
在Men.h文件的Men类声明中增加析构函数的声明:
public:
~Men();
在Men.cpp文件中增加析构函数的实现代码:
Men::~Men()
{
cout<<"执行了Men::~Men()析构函数"<<endl;
}
继续在Women.h文件的Women类声明中增加析构函数的声明:
public:
~Women();
在Women.cpp文件中增加析构函数的实现代码:
Women::~Women()
{
cout<<"执行了Women::~Women()析构函数"<<endl;
}
完善一下Women.cpp中Women构造函数的实现代码----增加一条输出语句如下:
Women::Women()
{
std::cout << "执行了Women::Women()构造函数" << std::endl;
}
===========================测试一:=============================
在main主函数中,增加如下代码:
Men men;
程序运行结果如下:
可以看到:
当定义一个子类对象时,先执行的是父类的构造函数体,再执行子类的构造函数体,当对象超出作用域范围被系统回收时,先执行的是子类的析构函数体,再执行父类的析构函数体。
===========================测试二:=============================
在main主函数中,增加如下代码:
Men *pmen=new Men;
程序运行结果如下所示:
可以看到:
用new 创建子类对象,先执行父类构造函数,再执行子类的构造函数。
因为创建对象时是通过new来创建,所以需要用delete来释放内存,在刚刚的new语句下面添加delete语句如下:
Men* pmen = new Men;
delete pmen;
运行结果如下:
可以看到:
delete用new创建的子类对象,先执行子类析构函数,再执行父类析构函数。
=============================测试三:=============================
在main主函数中增加如下代码:
Human* phuman = new Men;
delete phuman;
运行结果如下:
可以看到:
执行delete phuman;后只调用了父类析构函数,没有调用子类的析构函数。
问题:如果在子类Men的构造函数中new了一块内存,并且我们在子类析构函数中进行delete这块内存,但是系统没有调用到析构函数,那就会导致内存泄漏。
问题解决:
只需要把父类Human的析构函数声明为虚函数即可,其它代码不需要修改。
再次运行程序,结果如下:
可以看到,执行delete phuman;后,子类的析构函数和父类的析构函数都被调用了。
经典总结:
(1)如果想要把一个类作为父类,务必要把这个类的析构函数写成virtual析构函数。只要父类的析构函数是虚函数,就能够保证delete父类指针时能够调用正确的析构函数。
(2)普通的类可以不写析构函数,但是如果是一个父类(有子类的类),则必须写一个析构函数,并且这个析构函数必须是一个虚析构函数(否则会出现内存泄漏)。
(3)虚函数(虚析构函数也是虚函数的一种)会增加内存和执行率上的开销,类里面定义虚函数,编译器就会给这个类增加虚函数表,在这个表里存放虚函数地址信息。
(4)读者将来在寻找C++开发工作时,遇到面试官考核诸如“为什么父类(基类)的析构函数一定要写成虚函数”的问题时,简而言之的答案是:只有这样,当delete一个指向子类对象的父类指针时,才能保证系统能够依次调用子类的析构函数和父类的析构函数,从而保证子类对象(父类指针指向的子对象)内存被正确的释放。
2022.07.11结。