C++虚函数、虚析构函数浅析
C++虚函数浅析
学习过C++的都知道可以通过虚函数实现多态。在基类中定义⼀个虚函数,在派⽣类中可以重写这个虚函数,实现派⽣类⾃⼰的特性。
虚函数的⼯作原理:
C++规定了函数名参数返回值,没有规定实现,可以根据需要⾃⾏实现内容。通常编译器处理虚函数的⽅法是给每个对象添加⼀个隐藏成员。该成员保存了⼀个指向函数地址的数组指针,这个数组指针也就是虚函数表。虚函数表中保存了对象中所有虚函数的地址(包括继承的基类的虚函数地址),如果派⽣类多重继承就会存在多个虚函数表,派⽣类本⾝的虚函数表会出现在继承顺序第⼀个基类后⾯,下⾯举例演⽰⼀下:
class a{
public:
virtual void base_a1();
virtual void base_a2();
]
class b{
public:
virtual void base_b1();
virtual void base_b2();
]
class c{
public:
virtual void base_c1();
virtual void base_c2();
]
class d:public a,public b,public c{
virtual void derive_d1();
virtual void derive_d2();
}
int main(){
d derive_d;
}
上⾯代码中定义derive_d的虚函数表结构如下图:
在对象所占内存的开始位置存在隐藏成员指针a、指针b、指针c,他们分别指向对应的虚函数表,对象⾃⼰的虚函数表在第⼀个继承的基类的后⾯。
class d:public a,public b,public c{
void base_b2();
virtual void derive_d1();
virtual void derive_d2();
}
如果派⽣类重写了基类的函数(⽅法),那么在上图base_b2处的地址将变成指向派⽣类实现base_b2⽅法的地址。此时⽤派⽣类的对象在调⽤base_b2⽅法时调⽤的就是派⽣类重写的base_b2⽅法。
虚析构函数:
为什么基类的析构函数必须声明称虚函数呢?⼤家可能都知道,基类的析构函数不声明成虚函数时在释放时候有可能会导致⼦类的析构函数调⽤不到的情况。
class Base{
public:
析构函数的定义Base()
{
qDebug()<<"base";
}
~Base()
{
qDebug()<<"~base";
}
};
class Derive:public Base{
public:
Derive()
{
qDebug()<<"Derive";
}
~Derive()
{
qDebug()<<"~Derive";
}
};
int main(){
Base *p=new Derive();
delete(p);
}
输出:
base
Derive
~base
上⾯代码执⾏情况就会先调⽤基类的构造函数然后调⽤派⽣类的构造函数,然后调⽤基类的析构函数。这就导致了内存泄露,为派⽣类分配了内存但是并没有释放。如果main函数像下⾯这样实现就不会出现这种情况:
int main(){
Derive *p=new Derive;
delete(p);
}
输出:
base
Derive
~Derive
~base
因为派⽣类的析构函数会调⽤基类的析构函数。虽然这种⽅法可以避免内存泄露,但是有时候某种情况下必须定义⽗类指针去指向⼀个⼦类的对象所以还是存在风险。只要将Base类如下声明就可以完全避免这个问题。
class Base{
public:
virtual ~Base();
}
输出:
base
Derive
~Derive
~base
这样⽆论你是定义⼦类指针去指向⼀个⼦类对象还是⽤⽗类定义指针去指向⼦类对象都不会有内存泄露的问题。
到这⾥可能⼤家都知道,但是作者在这⾥提出⼀个疑问,为什么⽗类声明成虚构造函数就能在delete时候既调⽤到⼦类的析构函数⼜能调⽤到⽗类的析构函数呢?这就不得不提⼀下C++中的静态联编和动态联编了。
C++中的静态联编和动态联编
在提到动态和静态的时候我不禁想起了C语⾔中的动态库和静态库,其实动态联编和静态联编的区别与动态库和静态库的区别⼗分相似。
程序在调⽤函数时候调⽤哪个函数,执⾏哪段代码是由编译器负责的,在C语⾔中每个函数名不同调⽤起来就⼗分简单,但是C++中可以重载的缘故,编译器必须确定什么时候执⾏哪个函数调⽤哪段代码,在编译过程中可以确定调⽤哪段代码的被称为静态联编(早期联编)。由于C++中存在虚函数,使⽤哪个函数调⽤哪段代码在编译时并不能确定就像之前例⼦中说的定义⽗类的指针去指向⼀个⼦类的对象,这时编译器⽆法做出正确的判断,不知道是该调⽤⽗类的析构函数还是⼦类的析构函数。编译器必须在程序运⾏时调⽤正确的虚函数代码,这就被称为动态联编。动态联编和虚函数是息息相关的(虚函数采⽤动态联编⾮虚函数采⽤静态联编)。动态联编效率会低于静态联编,所以不要声明没有必要的虚函数,这样会导致效率降低。
介绍完静态联编和动态联编,我们回到上⾯的问题:
为什么⽗类声明成虚构造函数就能在delete时候既调⽤到⼦类的析构函数⼜能调⽤到⽗类的析构函数呢?
因为在定义为⾮虚析构函数时会采⽤静态联编,调⽤析构函数时因为是静态联编,在编译时会按照指针定义的类型也就是⽗类的类型,所以在调⽤哪个析构函数做选择时会选择⽗类的析构函数;但是在将基
类的析构函数定义为虚析构函数后,就会采⽤动态联编,调⽤析构函数时会按照指针实际所指向的类型,也就是⼦类的类型,调⽤的⾃然就是⼦类的析构函数。上⾯说到过⼦类的虚构函数会调⽤⽗类的析构函数,所以⽗类⼦类的析构函数都被调⽤到了。
注意:
1. 内联函数不能是虚函数,因为内联函数是在编译阶段展开的,⽽虚函数是在运⾏时动态调⽤的,编译时⽆法展开。
2. 构造函数不能是虚函数,因为构造函数是在创建对象时候调⽤的,在创建派⽣类对象时会先调⽤基类的构造函数再调⽤派⽣类的构造
函数。构造函数声明成虚函数是毫⽆意义的。
3. 静态成员函数不能是虚函数,静态函数相当于普通函数和类、实例并没有关系所以也不存在多态,⽽虚函数是⼀种特殊的成员函数⽤
来实现运⾏时多态的。所以静态成员函数声明成虚函数毫⽆意义。
以上内容均是作者查阅资料加⾃⼰理解,如有疑问,望读者不吝赐教,谢谢!
参考⽂献《C++ primer plus》(第六版)中⽂版
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论