C++调用虚函数的注意事项
当在类的内部调用虚函数时,需要注意如下几点。
CDerived:func2()
第 20 行调用 func1 成员函数。进入 func1 成员函数,执行到第 8 行,调用 func2 函数。看起来调用的应该是 CBase 类的 func2 成员函数,但输出结果证明实际上调用的是 CDerived 类的 func2 成员函数。
这是因为,在 func1 函数中,
当本程序执行到第 8 行时,this 指针指向的是一个 CDerived 类的对象,即 d,因此被调用的就是 CDerived 类的 func2 成员函数。
请看下面的程序:
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 关键字,也自动成为虚函数。
例如下面的程序:
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 中都有的同名、同参数表的虚函数,故这条函数调用语句就是多态的。
在成员函数中调用虚函数
类的成员函数之间可以互相调用。在成员函数(静态成员函数、构造函数和析构函数除外)中调用其他虚成员函数的语句是多态的。例如下面的程序:#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 中都有的同名、同参数表的虚函数,故这条函数调用语句就是多态的。
所有教程
- socket
- Python基础教程
- C#教程
- MySQL函数
- MySQL
- C语言入门
- C语言专题
- C语言编译器
- C语言编程实例
- GCC编译器
- 数据结构
- C语言项目案例
- C++教程
- OpenCV
- Qt教程
- Unity 3D教程
- UE4
- STL
- Redis
- Android教程
- JavaScript
- PHP
- Mybatis
- Spring Cloud
- Maven
- vi命令
- Spring Boot
- Spring MVC
- Hibernate
- Linux
- Linux命令
- Shell脚本
- Java教程
- 设计模式
- Spring
- Servlet
- Struts2
- Java Swing
- JSP教程
- CSS教程
- TensorFlow
- 区块链
- Go语言教程
- Docker
- 编程笔记
- 资源下载
- 关于我们
- 汇编语言
- 大数据
- 云计算
- VIP视频