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方法。

基类中的私有虚函数:

基类中的私有虚函数可以被派生类重写,如果使用基类指针指向一个派生类对象,调用该方法会报错,除非是基类的成员函数或者友元才可以。
如果是派生类的指针,则会执行派生类类重写后的逻辑。
基类的所有成员都会被派生类继承,只是private修饰的成员,派生类类没有访问权限。因此,private、public、protected等只是表示访问权限,并不影响基类成员是否被派生类继承。而 virtual 修饰该方法可以被派生类重写,被virtual修饰的成员函数不论他们是private、protected还是 public 的,都会被放置到虚函数表中,那么就代表派类可以重写。派生类在重写时可以为函数在派生类中的访问权限,例如将 private 改为 public或protected,但基类的访问权限依然为private。

虚析构函数:

为什么基类的析构函数必须声明称虚函数呢?大家可能都知道,基类的析构函数不声明成虚函数时在释放时候有可能会导致子类的析构函数调用不到的情况。

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时候既调用到子类的析构函数又能调用到父类的析构函数呢?

因为在定义为非虚析构函数时会采用静态联编,调用析构函数时因为是静态联编,在编译时会按照指针定义的类型也就是父类的类型,所以在调用哪个析构函数做选择时会选择父类的析构函数;但是在将基类的析构函数定义为虚析构函数后,就会采用动态联编,调用析构函数时会按照指针实际所指向的类型,也就是子类的类型,调用的自然就是子类的析构函数。上面说到过子类的虚构函数会调用父类的析构函数,所以父类子类的析构函数都被调用到了。

虚函数重载

直接上代码

class Base
{
public:
    virtual void fun(){
        printf("base fun \n");
    }
    virtual void fun(int a){
        printf("base fun int\n");
    }
    int a=10;
};
class Drive:public Base{
public:
    virtual void fun(){
        printf("drive fun %d\n",a);
    }
//    using Base::fun;
};
int main(int argc, char *argv[])
{
//    Drive *d = new Drive(); //使用派生类编译会报错没有匹配的函数;如果在派生类中声明using Base::fun;也不会报错
//    Base *d = new Drive(); //使用基类类型指针指向派生类编译不会报错
    d->fun(10);
    return 0;
}

上面代码中基类的虚函数中出现了重载,使用基类类型指针指向派生类编译不会报错,使用派生类编译会报错没有匹配的函数因为派生类中没有实现带一个int参数的fun方法,但是在派生类中声明using Base::fun;也后不会报错了。笔者理解是如果用基类指针去指向一个派生类的对象,在使用时程序是当基类使用的(publick继承派生类中存在一个和基类完全相同的基类所以可以这么使用),这就相当于正常的函数重载一样,只是基类的一个重载实现被派生类重写了(其实是虚函数表里面的指针被子类的替换了)。但是如果使用派生类的指针,程序就认为使用了派生类,派生类和基类之间是无法重载的(重载的前提条件在一个区域内,这里可以理解为同一个类里面)所以编译会报错,但是使用了using Base::fun声明后相当于在同一个作用区域所以就不会报错了。

注意:

  1. 内联函数不能是虚函数,因为内联函数是在编译阶段展开的,而虚函数是在运行时动态调用的,编译时无法展开。
  2. 构造函数不能是虚函数,因为构造函数是在创建对象时候调用的,在创建派生类对象时会先调用基类的构造函数再调用派生类的构造函数。构造函数声明成虚函数是毫无意义的。
  3. 静态成员函数不能是虚函数,静态函数相当于普通函数和类、实例并没有关系所以也不存在多态,而虚函数是一种特殊的成员函数用来实现运行时多态的。所以静态成员函数声明成虚函数毫无意义。

以上内容均是作者查阅资料加自己理解,如有疑问,望读者不吝赐教,谢谢!
参考文献《C++ primer plus》(第六版)中文版

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值