目录
- 虚函数与纯虚函数
- 虚函数
- 纯虚函数
- 虚函数的工作原理
- 构造函数/析构函数/友元函数与虚函数的关系
- 重写虚函数
- 虚函数默认参数问题
- 虚函数在构造函数和析构函数中的行为
虚函数与纯虚函数
虚函数
虚函数是指在基类中使用关键字virtual声明的函数,其是实现运行时多态的核心机制,允许派生类重写基类方法,通过基类指针或引用调用实际对象的函数。
比如说,基类Animal中有一个虚函数speak(),然后子类Dog和Cat各自重写该函数,当使用Animal指针指向Dog或Cat对象时,调用speak()就会根据实际对象类型执行对应的函数。
纯虚函数
纯虚函数就是在基类声中声明虚函数时加上=0,这样基类就变成了抽象类(包含纯虚函数的类只用作基类),不能实例化(不能创建该类的对象,类的实例化和创建类的对象在C++中通常指的是同一个过程,即将类的定义转化为具体的内存实体),派生类必须实现这个纯虚函数才能被实例化。
比如:virtual void speak()=0; 这样,Animal类就是抽象类,Dog必须实现speak()才能创建对象。
虚函数的工作原理
通常,编译器处理虚函数的方法为:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组就称为虚函数表(virtual function table,vtbl),这种指针就称为虚指针(virtual pointer,vptr)。虚函数表中存储了为类对象进行声明的虚函数的地址。
基类对象包含一个指针,该指针指向基类中所有虚函数的地址表,则其派生类对象将包含一个指向指向独立地址表的指针。无论类中包含的虚函数是1个还是10个,均只需要在对象中添加一个地址成员,只是表的大小不同而已
- 如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址
- 如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址
- 如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中
具体见下示例:
class Scientist{
{
···
char name[40];
public:
virtual void show_name();
virtual void show_all();
···
};
class Physicist : public Scientist
{
···
char field[40];
public:
void show_all(); //redefined
virtual void show_field(); //new
···
};
上述代码中,具体虚函数表情况如下:

Physicist adam("Adam Crusher", "nucleat structure");
Scientist *psc = &adam;
psc->show_all();
上代码中psc->show_all();的具体过程为:
- 获悉psc->vptr的地址(2096)
- 前往2096处的表
- 获悉表中第2个函数的地址(6820)
- 前往地址6820,并执行此处的函数
还有一个问题:虚函数表是每个类一个,还是每个对象一个?
每个类有一个虚函数表,而每个对象有一个指向该表的指针。这样节省空间,即同一个类的所有对象共享同一个虚函数表,只需要各自维护一个vptr即可。
构造函数/析构函数/友元函数与虚函数的关系
构造函数不能是虚函数,因为在构造对象时,对象的类型是明确的,不会存在派生类构造时还需要动态绑定(动态联编)的情况,即创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,派生类不继承基类的构造函数,所以将类构造函数声明为虚无任何意义。
静态联编(静态绑定):Static Binding,在编译阶段确定函数调用的具体实现,基于变量或表达式的静态类型(声明类型)
动态联编(动态绑定):Dynamic Binding,在运行时根据对象的实际类型(动态类型)确定函数调用的具体实现
析构函数应当是虚函数,除非该类不用做基类,因为如果基类析构函数不是虚函数,那么删除基类指针指向的派生类对象时,行为是未定义的,通常只会调用基类的析构函数,导致派生类的资源未被释放,内存泄漏。所以,如果一个类可能被继承,并且可能通过基类指针删除对象,那么基类的析构函数必须声明为虚的。同时,通常应给基类提供一个虚析构函数,即使它并不需要析构函数
比如:
class Base{
public:
virtual ~Base(){}
};
class Derived : public Base{
public:
~Derived() override{
//释放Derived的资源
}
};
按照上代码,当Base *ptr = new Derived(); delete ptr; 时,会先调用Derived的析构函数,再调用Base的析构函数,正确释放资源。
友元函数不能是虚函数,因为友元函数不是类成员,而只有成员才能是虚函数。
重写虚函数
在派生类中重写虚函数时,可以使用override关键字(C++11引入)来显式说明,这样编译器会检查是否真的重写了类的虚函数,防止因为函数签名不一致而导致的错误。比如,如果基类有virtual void foo(int),而派生类写了void foo(float),可能不会被认为是重写,导致错误。而使用override的话,编译器会报错,提示没有重写。
关键字扩展
override(C++11):显式标记重写,编译器检查签名一致性
final(C++11):阻止类被继承或虚函数被重写
使用上述两个关键字可有效增加代码的安全性
虚函数默认参数问题
如果基类虚函数有默认参数,则派生类重写时不应该改变默认参数的值,因为默认参数的值是在编译时根据指针或引用的静态类型决定的,而不是动态类型,所以,如果基类虚函数有默认参数,派生类重写时即使改变了默认参数,当通过基类指针调用时,使用的还是基类的默认参数,这样可能会导致意料之外的结果。
比如:基类有virtual void foo(int x = 1),派生类重写为void foo(int x = 2)。当通过基类指针调用foo(),执行的是派生类的函数体,但这时参数x的值为1,而可能函数体期望的值为2,这就可能导致错误。故应该避免在虚函数中使用默认参数,或者在派生类中保持默认参数与基类中的默认参数一致。
虚函数在构造函数和析构函数中的行为
在基类的构造函数中调用虚函数,实际调用的是基类自己的版本,而不是派生类的,因为此时派生类尚未构造完成,对象类型被视为基类。
在析构函数中调用虚函数,同样只会调用当前类的版本,因为派生类已经被析构。
因此,在构造函数和析构函数中调用虚函数可能不会得到预期的多态行为,应该避免这样做。
C++多态:
其是面向对象编程的核心特性之一,允许不同类的对象对同一消息(方法调用)做出响应。其核心思想是一个接口,多种实现,通过多态可以显著提升代码的灵活性和可维护性。分为
编译时多态(静态多态)和运行时多态(动态多态)
静态多态:
在编译阶段确定具体调用函数,实现方式有函数重载、运算符重载和模板
动态多态:
在程序运行时确定调用的函数,实现方式为虚函数+继承
2454

被折叠的 条评论
为什么被折叠?



