目录
1.虚函数的概念
2.虚函数的定义
3.虚函数的作用
4.用虚函数实现多态的方法
5.动态绑定和静态绑定
6.纯虚函数和抽象类
7.虚析构和纯虚析构
在C++程序中我们经常可以看见关键字virtual来定义一个函数,在这里我们需要知道虚函数的概念:在某基类中声明为virtual并且在它的一个或多个派生类中被重写的成员函数成为虚函数。
重写:(发生在继承类中)方法名称、参数类型和返回值类型完全相同。
class Base { //基类
public:
virtual void fun(); //格式 virtual + 成员函数说明
};
class Son :Base{ //派生类 格式 class 类名: 基类名
public:
//对基类的虚函数进行重写
virtual void fun(); //格式 virtual + 成员函数说明
};
在C++中,虚函数是实现多态性的主要手段之一。多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,这样就可以用同一个函数名调用不同内容的函数。换言之,可以用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。
在基类中定义一个虚函数,它的派生类继承该虚函数并且重写该函数,对于不同派生类的对象接收同一个信息,调用相同的函数名,但是执行的操作不同(执行的为各派生类中重写的函数),这样就利用虚函数实现了多态。
代码详解
#include
using namespace std;
class Base { //基类
public:
virtual void fun() //格式 virtual + 成员函数说明
{
cout << "Base-fun()" << endl;
}
};
class Son1 :public Base{ //派生类 格式 class 类名: 基类名
public:
//对基类的虚函数进行重写
virtual void fun() //格式 virtual + 成员函数说明
{
cout << "Son1-fun()" << endl;
}
};
class Son2 :public Base { //派生类 格式 class 类名: 基类名
public:
//对基类的虚函数进行重写
virtual void fun() //格式 virtual + 成员函数说明
{
cout << "Son2-fun()" << endl;
}
};
void test1()
{
Base* p1 = new Son1;//一个基类指针p1指向新建的派生类Son1对象
Base* p2 = new Son2;//一个基类指针p2指向新建的派生类Son2对象
p1->fun();
p2->fun();
delete p1; delete p2;
}
int main()
{
test1();
}
在本例中,Base中的虚函数fun()被派生类Son1和Son2所继承,在每一个派生类的定义中,fun()函数都被重写。在main函数里定义两个基类指针p1和p2,分别指向new出的不同派生类的对象。
然后当我们通过基类指针调用fun()函数时,基类指针指向的是哪一个派生类的对象就执行该类下对应的fun()函数。即执行的是Son1::fun()和Son2::fun()。
其实在派生类中重新定义基类的虚函数时不再需要virtual,读者可以自行实践。
理解C++中动态绑定和静态绑定的区别可以帮助我们更好的理解多态性。
静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
c++中,我们在使用基类的引用(指针)调用虚函数时,就会发生动态绑定。所谓动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。在上述例子中已经有体现。
并且只有采用“指针->函数()”或“引用变量. 函数()”的方式调用虚函数才会执行动态绑定。
代码详解
(基类和派生类的定义与前图相同,不再赘述)
void test2()
{
Base obj_base;
Son1 obj_son1;
Base* p_1 = &obj_son1;//指针方式
Base& p_2 = obj_son1;//引用方式
obj_base.fun(); //静态绑定:调用对象本身(基类Base对象)的fun()
obj_son1.fun(); //静态绑定:调用对象本身(派生类Son1对象)的fun()
p_1->fun(); //动态绑定:调用被引用对象所属类(派生类Son1)的fun()
p_2.fun(); //动态绑定:调用被引用对象所属类(派生类Son1)的fun()
}
int main()
{
test2();
}
在C++中,许多情况下,在基类中不能对虚函数给出有意义的实现,故给它说明为纯虚函数,它的实现由该基类的派生类去完成(即重写该函数)。带有纯虚函数的类称为抽象类。
纯虚函数的定义
virtual <类型><函数名>(<参数表>)=0;
class Base { //基类
public:
/*虚函数*/
//virtual void fun() //格式 virtual + 成员函数说明
//{
// cout << "Base-fun()" << endl;
//}
/*纯虚函数*/
virtual void fun() = 0;
};
当基类中出现有纯虚函数之后,该类便被称为抽象类,抽象类具有一个特点:无法实例化对象
当Base类是抽象类之后如果我们写一行代码 Base b;希望创建一个Base类的对象,编译器便会报错(VS2019):E0322 不允许使用抽象类类型 "Base" 的对象:
基类的派生类中如果没有对纯虚函数进行重写,则该派生类也无法实例化对象。只有派生类对该纯虚函数进行重写操作后,该派生类便能实例化对象。
/*基类Base中有纯虚函数*/
void test3()
{
Base obj_base2; //错误 基类是抽象类,不允许使用抽象类类型"Base"的对象
/*如果派生类Son1中没有对纯虚函数进行重写*/
Son1 obj_son2; //错误 Son1是抽象类,不允许使用抽象类类型"Son1"的对象
/*派生类Son1中有对纯虚函数进行重写*/
Son1 obj_son3; //正确 Son1有对纯虚函数进行重写,可以实例化对象
}
纯虚函数和虚函数的最主要区别就是:是否可以实例化对象,其余操作大体相同。
什么时候需要用到虚析构?为什么要使用虚析构?
事实上我们在C++中使用虚析构是为了解决基类类指针释放子类对象不干净的问题
原因是基类的指针在析构时不会调用派生类中的析构函数,导致子类如果有堆区属性,会造成内存泄漏
下面用一组代码来分析
#include
using namespace std;
#include
//虚构和纯虚构
class Animal
{
public:
Animal()
{
cout << "Animal构造函数调用" << endl;
}
~Animal()//普通析构函数
{
cout << "Animal析构函数调用" << endl;
}
//virtual ~Animal()//虚析构函数
//{
// cout << "Animal析构函数调用" << endl;
//}
virtual void speak() = 0;//当类中有纯虚函数,类为抽象类
};
class Dog :public Animal
{
public:
Dog() { m_Name = NULL; }
Dog(string name)
{
cout << "Dog构造函数调用" << endl;
m_Name = new string(name); //在堆区开辟内存,最后需要回收
}
virtual void speak() //基类虚函数的重写
{
cout << *m_Name << "小狗在说话" << endl;
}
~Dog()
{
if (m_Name != NULL)
delete m_Name;//在析构函数中回收开辟的内存
cout << "Dog析构函数调用" << endl;
}
string* m_Name;
};
void test01()
{
Animal* dog = new Dog("PETTER");
dog->speak();
delete dog;
}
int main()
{
test01();
}
从程序的执行结果来看,我们调用了Animal1和Dog的构造函数,但是我们发现程序却只执行了基类Animal的析构函数,很显然我们派生类Dog的析构函数没有被执行。但是我们在派生类中为m_Name在堆区开辟了空间,只有执行了它的析构函数才能对这部分空间进行回收,否则便造成了内存泄漏。 那么怎么解决这个问题呢?很简单,我们只需要将基类中的析构函数前加上关键字virtual使得它变为虚析构函数,这时我们便发现程序便能执行派生类的析构函数了!成功对内存进行回收。
更改代码
//~Animal()//普通析构函数
//{
// cout << "Animal析构函数调用" << endl;
//}
virtual ~Animal()//虚析构函数
{
cout << "Animal析构函数调用" << endl;
}
现在我们可以执行派生类的析构函数啦!
从上面我们得知虚函数和纯虚函数的关系,那么我们也不难知道纯虚析构的定义
纯虚析构语法:virtual ~类名()=0;
类名::~类名(){}
与上面纯虚函数不同的是:纯虚函数需要有声明,同时也需要有实现过程。(虚函数只需要有声明)否则在编译过程便会报错:无法解析的外部符号。
class Animal
{
public:
virtual ~Animal() = 0;//纯虚析构的声明,必须有实现过程
};
Animal::~Animal()//Animal纯虚析构的实现代码
{
cout << "Animal纯虚析构函数调用" << endl;
}
值得一提的是,如果基类中含有纯虚析构函数,那么该类也是抽象类,无法实例化对象。
虚析构和纯虚析构的共性
1.都可以解决父类指针释放子类对象
2.都需要具体的函数实现
虚析构和纯虚析构的区别:
如果是纯虚析构,则该类属于抽象类,无法实例化对象。