预习和提问
什么是多态? / 为什么使用多态?
什么是虚函数?
特点,注意?
什么是虚函数表?
纯虚函数和抽象类又是什么?
面试怎么问?
虚函数使用方法
定义
函数返回类型之前使用virtual
只在成员函数的声明中添加,实现中不需要
继承
某个成员函数被声明为虚函数,那么它的子类,以及子类的子类中,所继承的这个成员函数,也自动是虚函数
子类中重写这个虚函数,可以不用在写virtual,但建议写,可读性更好
多态性的解释
(1)体现在具有继承关系的父类和子类之间.子类重新定义(覆盖/重写)父类的成员函数,同时父类和子类中又把这个函数声明为了virtual函数
(2)通过父类的指针,只有到了程序运行时期,根据具体执行到的代码行,才能找到动态绑定到父类指针上的对象(new的是哪个),这个对象是某个子类对象,或者父类对象,系统内部实际上是要查类的虚函数表,根据虚函数表找到函数的入口地址,从而调用父类或者子类的虚函数
理解多态
简单说就是想要实现多态这种效果, 就需要使用虚函数
多态理解为一种效果,形式,而虚函数是实际功能,手段,机制
示例
Main
#include "Son.h"
#include "Father.h"
#include <iostream>
int main()
{
Father father;
Son son;
Father* tmp;
tmp = &father;
tmp->play(); //Father---play()
tmp = &son;
tmp->play(); //Son---play()
return 0;
}
Father.h
#pragma once
#include <iostream>
using namespace std;
class Father
{
public:
virtual void play();
};
Father.cpp
#include "Father.h"
void Father::play()
{
cout << "Father---play()"<< endl;
}
Son.h
#include "Son.h"
void Son::play()
{
cout << "Son---play()" << endl;
}
Son.cpp
#pragma once
#include "Father.h"
class Son : public Father
{
public:
void play();
};
示例中是写死的一个&father或&son,实际项目中可以根据用户的输入,实现过程中的各种状态来创建对象,根据调用时的指针
根据执行时子类对象进来就调用子类方法,父类对象进来就调用父类方法
总结:形式上,原本调用父类的方法,但是,实际上会调用子类的同名方法
纯虚函数和抽象类
纯虚函数:在基类中声明的虚函数,但是他在基类中没有函数体,或者说没有实现,只有一个声明,但它要求任何派生类都要定义该虚函数自己的实现方法
定义
在该虚函数的函数声明末尾的分号之前加"=0"
示例
Father.cpp
//此示例继续上面代码
class Father
{
public:
virtual void play2() = 0; //声明了一个纯虚函数
};
Main.cpp
int main()
{
//Father father; //报错 不允许使用抽象类类型 "Father" 的对象
//Father* father = new Father; //报错,函数体都没有,肯定报错
//Son* son = new Father; //同样也是不允许的
//Father* father = new Son; //必须在子类中定义这个纯虚函数的函数体,否则报错
//不允许使用抽象类类型 "Son" 的对象:
return 0;
}
注意
(1)纯虚函数没有函数体,只有函数声明
(2)一旦一个类中有纯虚函数,那就不能生成这个类的对象了(有纯虚函数的类就叫抽象类)
(3)抽象类不能用来生成对象,主要目的是用来统一管理子类对象;换句话说,就是主要用于当做父类来生成子类用的.
(4)子类中必须实现该父类中的纯虚函数,那上面例子说就是,定义了一个play2纯虚函数,必须在子类中定义play2函数体,如下
class Son : public Father
{
public:
void play2(){};
//Father* father = new Son //main.cpp中,这一行就不会报错了
};
父类的析构一般写成虚函数
先说结论:用基类指针new子类的对象,在delete的时候系统不会调用子类的析构函数
示例
//main
Son son;
Son* tmp_son = new Son;
delete tmp_son;
到这里,还都是正常释放,看下面示例
Father* tmp_father = new Son;
delete tmp_father; //没有执行子类的析构函数
但是在用父类new子类对象的时候,没有释放"Son"的析构,这段示例可以自己尝试一下.
这肯定就有问题了,只删除了一半
解决
把父类的析构写成虚函数
virtual ~Father();
完美解决这个问题,只需要在父类的析构上加了一个virtual ,这个问题就不存在了,子类的析构成功执行
结论
(1)delete tmp_father时,肯定是要调用的父类的析构函数, 但是父类析构函数中,要想调用子类(Son)的析构函数,父类(Father)的析构就要声明成virtual, 也就是说C++中为了获得运行时的多态行为,所调用的成员函数必须得是virtual的.
因为虚函数特性,只有虚函数才能做到用父类指针调用子类的虚函数
(2)父类中的析构函数的虚属性会继承给子类,这意味着子类的析构函数也是虚析构,就算不用virtual修饰也可.虽然名字不同
面试常问
为什么父类的析构一定要写成虚函数?
只有这样,当delete一个指向子类对象的父类指针时,才能保证系统能够依次调用子类的析构函数和父类的析构函数,从而保证对象(父类指针指向子对象)的内存被正确释放
多态的原理?
多态是用虚函数表实现的。
有虚函数的类都会生成一个虚函数表,这个表在编译时生成。
动态绑定与静态绑定?
静态绑定是程序编译时确定程序行为。
动态绑定是程序运行时根据具体的对象确定程序行为。
静态成员可以是虚函数吗?
不能, 因为静态成员函数没有this指针, 因为有this指针才能访问到虚表指针,有虚表指针才能找到虚表从而调用实际应该调用的函数。
构造函数可以是虚函数吗?/虚函数指针在什么时候生成的的?
不能,因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的
对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的,如果是指针对象或者是引用对象,则调用的普通函数快,因为普通对象在编译时就确定地址了,虚函数构成多态,运行时调用虚函数需要到虚函数表中去查找
注:
对象布局,对象模型,虚函数表,后续会用大篇幅补充…