Loading

C++虚成员函数与动态联编

本篇博客会说明一下虚函数与动态联编的联系,以及虚指针和虚函数表的基本概念。首先了解何为动态联编,何为静态联编。

当程序调用函数时,编译器负责告诉你将使用哪个可执行代码块,将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。

在C语言中,每个函数名都对应一个不同的函数,所以用谁是谁,一对一,在编译过程就能完成联编,很明显的静态联编。而在C++中,由于函数重载的缘故,就不能简单的一对一去联编了,编译器必须查看函数名以及函数参数才能确定使用哪个函数(在C++中,编译器看到的并不是我们所定义的函数名,我们所定义的每个函数,都有一个函数签名,就算我们函数名相同,但我们参数不同,类型不同,甚至是const与non-const的关系,都会导致函数签名不相同),不过,这种程度在C/C++编译器中也能够在编译过程完成联编,这种在编译过程完成联编的就叫做静态联编。但C++中有一个东西使得编译器很难在编译阶段确定你要使用哪一个函数,那就是虚函数,因为编译器不知道用户将选择哪种类型的对象,所以,编译器必须生成能够在程序运行时选择正确的虚函数的代码,这种称为动态联编。

 

接下来观察虚函数与动态联编的联系:

假设我们有基类Baseclass,与子类Derivedclass,基类中有execf()方法。

Deriveclass dc;
Baseclass * bc;
bc = &dc;
bc->execf();

如果基类中没有将execf()声明为虚方法,则bc->execf()将会根据指针类型(Baseclass*)调用Baseclass::execf()。指针类型在编译时已知,因此编译器在编译时,将execf()关联到Baseclass::execf(),所以,编译器对非虚方法使用静态联编。

但如果在基类中将execf()声明为虚方法,则dc->execf()根据对象类型(Deriveclass)调用Deriveclass::execf(),在这个例子中,对象类型为BrassPlus,但通常只有在运行程序时才能确定对象类型。所以编译器生成的代码将在程序执行时,根据对象类型将execf()关联到Baseclass::execf()或者Deriveclass::execf(),所以编译器对虚方法使用动态联编。

 

接下来说明一下虚指针与虚表(也称虚函数表)。

当我们定义一个类时,并创建一个对象,编译器会给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这个指针就是虚指针,这个数组就是虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址,例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表,派生类对象将包含一个指向独立虚表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存这个重定义虚函数的地址;如果派生类没有重新定义基类虚函数,派生类的虚表将保存函数原始的地址,也就是跟基类中这个虚函数的地址相同(无则不变,有则更新),如果派生类定义了新的虚函数(基类没有的),则该虚函数的地址也将被添加到虚表中。

 

通过下面的代码和图片就能清晰理解其中的关系了。

#ifndef __TEST__
#define __TEST__

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

class B :public A {
public:
    virtual void vfunc1();
    void func2();
private:
    int m_data3;
};

class C :public B {
public:
    virtual void vfunc1();
    void func2();
private:
    int m_data1, m_data4;
};

#endif

 

 

 

  可以看到在B类对象中重新定义了A类中的虚函数vfunc1(),所以在B的虚表中vfunc2的地址从0x401ED0更新为0x401F80,而因为没有重新定义vfunc2(),所以地址仍然是0x401F10,C类对象也是如此。

 

  调用虚函数时,程序查看存储在对象中的vtbl地址,然后转向相应的函数地址表,如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数,如果使用类声明中的第三个函数,程序将使用地址为数组中第三个元素的函数。

  在使用虚函数时,在内存和执行速度方面有一定的成本:

  • 每个对象都将增大,增大量为存储地址的空间
  • 对于每个类,编译器都创建一个虚函数地址表(数组)
  • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址

虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

 

对于虚析构函数的一些注意事项:

1.构造函数不能是虚函数,创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制,因此,派生类不继承基类的构造函数。

2.析构函数应当是虚函数,除非类不用做基类,比如看下面代码:

#include<iostream>
using namespace std;

class A {
public:
    A() { cout << "A::ctor()" << endl; }
    ~A() { cout << "A::dtor()" << endl; }
};

class B : public A {
public:
    B() { cout << "B::ctor()" << endl; }
    ~B() { cout << "B::dtor()" << endl; }
};

int main() {
    A * p = new B();
    delete p;

    system("pause");
    return 0;
}

假如 基类析构函数不是虚函数,当使用A类指针指向一个B类对象时,释放内存时,我们会发现以下结果:

 

 B类的析构函数没有被调用,这样会造成严重的影响,因为delete p只删除了A类所拥有的内存空间,并没有释放掉B类对应的内存空间,并且也再也无法访问这片空间,这样就造成了内存泄漏。

那么我们将析构函数设为virtual呢。

#include<iostream>
using namespace std;

class A {
public:
    A() { cout << "A::ctor()" << endl; }
    virtual ~A() { cout << "A::dtor()" << endl; }
};

class B : public A {
public:
    B() { cout << "B::ctor()" << endl; }
    ~B() { cout << "B::dtor()" << endl; }
};

int main() {
    A * p = new B();
    delete p;

    system("pause");
    return 0;
}

结果如下:

 

 

可以发现我们成功调用了B类的析构函数。

 

有关虚函数的注意事项:

  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法,这称为动态联编,这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
  • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚方法。
posted @ 2021-01-18 17:30  eveilcoo  阅读(260)  评论(0编辑  收藏  举报