title: 多态与虚函数
date: 2020-09-14 19:59:42
tags: c++
categories: c++
多态指一个实体具有多种形态,是OOP的一个重要特性多态分为静态多态和动态多态。
-
静态多态:函数地址早绑定,编译阶段确定函数地址。函数重载和运算符重载都属于静态多态。
-
动态多态:函数地址晚绑定,运行阶段确定函数地址。使用派生类和虚函数实现动态多态。
1、多态入门
下面看一个例子
#include <iostream>
using namespace std;
class Animal
{
public:
void speak(void)
{
cout<<"动物在说话"<<endl;
}
};
class cat:public Animal
{
public:
void speek(void)
{
cout<<"小猫在说话"<<endl;
}
};
class Human:public Animal
{
public:
void speek()
{
cout<<"人在说话"<<endl;
}
};
int main(int argc, char const *argv[])
{
Animal *p = new cat; //基类的指针可以指向子类的对象
p->speak();
p = new Human;
p->speak();
return 0;
}
//PS E:\vscodeworkspace> .\a.exe
//动物在说话
//动物在说话
上面的代码使用了基类的指针指向一个子类的对象,然后调用speak()
成员函数,输出的结果都是在执行基类的函数。而我们这段代码的用意是想通过基类指针指向不同对象时调用不同对象的成员函数。也就是在运行时根据不同的对象动态的决定执行哪个对象下的方法。显然上面的代码是不行的。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void speak(void) //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了
{
cout<<"动物在说话"<<endl;
}
void func(int)
{
cout<<" Animal func(int)"<<endl;
}
};
class cat:public Animal
{
public:
void speak(void) //派生类与基类中同名的函数,即重写函数前面可以不加virtual关键词,基类必须加
{
cout<<"小猫在说话"<<endl;
}
void func(string)
{
cout<<" cat func(string)"<<endl;
}
};
class Human:public Animal
{
public:
void speak() 派生类与基类中同名的函数,即重写函数前面可以不加virtual关键词,基类必须加
{
cout<<"人在说话"<<endl;
}
};
int main(int argc, char const *argv[])
{
Animal *p = new cat; //基类的指针指向子类的对象
p->speak(); //调用cat的speek()
p->func(5); //调用基类的fun(int)
//p->func("hello c++"); 出错,子类指针向父类转化时,导致子类中的一些属性丢失不能访问。
//只能访问从父类哪里继承过去的属性
p = new Human;
p->speak();
p->func(10);
return 0;
}
// PS E:\vscodeworkspace> .\a.exe
// 小猫在说话
// Animal func(int)
// 人在说话
// Animal func(int)
上面是改进版,实现了动态多态。将基类中的speak()前面加virtual关键词将其申请为虚函数,子类中的该方法可加可不加。就能实现父类的指针或引用根据不同的对象,调用不同的方法。
2、 多态产生的条件
1、有继承关系:在多态中必须存在有继承关系的子类和父类。基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。
2、重写父类的虚函数:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。重写:函数返回值类型 、函数名、参数列表 完全一致称为重写
3、父类的指针或引用指向子类的对象(向上转型):向上转型存在一些缺憾,那就是它必定会导致一些方法和属性的丢失,而导致我们不能够获取它们。所以父类类型的引用可以调用父类中定义的所有属性和方法,对于只存在与子类中的方法和属性它就望尘莫及了。
3、什么时候声明虚函数
首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
4、纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0
,表明此函数为纯虚函数。
只要类中有一个纯虚函数,这个类就成为抽象类。之所以叫抽象类是因为它无法实例化,即无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
#include <iostream>
using namespace std;
class base //抽象类:类中只要有一个纯虚函数,它一定是抽象类,即使它还有一个非纯虚函数
{
public:
virtual void fun(void) = 0; //纯虚函数,基类不实现,让派生类去实现
void fun1(void)
{
;
}
};
class son:public base
{
public:
void fun(void) //派生类中一定要对基类中的纯虚函数实现,不然它继承后还是抽象类
{
cout<<"son::fun()"<<endl;
}
};
int main(int argc, char const *argv[])
{
//base *p = new base; 抽象类不能实例化
base *p =new son;
p->fun();
delete p;
return 0;
}
5、虚析构和纯虚析构函数
-
虚析构:就析构函数为虚函数
-
为什么要虚析构函数:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
-
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
先看看下面示例:
#include <iostream> using namespace std; class base { public: base() { cout<<"基类构造函数"<<endl; } ~base() { cout<<"基类析构函数"<<endl; } }; class Son :public base { private: int *p; public: Son(int age) { cout<<"派生类构造调用"<<endl; p =new int(age); //构造函数中在堆区申请一块空间 } ~Son() { if(p != NULL) { cout<<"派生类析构调用"<<endl; delete p; p = NULL; } } }; int main(int argc, char const *argv[]) { base * b = new Son(10); delete b; return 0; } /* 输出如下: PS E:\vscodeworkspace> .\a.exe 基类构造函数 派生类构造调用 基类析构函数 */
在派生类Son类的构造函数中在堆区开辟了一个int型的数据。然后创建一个基类的指针指向派生类的对象,构成多态。最后通过基类指针释放派生类的对象。由程序的运行结果可看出,在使用基类指针释放派生类对象时并没有执行派生类的析构函数,这就导致派生类在堆区申请的内存没有被释放,会造成内存泄漏。怎么解决这个问题呢?将基类的析构函数改为虚析构或者纯虚析构函数,可以解决在使用多态时,父类的指针不能对子类在堆区申请的空间进行释放的问题,避免导致内存泄漏。
#include <iostream>
using namespace std;
class base
{
public:
base()
{
cout<<"基类构造函数"<<endl;
}
/********这是虚虚析构**********/
// virtual ~base()
// {
// cout<<"基类析构函数"<<endl;
// }
/*********这是纯虚析构**********/
virtual ~base() = 0; //包含了纯虚析构函数的类也是抽象类,无法实例化对象
};
base::~base()
{
cout<<"纯虚析构函数"<<endl; //为了展示基类的构造函数被调用,这里还是对纯虚函数做以实现
}
class Son :public base
{
private:
int *p;
public:
Son(int age)
{
cout<<"派生类构造调用"<<endl;
p =new int(age); //构造函数中在堆区申请一块空间
}
~Son()
{
if(p != NULL)
{
cout<<"派生类析构调用"<<endl;
delete p;
p = NULL;
}
}
};
int main(int argc, char const *argv[])
{
base * b = new Son(10);
delete b;
return 0;
}
//运行结果
PS E:\vscodeworkspace> .\a.exe
基类构造函数
派生类构造调用
派生类析构调用
纯虚析构函数
总结① 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象不干净的问题。② 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。③拥有纯虚析构函数的类也属于抽象类
6、 多态的实现原理、多态下的对象模型(虚函数表)
先看看下面几个类的占用内存情况。
当类中存在至少一个虚函数时,编译器会向类插入一个隐形的成员变量为虚函数表指针(vptr),虚函数表指针指向虚函数表。当类中存在虚函数时编译器在编译阶段就会为类生一个虚函数表(vtbl),虚函数表会一直伴随着该类。
下面是类中存在虚函数时类对象的模型
发生多态时类对象是怎样的呢?
当子类继承父类时会将父类的所有属性都继承下来,包括虚函数表中的各个成员。子类再添加上自己的属性就构成了自己的对象模型。子类对父类中的虚函数实现的话,虚函数表中的函数地址就会替换子类中函数的地址。如果虚函数没被实现,就还是父类中虚函数的地址。
借助vs工具查看
当发生多态时,即父类指针或引用指向子类对象时。父类的指针会根据不同的对象,去不同对象的虚函数表中寻找要执行的函数。