虚函数

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”。

虚函数的工作原理

编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编

当类中存在虚函数时,编译器默认会给对象添加一个隐藏成员。该成员为一个指向虚函数表(virtual function table,vtbl)的指针。

虚函数表是一个保存了虚函数地址的数组。编译器会检查类中所有的虚函数,依次将每个虚函数的地址,存入虚函数表。

虚函数表主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
没有覆盖父类的虚函数是毫无意义的。

一般继承(无虚函数覆盖)
#include <iostream>

using namespace std;

class Base
{
public:
    virtual void f()
    {
        cout << "Base::f" << endl;
    }
    virtual void g()
    {
        cout << "Base::g" << endl;
    }
};

class Derive : public Base
{

};

int main()
{
    Base *b = new Derive();
    b->f();
}

//打印结果为:
Base::f

image

虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。

一般继承(有虚函数覆盖)
#include <iostream>

using namespace std;

class Base
{
public:
    virtual void f()
    {
        cout << "Base::f" << endl;
    }
    virtual void g()
    {
        cout << "Base::g" << endl;
    }
    virtual void h()
    {
        cout << "Base::h" << endl;
    }
};

class Derive : public Base
{
    virtual void f()
    {
        cout << "Derive::f" << endl;
    }
    virtual void g1()
    {
        cout << "Derive::g1" << endl;
    }
    virtual void h1()
    {
        cout << "Derive::h1" << endl;
    }
};

int main()
{
    Base *b = new Derive();
    b->f();
}

//打印结果为:
Derive::f

image

派生类覆盖父类虚函数的函数被放到了虚表中原来父类虚函数的位置。没有被覆盖的函数依旧。

多重继承(无虚函数覆盖)

image

每个父类都有自己的虚表。

子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

多重继承(有虚函数覆盖)

image

个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。

总结:

  • 1, 虚函数是非静态的、非内联的成员函数,而不能是友元函数,但虚函数可以在另一个类中被声明为友元函数。
  • 2, 虚函数声明只能出现在类定义的函数原型声明中,而不能在成员函数的函数体实现的时候声明。
  • 3, 一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
  • 4, 若类中一个成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。当使用该成员函数操作指针或引用所标识的对象时 ,对该成员函数调用可采用动态联编。
  • 5, 定义了虚函数后,程序中声明的指向基类的指针就可以指向其派生类。在执行过程中,该函数可以不断改变它所指向的对象,调用不同 版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。 纯虚函数 版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。
纯虚函数

虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

1, 当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,其实现留待派生类完成。

2, 纯虚函数的作用是为派生类提供一个一致的接口。
纯虚函数不能实例化,但可以声明指针。

类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。抽象类不能被用于实例化对象,它只能作为接口使用。

虚基类 和 虚继承

image

#include <iostream>

using namespace std;

class A
{
public:
    int iValue;
};

class B :public A
{
public:
    void bPrintf(){ cout << "This is class B" << endl; };
};

class C :public A
{
public:
    void cPrintf(){ cout << "This is class C" << endl; };
};

class D :public B, public C
{
public:
    void dPrintf(){ cout << "This is class D" << endl; };
};

void main()
{
    D d;
//    cout << d.iValue << endl; //错误,不明确的访问
    cout << d.A::iValue << endl; //正确
    cout << d.B::iValue << endl; //正确
    cout << d.C::iValue << endl; //正确
}

类B C都继承了类A的iValue成员,因此类B C都有一个成员变量iValue ,而类D又继承了B C,这样类D就有一个重名的成员 iValue(一个是从类B中继承过来的,一个是从类C中继承过来的).在主函数中调用d.iValue 因为类D有一个重名的成员iValue编译器不知道调用 从谁继承过来的iValue所以就产生的二义性的问题.正确的做法应该是加上作用域限定符 d.B::iValue 表示调用从B类继承过来的iValue。不过 类D的实例中就有多个iValue的实例,就会占用内存空间。所以C++中就引用了虚基类的概念,来解决这个问题。

class A  
{  
public:  
 int iValue;  
};  
  
class B:virtual public A  
{  
public:  
 void bPrintf(){cout<<"This is class B"<<endl;};  
};  
  
class C:virtual public A  
{  
public:  
 void cPrintf(){cout<<"This is class C"<<endl;};  
};  
  
class D:public B,public C  
{  
public:  
 void dPrintf(){cout<<"This is class D"<<endl;};  
};  
  
void main()  
{  
 D d;  
 cout<<d.iValue<<endl; //正确  
}

image
在继承的类的前面加上virtual关键字表示被继承的类是一个虚基类,它的被继承成员在派生类中只保留一个实例。例如iValue这个成员,从类 D这个角度上来看,它是从类B与类C继承过来的,而类B C又是从类A继承过来的,但它们只保留一个副本。因此在主函数中调用d.iValue时就不 会产生错误。

总结:

  • 1, 一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
  • 2, 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的子对象。
  • 3, 虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。
  • 4, 最派生类是指在继承结构中建立对象时所指定的类。
  • 5, 派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。
  • 6, 从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生 类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象 只初始化一次。
  • 7, 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。

析构函数与虚函数

将析构函数定义为虚函数主要原因是因为多态的存在。

#include <iostream>

class Base
{
public:
    Base(){ std::cout << "Constructing Base!" << std::endl; };
    ~Base() { std::cout << "Destroy Base!" << std::endl; };
};

class Derive : public Base
{
public:
    Derive(){ std::cout << "Constructing Derive!" << std::endl; };
    ~Derive() { std::cout << "Destroy Derive!" << std::endl; };
};

int main()
{
    Base *basePtr = new Derive();

    delete basePtr;
    return 0;
}

//打印结果为:
Constructing Base!
Constructing Derive!
Destroy Base!

//只删除了基类的分配的空间,派生类的对象的空间没有删除,会造成内存泄漏。

析构函数应是虚函数,除非类不用做基类。

由虚函数表,我们知道,若析构函数不声明为virtual,则调用的将是Base类的析构函数,而没有调用Derive类的析构函数,此时造成了内存泄露。

所以析构函数必须声明为虚函数,调用的将是子类Derive的析构函数,

我们还需要知道的一点是,子类析构函数,一定会调用父类析构函数,释放父类对象,则内存安全释放。
析构函数的调用顺序为先调用派生类析构函数清理新增的成员,再调用子对象析构函数(基类析构函数)清理子对象,最后再调用基类析构函数清理基类成员。

#include <iostream>

class Base
{
public:
    Base(){ std::cout << "Constructing Base!" << std::endl; };
    virtual ~Base() { std::cout << "Destroy Base!" << std::endl; };
};

class Derive : public Base
{
public:
    Derive(){ std::cout << "Constructing Derive!" << std::endl; };
    ~Derive() { std::cout << "Destroy Derive!" << std::endl; };
};

int main()
{
    Base *basePtr = new Derive();

    delete basePtr;
    return 0;
}

//打印结果:
Constructing Base!
Constructing Derive!
Destroy Derive!
Destroy Base!

构造函数为什么不能是虚函数

1. 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

2. 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

3. 构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

4. 从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。

5. 当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。


Simple
10 声望4 粉丝