1.多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
案例:
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal)
{
animal.speak();
}
//
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}
int main() {
test01();
system("pause");
return 0;
}
基类动物类里有一个speak函数,函数前面加了virtua成了虚函数,子类猫和狗都继承了动物类并对speak函数进行了重写(子类和父类中的虚函数拥有相同的名字,返回值,参数列表)。
父类的指针或者引用指向子类:
Animal & animal = cat
Animal & animal = dog
animal.speak();会产生不同的效果,就构成了多态
多态满足条件
-
有继承关系
-
子类重写父类中的虚函数
多态使用条件
-
父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
2.多态的实现原理
一开始动物类的speak函数没加virtual的时候,动物类的大小为空,sizeof为1,因为speak函数不算在类的大小里面,加上virtual之后,动物类的大小变为了4,因为动物类的内部多了个vfptr指针大小为4,该指针指向了虚函数表,虚函数表中装的是动物类的speak函数的地址。
猫类对动物类进行了继承,同时也继承下来了动物类的虚函数指针和虚函数表,因为猫类对动物类的speak进行了重写,所以虚函数表中speak的地址替换成了猫自己的speak地址。
当父类指针或引用指向子类时,会通过虚函数表自动找到对应的地址。
3.纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为==抽象类==
抽象类特点:
-
无法实例化对象
-
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
int main() {
test01();
system("pause");
return 0;
}
4.虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
-
可以解决父类指针释放子类对象
-
都需要有具体的函数实现
虚析构和纯虚析构区别:
-
如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
class Animal {
public:
Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;
//析构函数加上virtual关键字,变成虚析构函数
//virtual ~Animal()
//{
// cout << "Animal虚析构函数调用!" << endl;
//}
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用!" << endl;
}
//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
class Cat : public Animal {
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
void test01()
{
Animal *animal = new Cat("Tom");
animal->Speak();
//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
//怎么解决?给基类增加一个虚析构函数
//虚析构函数就是用来解决通过父类指针释放子类对象
delete animal;
}
int main() {
test01();
system("pause");
return 0;
}
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
多态的情况下若不使用虚析构的话,走不到子类的析构函数,若子类在堆区开辟空间,会无法进行回收,此时就需要让父类的析构函数变为虚析构或纯虚析构,这样才能使子类的析构函数运行,进行回收内存空间,避免造成内存泄漏。
例子:
#include<iostream>
using namespace std;
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
当我们使用new
,申请了一块子类空间,而我们想用父类指针去指向这块子类空间,我们像上述代码一样,不重写析构函数,那么下方的delete p2
将是错误的,运行结果如下
而当我们完成子类的析构函数的重写(在父子类的析构函数前加上virtual),结果才是正确的
知识点:
(new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。)
父类指针 new 子类对象
会调用父类和子类的构造函数
delete回收父类指针
如果父类是虚析构,回收顺序:先调用子类析构,再调用父类析构
父类不是虚析构,就只调用父类析构,不走子类析构
虚函数
一、虚函数(Virtual Function)
1.1 定义和作用
虚函数是在基类中使用关键字 virtual 声明的成员函数,它允许派生类对其进行重写(Override),实现运行时多态。当通过基类指针或引用调用虚函数时,实际调用的是对象类型对应的派生类中的函数,这个过程称为动态绑定(Dynamic Binding)或晚绑定(Late Binding)。
1.2 实现原理
虚函数的实现原理基于虚函数表(Virtual Table,简称VTable)。每个使用虚函数的类都有一个虚函数表,该表是一个函数指针数组,存储了指向类的虚函数的指针。类的每个实例都包含一个指向其虚函数表的指针(vptr),通过这个指针可以找到并调用正确的虚函数实现。
当派生类覆盖(重写)基类的虚函数时,派生类的虚函数表中相应位置的函数指针会被更新为指向派生类中的函数。如果派生类没有重写虚函数,则派生类的虚函数表中会保留指向基类虚函数的指针。
C++虚函数的底层实现原理详解_虚函数底层实现原理-CSDN博客
基类如果存在虚函数,那么在子类对象中,除了成员函数与成员变量外,编译器会自动生成一个指向**该类的虚函数表(这里是类ClassB)**的指针,叫作虚函数表指针。通过虚函数表指针,父类指针即可调用该虚函数表中所有的虚函数。
C++多态虚函数表详解(多重继承、多继承情况)_一个类有几个虚函数表-CSDN博客
首先不考虑继承的情况。如果一个类中有虚函数,那么该类就有一个虚函数表
只要基类有虚函数,子类不论实现或没实现,都有虚函数表。
基类的虚函数表和子类的虚函数表不是同一个表。
多继承下会有多个虚函数指针 多个虚函数表
父类指针指向子类对象实现多态,将子类的地址赋值给父类指针,地址会是同一个,父类指针只会访问父亲自己里面有的,不会访问子类额外添加的东西
构造函数是否可以是虚函数
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
自己理解:虚函数表对应的是虚函数指针,构造函数如果是虚函数的话,就需要用虚函数指针去找虚函数表,而虚函数指针是对象里的,但是对象还没有实例化,就没有虚函数指针,无法找到虚函数表。。
虚函数表是在编译期间建立的
只读数据段:常量区