1.虚函数
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
#include<iostream>
#include<string>
using namespace std;
class Flower
{
public:
virtual void display()//基类的虚函数,形成动态绑定,永远不会运行“动物”,Flower类也从1个字节变成了4个字节(指针)
//只有在程序运行的时候才知道speak函数具体运行哪个子类的speak函数
{
cout << "动物!" << endl;
}
};
class Rose:public Flower
{
public:
void display()//派生类重写了基类的虚函数,覆盖了原来虚函数表中的地址
{
cout << "玫瑰" << endl;
}
};
void fun(Flower &f1)//派生类的对象作为实参,形参可以用基类的引用或者指针来接收,目的在于实现多态性
{//实现了派生类对象向基类类型的转换
f1.display();//加上virtual之后,这一行就要去看你f1派生对象的类型,去调用相对应的display函数
}
int main() {
Rose r1;
fun(r1);
system("pause");
return 0;
}
下面通过编译器开发人员命令提示来说明多态的问题,当在基类没有virtual时,这时基类实际上是一个空类,只占1个字节。
当加上virtual之后,基类变成了4个字节
多了一个vfptr:virtual function pointer虚函数指针(指针4个字节),指向一个虚函数表vftable。
到了派生类,派生类如果没有重写虚函数,此时占4个字节,为从基类继承而来的vfptr,指向了哪个基类的vftable,这个是完完全全从基类继承过来的吧,如下图:
所以,当你在调用函数调用display的时候,指向的是Flower::display,但本意是要他输出rose的,所以在派生类重写了虚函数之后,再看:
这样,再调用的时候,就会调用Rose作用域下的display函数了。
由于基类中的虚函数一般不会被执行,那么就直接
virtual void display() = 0;
此时,这个虚函数被称为纯虚函数,这个Flower类就被称为抽象类
当是抽象类的时候,不能实例化对象,包括堆区和栈区,但可以定义一个类指针,比如:
Flower f1;//报错
new Flower;//报错
当一个抽象类作为派生类的父类时,派生类必须要重写这个纯虚函数,否则这个类也将会被定义为抽象类,不能被实例化
这个目的就是要实现多态,重写虚函数
通过一个父类指针,只想多个子类的对象,加强通用性。
2.虚析构和纯虚析构
先放上一段代码,不涉及虚析构和纯虚析构,只是我们要再此基础上添内容。
#include<iostream>
#include<string>
using namespace std;
class Flower
{
public:
Flower()
{
cout << "Flower的构造函数" << endl;
}
virtual void display() = 0;
~Flower()
{
cout << "Flower的析构函数" << endl;
}
};
class Rose:public Flower
{
public:
Rose()
{
cout << "Rose的构造函数" << endl;
}
void display()
{
cout << "玫瑰" << endl;
}
~Rose()
{
cout << "Rose的析构函数" << endl;
}
};
void fun()
{
Rose r1;
Flower* f1 = &r1;
f1->display();
}
int main() {
fun();
system("pause");
return 0;
}
运行结果:
比如说,派生类中有在堆区开辟的一段空间,那么按道理说,应该在派生类的析构函数中去释放,这个需要注意。
但还有一种情况,在调用函数中基类的指针或引用绑定的是堆区的派生类数据。如下所示:
#include<iostream>
#include<string>
using namespace std;
class Flower
{
public:
Flower()
{
cout << "Flower的构造函数" << endl;
}
virtual void display() = 0;
~Flower()
{
cout << "Flower的析构函数" << endl;
}
};
class Rose :public Flower
{
public:
Rose(string c)
{
color = c;
cout << "Rose的构造函数" << endl;
}
void display()
{
cout << "玫瑰" << endl;
}
~Rose()
{
cout << "Rose的析构函数" << endl;
}
string color;
};
void fun()
{
Flower* f1 = new Rose("blue");//在局部函数定义的f1,利用完就要释放
f1->display();}
int main() {
fun();
system("pause");
return 0;
}
如果你f1不释放,这时候,造成程序内存泄露,两个类始终都不会调用析构函数,直到程序结束。
进一步的,我如果注意到了f1要释放,因此在fun函数中加了一句
delete f1;
这时,我们认为至少是调用函数中的f1内存被释放了,但是,问题在于当调用函数结束后,Rose中的析构函数并没有被执行,原因通过父类指针去释放,会导致子类对象可能清理不干净,即一旦子类对象中有在堆区开辟的数据,在析构中去释放,但这种情况下根本不会去走派生类的析构函数,造成内存泄漏。如下图所示,根本没有Rose的析构:
问题总结:当基类指针指向或引用堆区开辟的派生类时,删除父类指针并不会调用子类的析构函数
派生类除了自己的析构函数,还要继承基类的析构函数,用父类的指针默认调用的时父类的析构
要想解决,就要将父类的析构函数加上virtual变成虚析构,这样的话,在析构的时候才会先调用子类的析构,再调用父类的析构,完整程序如下:
#include<iostream>
#include<string>
using namespace std;
class Flower
{
public:
Flower()
{
cout << "Flower的构造函数" << endl;
}
virtual void display() = 0;
virtual ~Flower()
{
cout << "Flower的析构函数" << endl;
}
};
class Rose :public Flower
{
public:
Rose(string c)
{
color = c;
cout << "Rose的构造函数" << endl;
}
void display()
{
cout << "玫瑰" << endl;
}
~Rose()
{
cout << "Rose的析构函数" << endl;
}
string color;
};
void fun()
{
Flower* f1 = new Rose("blue");//在局部函数定义的f1,利用完就要释放
f1->display();
delete f1;
}
int main() {
fun();
system("pause");
return 0;
}
纯虚析构其实比较鸡肋,在析构函数的前面加上virtual,形成纯虚析构,这时候这个类也会被定义为抽象类。但要注意的是,纯虚析构虽然是后边等于0,但是结构上一定要有具体的实现,否则编译就会被报错。
3.构造函数[下篇会专门推送构造函数]
由上述程序可以看到:
派生类构造时,先构造基类部分,然后再构造派生类部分;派生类析构时,先析构派生类部分,然后再析构基类部分。
因此:在基类构造函数执行的时候,派生类的部分是未定义状态;在基类析构函数执行的时候,派生类的部分已经被释放了。
所以在基类的构造函数或析构函数中调用虚函数是不建议的