不能声明为虚函数的函数:

使用虚函数时,有两点要注意:
(1)只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。
(2)一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。

根据什么考虑是否把一个成员函数声明为虚函数呢?
主要考虑以下几点:
(1)首先看成员函数所在的类是否会作为基类。
(2)然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。

有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。

需要说明的是:使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。

常见的不能声明为虚函数的有:普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数。

设置虚函数须注意:
1:只有类的成员函数才能说明为虚函数;
原因:普通函数(非成员函数)只能被overload(重载),不能被override(重写),声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
2:静态成员函数不能是虚函数;
原因:<1>从技术层面上说,静态函数的调用不需要传递this指针。但是虚函数的调用需要this指针,来找到虚函数表。相互矛盾
<2>从存在的意义上说,静态函数的存在时为了让所有类共享。可以在对象产生之前执行一些操作。与虚函数的作用不是一路的。
3:内联函数不能为虚函数;
原因:内联函数是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数)inline函数和virtual函数有着本质的区别,inline函数是在程序被编译时就展开,在函数调用处用整个函数体去替换,而virtual函数是在运行期才能够确定如何去调用的,因而inline函数体现的是一种编译期机制,virtual函数体现的是一种运行期机制。因此,一切virtual函数都不可能是inline函数。
4:构造函数不能是虚函数;
原因:
<1>从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,可是这个指向vtable的指针其实是存储在对象的内存空间的。如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
<2> 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
<3>构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
<4>从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。
<5> 当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。
5:友元函数不能是虚函数;
原因:友元函数不是类的成员函数,由友元函数不可被继承,所以不能是虚函数。

注意:空指针不可调用虚函数,因为虚指针在创建对象时创建,没有创建对象就没有虚指针,那么用空指针调用就会崩溃,空指针可以调用成员函数,前提是该成员函数没有调用类的成员变量,因为指针只要拿到函数的入口地址,就可以调用该函数,但若调用成员变量,就会因为找不到成员变量而崩溃。

6:析构函数可以是虚函数,而且通常声明为虚函数。
原因:析构函数执行时先调用派生类的析构函数,其次才调用基类的析构函数。构造一个派生类的动态对象,如果析构函数不是虚函数,而程序执行时又要通过基类的指针去销毁派生类的动态对象,那么用delete销毁对象时,只调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全,引起内存泄漏。类析构函数要声明为虚函数这样派生类调用析构函数才能层层回调,释放资源。这也是虚函数的作用–提供回调的指针。

分析下述的代码:

#include <iostream>
using namespace std;

class Point
{
public:
    virtual void FunTest()
    {
        cout<<"Point:FunTest()"<<endl;
    }

    Point()
    {
        cout<<"Point:Point()"<<endl;
    }

    ~Point()
    {
        cout<<"Point:~Point()"<<endl;
    }
};

class Circle:public Point
{
public:
    Circle()
    {
        cout<<"Circle:Circle()"<<endl;
    }

    ~Circle()
    {
        cout<<"Circle:~Circle()"<<endl;
    }

private:
    int radius;
};

int main()
{
    Point* p = new Circle;  //p是指向基类的指针变量,指向new开辟的动态存储空间
    delete p;               //delete释放p所指向的空间

    system("pause");
    return 0;
}

运行结果:
这里写图片描述
可以看出,它明明申请开辟了一个派生类的动态内存,却没有在最后调用析构函数进行释放,存在内存泄漏的问题。
因此,我们要把基类的析构函数声明成虚函数,代码如下:

#include <iostream>
using namespace std;

class Point
{
public:
    virtual void FunTest()
    {
        cout<<"Point:FunTest()"<<endl;
    }

    Point()
    {
        cout<<"Point:Point()"<<endl;
    }

    virtual ~Point()
    {
        cout<<"Point:~Point()"<<endl;
    }
};

class Circle:public Point
{
public:
    Circle()
    {
        cout<<"Circle:Circle()"<<endl;
    }

    ~Circle()
    {
        cout<<"Circle:~Circle()"<<endl;
    }

private:
    int radius;
};

int main()
{
    Point* p = new Circle;  //p是指向基类的指针变量,指向new开辟的动态存储空间
    delete p;               //delete释放p所指向的空间

    system("pause");
    return 0;
}

运行结果如下:
这里写图片描述

在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。

因此在类的继承体系中,基类的析构函数不声明为虚函数容易造成内存泄漏。所以如果你设计一定类可能是基类的话,必须要声明其为虚函数。

类里面“定义”的成员函数是内联的,但是仍然可以成为虚函数,那么是不是可以说“内联函数不能成为虚函数”这句话有问题呢,是不是应该改成“显式定义的内联函数不能成为虚函数”。比如下面这个示例程序:

#include   <iostream> 
using   namespace   std; 

class Base
{ 
public: 
    virtual void f1()
    {
        cout<<"Father "<<endl;
    } 
}; 

class Drived1: public Base
{ 
public: 
    void f1()
    {
        cout<<"Son1 "<<endl;
    } 
};

class Drived2: public Base
{ 
public: 
    void f1()
    {
        cout<<"Son2 "<<endl;
    } 
}; 

void myPrint(Base* pBs)
{ 
    pBs->f1(); 
} 

int main() 
{ 
    Base father; 
    Drived1 son1; 
    Drived2 son2; 
    myPrint(&father); 
    myPrint(&son1); 
    myPrint(&son2); 

    system("pause");    
    return 0; 
} 

输出:
Father
Son1
Son2

可以发现,虽然f1在基类中定义的,按理说应该是内联函数,但是它仍然可以成为虚函 数。
类中定义的成员函数(函数体在类中)能成为虚函数,大部分编译器能够将虽然声明为inline但实际上不能inline的函数自动改为不inline的。
要想成为虚函数,必须能够被取到地址.内联函数不能被取到地址所以不能成为虚函数.

你写inline virtual void f(),不能保证函数f()一定是内联的,只能保证f()是虚函数(从而保证此函数一定不是内联函数)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值