一、什么是虚函数?
在基类中使用 virtual
关键字修饰的成员函数称为虚函数。
用法格式:
cpp
virtual 函数返回值类型 函数名 (参数表){
函数体;
}
虚函数是 C++ 中实现多态性的重要机制,它允许在派生类中重写基类中的函数。虚函数的核心特性在于,它使得通过基类指针或引用调用函数时,可以根据实际对象的类型选择相应的函数实现。
二、虚函数的作用
虚函数的作用,用专业术语来解释就是实现多态性(Polymorohism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
具体而言,就像买火车票这件事。同样是买火车票,不同的人去买有不同的结果,学生买火车票半价,而军人优先购票等。
下面来看这样一段代码:
cpp
#include<iostream>
using namespace std;
class A
{
public:
void print()
{
cout<<"This is A"<<endl;
}
};
class B : public A
{
public:
void print()
{
cout<<"This is B"<<endl;
}
};
class C : public A
{
public:
void print()
{
cout<<"This is C"<<endl;
}
};
int main()
{
//为了在以后便于区分,我这段main()代码叫做main1
A a;
B b;
C c;
a.print();
b.print();
c.print()
return 0;
}
运行结果:
cpp
This is A
This is B
This is C
通过class A、class B和class C的print()这个接口,可以看出这三个class因个体的差异而采用了不同的策略,但这并不是多态性行为。这里,a.print()
、b.print()
和 c.print()
都是静态绑定,因此编译器直接根据对象的类型选择调用相应的 print()
方法,而不管是否存在派生类。
现在把main()处的代码改一改。
cpp
#include<iostream>
using namespace std;
class A
{
public:
void print()
{
cout<<"This is A"<<endl;
}
};
class B : public A
{
public:
void print()
{
cout<<"This is B"<<endl;
}
};
class C : public A
{
public:
void print()
{
cout<<"This is C"<<endl;
}
};
int main()
{
//为了在以后便于区分,我这段main()代码叫做main2
A a;
B b;
C c;
A *p1 = &a;
A *p2 = &b;
A *p3 = &c;
p1->print();
p2->print();
p3->print();
return 0;
}
运行结果:
cpp
This is A
This is A
This is A
问题来了,p2、p3明明指向的是class B、class C的对象但却是调用的class A的print()函数,这不是我们所期望的结果,那么解决这个问题就需要用到虚函数。
cpp
#include<iostream>
using namespace std;
class A
{
public:
virtual void print()
{
cout<<"This is A"<<endl;
}
};
class B : public A
{
public:
void print()
{
cout<<"This is B"<<endl;
}
};
class C : public A
{
public:
void print()
{
cout<<"This is C"<<endl;
}
};
int main()
{
//为了在以后便于区分,我这段main()代码叫做main2
A a;
B b;
C c;
A *p1 = &a;
A *p2 = &b;
A *p3 = &c;
p1->print();
p2->print();
p3->print();
return 0;
}
运行结果:
cpp
This is A
This is B
This is C
毫无疑问,class A的成员函数print()已经成了虚函数,那么class B的print()成了虚函数了吗?回答是Yes,我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。所以,class B的print()也成了虚函数。那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了(语法上可加可不加,不加的话编译器会自动加上,但为了阅读方便和规范性,建议加上)。
现在重新运行main2的代码,这样输出的结果就是This is A、This is B和This is B了。
现在来消化一下,我作个简单的总结,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
C++中的虚函数的作用主要是实现了多态 的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数(动态绑定 )。这种技术可以让父类的指针有"多种形态",这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
三、虚函数的底层实现
虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数。
实现原理:虚函数表+虚表指针
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针 (vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。
举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:
如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。
如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。
调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。
四、使用虚函数后的变化
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 每个类编译器都创建一个虚函数地址表。
(3) 对每个函数调用都需要增加在表中查找地址的操作。
五、虚函数的注意事项
在使用虚函数时,有一些重要的注意事项,以确保代码的正确性、可维护性和性能。以下是一些关键点:
1. 基类析构函数应为虚函数
如果类包含虚函数,其析构函数也应声明为虚函数。这确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,避免资源泄漏。
2. 尽量避免在构造和析构函数中使用虚函数
在对象的构造和析构阶段,虚指针尚未设置(构造时)或已经清除(析构时),因此调用虚函数可能导致未定义行为。应避免在构造函数和析构函数中调用虚函数。
3. 虚函数的重写
当派生类重写基类的虚函数时,使用 override
关键字可以增加可读性,并在编译时进行检查。如果派生类中的函数没有正确重写基类的虚函数,编译器会给出警告。
4. 性能开销
虚函数调用比普通函数调用的性能开销稍高,因为它涉及查找虚函数表。对于频繁调用的性能敏感函数,尽量使用非虚函数,或者考虑其他设计方案。
5. 多重继承中的虚函数
在多重继承中,虚函数可能会导致复杂性。应仔细设计类的继承结构,以避免潜在的二义性问题。使用虚基类可以帮助解决此类问题。
6. 指向虚函数的指针
在一些情况下,使用基类指针指向派生类对象时,应确保适当地处理对象的生命周期,避免内存泄漏或访问无效指针。