多态是C++面向对象三大特性之一
多态分为两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名动态多态: 派生类和虚函数实现运行时多态 静态多态和动态多态区别:静态多态的函数地址早绑定 - 编译阶段确定函数地址动态多态的函数地址晚绑定 - 运行阶段确定函数地址 静态多态比较简单,如果想要对静态多态中的函数重载了解, 可以参考这篇博文: C++函数提高通过一个例子来了解一下动态多态。
#include <iostream> #include <string> using namespace std; class Animal { public: virtual void speak() { cout << "动物叫叫叫" << endl; } }; class Cat :public Animal { public: void speak() { cout<<"小猫喵喵喵" <<endl; } }; void test01(Animal &animal) { animal.speak(); } int main() { Cat cat; test01(cat); system("pause"); return 0; }如果我们想要使用多态技术,前提得要构造一个多态。
需要构造继承关系父类一定要定义虚函数子类一定要重写重写:函数返回值类型 函数名 参数列表 完全一致称为重写 子类中重写函数virtual标识符可写可不写
我们在使用多态之前,一定要构造,这是非常重要的。 如果你已经对多态进行了构造,那么多态的使用就变的很简单了。 父类引用或指针指向子类对象
我们先把父类中的虚函数中标识符virtual给去掉,看看Animal类在内存中占多少个字节,以及显示的是哪个作用域下的speak函数。
#include <iostream> #include <string> using namespace std; class Animal { public: void speak() { cout << "动物叫叫叫" << endl; } }; class Cat :public Animal { public: void speak() { cout<<"小猫喵喵喵" <<endl; } }; void test01(Animal &animal) { animal.speak(); } int main() { Cat cat; test01(cat); printf("%d", sizeof(Animal)); system("pause"); return 0; }我们可以看到,当把virtual标识符去掉以后,调用的speak是Animal作用域下的speak,且此时对象在内存中所占一个字节
空类在内存中占一个字节
现在我们将virtual标识符 给加上,我们再看下效果。
#include <iostream> #include <string> using namespace std; class Animal { public: virtual void speak() { cout << "动物叫叫叫" << endl; } }; class Cat :public Animal { public: void speak() { cout<<"小猫喵喵喵" <<endl; } }; void test01(Animal &animal) { animal.speak(); } int main() { Cat cat; test01(cat); printf("%d", sizeof(Animal)); system("pause"); return 0; }仅仅只是在函数的前面加上了virtual标识符,结果却大不相同。 现在调用的speak函数是在Cat作用域下的,并且此时对象在内存中所占内存的大小为4个字节。
对于去掉virtual时,对象在内存中所占字节数为1,这个很简单,以为此类该类为空类,因为空类在内存中所占字节数为1。那为什么加上virtual时,对象在内存中所占字节数为4呢,字节为4,大家脑子里应该想的都是指针吧。 没错,就是指针,这个指针的名字是vfptr,也就是虚函数指针,这个指针指向的是虚函数表,而在这虚函数表中,就存放了虚函数的地址。 因此,就是通过这个vfptr来调用子类或父类作用域下的函数的。
一个类中,无论虚函数有多少个,只要没有成员属性,那么sizeof of 的结果就是4.
现在,我们来看看我说的到底对不对。
打开VS2019开发者命令行工具(每个版本都有)切换到源文件所在路径cl /dl reportSingleClassLayout类名 "源文件名.cpp"cd :切换目录 dir :查看目录下的成员
首先看看,如果没有virtual表示符,Animal和Cat类是什么样子的。 我们发现,此时Animal类所占字节确实是为1. 然后我们看下Cat类的情况。 因为Cat的父类是Animal,所以Cat继承了Animal的诸多属性,因此Cat类在内存中所在字节数为1。 那现在我们看下加上virtual标识符,结果又会是怎么样 这个时候,Animal类所在字节为4,也就是上图中vfptr占4个字节,vfptr中存储着vftable的首地址,也就是说vfptr指向vftable,而在这个vftable中,存储着&Animal::speak 函数的地址。
因此,通过vfptr就可以调用子类中的成员函数
那我们再来看看Cat类的情况怎么样,你可以猜猜,因为Cat类是Animal类在子类,所以Cat会继承Animal的成员属性,也就是vfptr中的内容和Animal中vfptr中的内容相同。并且vftable都指向&Animal::speak函数。 哦,好像不对,虽然Cat类中也有一个vfptr,指向Cat::vftable,但是vftable中存储的内容并不是&Animal::speak,而是&Cat::speak。为什么呢? 因为,当子类对父类中的虚函数重写的时候,会对vftable中的内容进行重写,把存储&Animal::speak的存储块中的内容重新写,写进去的内容为&Cat::speak。 这样的话,当父类的引用或指针指向子类对象时,如果父类中没有添加virtual,那么这个时候就会调用父类中的speak函数,如果加上virtual,那么就会调用子类中的speak。又因为同一个父类可以有多个子类,因此指向不同子类的对象时,调用不同子类作用域下的函数。 这就是博文开篇所提到的,在运行阶段确定函数的地址,因为不同的子类对象,地址是不一样的。
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容 因此可以将虚函数改为纯虚函数 纯虚函数语法: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; }多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,这样就会造成内存泄漏 解决方式:将父类中的析构函数改为虚析构或者纯虚析构 虚析构和纯虚析构共性:
可以解决父类指针释放子类对象都需要有具体的函数实现 虚析构和纯虚析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象只要类中存在一个纯虚函数,那么该类就是是抽象类,无法实例化对象。
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
#include <iostream> #include <string> using namespace std; class Animal { public: Animal() { cout << "Animal 构造函数调用!" << endl; } virtual void Speak() = 0; //析构函数加上virtual关键字,变成虚析构函数 //virtual ~Animal() //{ // cout << "Animal虚析构函数调用!" << endl; //} ~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); } 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; }我们知道,当定义一个子类时,是先调用父类构造函数,再调用子类构造函数,析构函数的调用方式相反。
我们运行上述代码,发现调用不了子类中的析构代码,这就会造成内存泄漏。而如果我们将父类中的析构函数改为虚析构函数的话,那么这个问题就可以解决了。 但是我们要注意,虚析构函数是要有主体的,不能和虚函数那样不需要函数主体。
#include <iostream> #include <string> using namespace std; 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); } 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; } #include <iostream> #include <string> using namespace std; 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); } 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; }我们发现,这个时候Cat析构函数被调用了,就不会造成内存泄漏。
注意:虚析构函数或纯虚析构函数一定要有函数体,这点和虚函数不一样
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
本篇博文对多态有了个大概的了解,可能再日后的学习过程当中会有更深地理解,届时再对多态进行深入的解析。