C++调用虚函数的注意事项

当在类的内部调用虚函数时,需要注意如下几点。

在成员函数中调用虚函数

类的成员函数之间可以互相调用。在成员函数(静态成员函数、构造函数和析构函数除外)中调用其他虚成员函数的语句是多态的。例如下面的程序:
#include <iostream>
using namespace std;
class CBase
{
public:
    void func1()
    {
        func2();
    }
    virtual void func2()  {cout << "CBase::func2()" << endl;}
};
class CDerived:public CBase
{
public:
    virtual void func2() { cout << "CDerived:func2()" << endl; }
};
int main()
{
    CDerived d;
    d.func1();
    return 0;
}
程序的输出结果如下:
CDerived:func2()

第 20 行调用 func1 成员函数。进入 func1 成员函数,执行到第 8 行,调用 func2 函数。看起来调用的应该是 CBase 类的 func2 成员函数,但输出结果证明实际上调用的是 CDerived 类的 func2 成员函数。

这是因为,在 func1 函数中,func2();等价于this -> func2();,而 this 指针显然是 CBase* 类型的,即是一个基类指针,那么this -> func2();就是在通过基类指针调用虚函数,因此这条函数调用语句就是多态的。

当本程序执行到第 8 行时,this 指针指向的是一个 CDerived 类的对象,即 d,因此被调用的就是 CDerived 类的 func2 成员函数。

在构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数不是多态,因为编译时即可确定调用的是哪个函数。如果本类有该函数,调用的就是本类的函数;如果本类没有,调用的就是直接基类的函数;如果直接基类没有,调用的就是间接基类的函数,以此类推。

请看下面的程序:
#include <iostream>
using namespace std;
class A
{
public:
    virtual void hello() { cout << "A::hello" << endl; }
    virtual void bye() { cout << "A::bye" << endl; }
};
class B : public A
{
public:
    virtual void hello() { cout << "B::hello" << endl; }
    B() { hello(); }
    ~B() { bye(); }
};
class C : public B
{
public:
    virtual void hello() { cout << "C::hello" << endl; }
};
int main()
{
    C obj;
    return 0;
}
程序的输出结果如下:
B::hello
A::bye

类 A 派生出类 B,类 B 派生出类 C。

第 23 行,obj 对象生成时会调用类 B 的构造函数,在类 B 的构造函数中调用 hello 成员函数。由于在构造函数中调用虚函数不是多态,所以此时不会调用类 C 的 hello 成员函数,而是调用类 B 自己的 hello 成员函数。

obj 对象消亡时,会引发类 B 析构函数的调用,在类 B 的析构函数中调用了 bye 函数。类B没有自己的 bye 函数,只有从基类 A 继承的 bye 函数,因此执行的就是类 A 的 bye 函数。

将在构造函数中调用虚函数实现为多态是不合适的。以上面的程序为例,obj 对象生成时,要先调用基类构造函数初始化其中的基类部分。在基类构造函数的执行过程中,派生类部分还未完成初始化。此时,在基类 B 的构造函数中调用派生类 C 的 hello 成员函数,很可能是不安全的。

在析构函数中调用虚函数不能是多态的原因也与此类似,因为执行基类的析构函数时,派生类的析构函数已经执行,派生类对象中的成员变量的值可能已经不正确了。

注意区分多态和非多态的情况

初学者往往弄不清楚一条函数调用语句是否是多态的。要注意,通过基类指针或引用调用成员函数的语句,只有当该成员函数是虚函数时才会是多态。如果该成员函数不是虚函数,那么这条函数调用语句就是静态联编的,编译时就能确定调用的是哪个类的成员函数。

另外,C++ 规定,只要基类中的某个函数被声明为虚函数,则派生类中的同名、同参数表的成员函数即使前面不写 virtual 关键字,也自动成为虚函数。

例如下面的程序:
#include <iostream>
using namespace std;
class A
{
public:
    void func1() { cout<<"A::func1"<<endl; };
    virtual void func2() { cout<<"A::func2"<<endl; };
};
class B:public A
{
public:
    virtual void func1() { cout << "B::func1" << endl;  };
    void func2() { cout << "B::func2" << endl; }  //func2自动成为虚函数
};
class C:public B  // C以A为间接基类
{
public:
    void func1() { cout << "C::func1" << endl; }; //func1自动成为虚函数
    void func2() { cout << "C::func2" << endl; }; //func2自动成为虚函数
};
int main()
{
    C obj;
    A *pa = &obj;
    B *pb = &obj;
    pa->func2();  //多态
    pa->func1();  //不是多态
    pb->func1();  //多态
    return 0;
}
程序的输出结果如下:
C::func2
A::func1
C::func1

基类 A 中的 func2 是虚函数,因此派生类 B、C 中的 func2 声明时虽然没有写 virtual 关键字,也都自动成为虚函数。所以第 26 行就是一个多态的函数调用语句,调用的是 C 类的 func2 成员函数。

基类 A 中的 func1 不是虚函数,因此第 27 行就不是多态的。编译时,根据 pa 的类型就可以确定 func1 就是类 A 的成员函数。

func1 在类 B 中成为虚函数,因此在类 B 的直接和间接派生类中,func1 都自动成为虚函数。因此,第 28 行,pb 是基类指针,func1 是基类 B 和派生类 C 中都有的同名、同参数表的虚函数,故这条函数调用语句就是多态的。