一、虚函数基本知识
1.1 函数绑定
函数绑定是将函数入口地址和函数调用相联系的过程,分为动态绑定和静态绑定。
- 静态绑定:在程序执行前完成,由编译系统或操作系统装入程序计算函数的入口地址。
- 动态绑定:在程序执行过程中完成,由程序自身计算函数的入口地址。
C++ 既支持静态绑定,也支持动态绑定。
1.2 虚函数
虚函数是动态绑定的基础,用于类继承关系中,它是在基类中定义的成员函数,而是非静态成员函数。
1.2.1 虚函数格式
virtual 函数类型 函数名(参数表);
1.2.2 用虚函数实现动态多态性的一般方法
- 在基类中定义虚函数
- 在派生类中定于与基类虚函数同名、同参数、同返回类型的成员函数。在派生类中重新实现基类中虚函数称为重写或覆盖。
1.2.3 虚函数特点
- 虚函数只能用于类层次结构中
- 在派生类中重新的虚函数必须与基类中被重写的虚函数具有相同函数名、函数类型、参数个数及参数类型。
- 虚函数具有继承性,虚函数的派生类及派生类的派生类中重新的函数均为虚函数。
1.2.4 虚函数的调用步骤
- 用基类定义指针变量,如 “基类 *p”;
- 将基类对象地址或派生类对象地址赋值给该指针变量,例如" p = &基类对象" 或 "p = & 派生类对象";
- 用 " 指针变量 ->虚函数(实参)" 方式去调用基类或派生类中的虚函数,例如"p->虚函数(实参)"。
1.2.5 隐藏、重载、覆盖(重写)
(1)隐藏:派生类的函数屏蔽了与其同名的基类函数。
- 情况一:派生类的函数与基类函数名相同,但参数不同,无论有无virtual,基类函数将被隐藏;
- 情况二:如果派生类的函数与基类的函数同名,并且参数也相同,但基类函数没有virtual 关键字,基类函数被隐藏;
(2)重载:同一作用域成员函数被重载
- 相同的范围、函数名相同、参数不同
(3)覆盖:派生类函数覆盖基类函数
- 不同的范围,分别位于基类和派生类;
- 函数名字相同,参数也相同
- 基类函数必需有virtual关键字
1.2.6 虚函数注意事项
- 只有类的成员函数才能声明为虚函数;
- 静态成员函数不能声明为虚函数,静态成员函数不受限于某个对象;
- 内联函数不能是虚函数,内联函数不能在执行中动态确定其位置;
- 构造函数不能是虚函数,在调用构造函数时对象还是未定型的空间;
- 在类继承结构中通常将析构函数设置为虚函数,设置虚析构函数后可以利用动态绑定的方式选择相应的析构函数;
- 在基类中声明了虚函数后,派生类声明的虚函数应与基类中的虚函数的参数个数相等,对应参数的类型相同。
PS: 为什么在类继承结构中通常将析构函数设置为虚函数?
class A
{
int x;
public:
A(int i) //构造函数
{
}
virtual ~A(){} //析构函数
virtual void display(){}
}
class B:public A //由A 派生B
{
char *buf;
public:
B(int i,char *p):A(i) //构造函数
{
buf = new char[i];
strcpy(buf,p);
}
~B() //析构函数
{
delete buf;
}
void display()
{
A::display();
}
}
主函数:
void main()
{
A *pa = new B(10,"json");
pa -> display();
delete pa;
}
分析:
- 基类A 的析构函数和display() 设置为虚函数,则派生类B中的析构函数和display() 也是虚函数;
- 当执行delete pa时,由于析构函数为虚函数,采用动态绑定,先执行类B的析构函数,然后调用类A 的析构函数;
调用 A::A() # 调用基类
调用 B::B() # 调用派生类,分配buf指向的空间
调用 类B 的display() 成员函数
调用 B::~B() #释放buf 指向的空间
调用 A::~A()
若将程序中的析构函数和display() 该为非虚函数,pa作为类A 对象指针,指向类B对象,但是执行 delete pa 语句时,仅仅调用类A 的析构函数,而不会调用 类B 的析构函数。
1.2.7 虚函数对象指针之间的转换
在虚函数的多态继承环境中对象指针之间的转换分为3种类型,即子类向基类的向上转换(隐式转换)、基类向子类的向下转换和横向转换。
(1)向上转换
向上转换即将派生类对象指针转换为基类对象指针,向上转换是一种隐式转换,可以直接转换,也可以使用dynamic_cast 运算符。
注意:不能使用基类指针调用派生类中的未重写父类成员函数。
class A
{
public:
virtual void f() {}
}
class B:public A
{
public:
void f() { }
void g() { }
}
void main()
{
B b;
A *pa = &b; //正确,隐式转换,或用 A *pa =dynamic_cast<A *>(&b);
pa -> fa() ;//正确,调用类B 的虚函数
pa -> g();//错误,用基类指针调用派生类独有的成员
}
(2)向下转换
向下转换是一种强制转换,这种转换是指对象指针的转换,而不是对象的直接转换,使用dynamic_cast运算符实现向下转换时,若转换失败则换回NULL,若转换成功则返回正常转换后的对象指针。
A *pa = new B;
B *pb = dynamic_cast<B *>(pa);
派生类的对象通常大于基类的对象,所以将基类对象指针直接转换为派生类对象指针时,可能会导致不可预见的异常。
A *pa = new A;
B *pb = dynamic_cast<B*>(pa);//结果 pb = NULL
(3)横向转换
在多继承层次结构结构中,一个派生类有两个基类,在这两个基类对象指针之间的相互转换称为横向转换。
class A
{
}
class B:public A
{
}
class C:public A
{
}
class D: public B,public C
{
D d;
B *pb = & d;//正确: 隐式向上转换
C *pc = dynamic_cast<C *>(pb);//正确的横向转换
}
二、虚函数表
2.1 带虚函数的派生对象的内存结构
2.1.1类的内存结构
- 所有成员函数都在代码区中唯一存放一份;
- 非静态数据成员则在每个对象存储一份,并按照定义的顺序依次存放;
2.1.2 虚函数表
虚函数表指针:当一个类中有虚函数时,其对象的存储就会在非静态数据成员前面加上一个vfptr 指针,这个指针用来指向一个虚函数表,称为虚函数表指针,vfptr 像数据成员一样存放,占4个字节。
虚函数表:虚函数表中存储着当前类对象的所有虚函数的地址,访问虚函数时,通过vfptr 间址找到vtable表,再进而间址找到要调用的函数。
(1)非虚继承的情况
在没有虚继承的类层次结构中,基类对象和派生类对象的存储组织如下:
- 在派生类对象中按照继承方式列表声明的顺序依次存放基类对象的非静态数据成员,最后是派生类对象的非静态数据成员;
- 若基类声明了虚函数,在基类对象开头有一个指向基类虚函数表的指针,然后是基类非静态数据成员;
- 在基类B虚函数表中依次是B基类的虚函数,若B基类又是另外一个基类A 派生的,则类A的虚函数放在派生类B的虚函数的前面,如果类B中重写了基类A的某个虚函数,则在B的虚函数表用自己的该函数替换原虚函数。
- 派生类中独有的虚函数被加在该虚函数表的后面。
(2)虚继承的情况
用的不多,暂省略
c++虚函数实现多态的原理:
若编译器发现一个类中有虚函数表,就会立即为此类生成虚函数表,虚函数表的各表项为指向对应虚函数代码的指针,如果是派生类并重写了虚函数,会用新的重写虚函数的地址代替原来的地址。
虚函数表举例:
三、纯虚函数和抽象类
3.1 纯虚函数
纯虚函数是一种特殊的虚函数。包含纯虚函数的类称为抽象类。
虚函数的声明格式
virtual 函数类型 函数名(参数表) = 0;
3.2 抽象类
抽象类是一种特殊的类,其中至少有一个纯虚函数。
抽象类的特点:
- 抽象类只能作用于其他类的基类,不能建立抽象类对象;
- 抽象类不能用作参数类型,函数返回类型或显示转换的类型;
- 可以定义指向抽象类的指针和引用,可以指向派生类。
四、虚函数常见错误问题汇总(持续更新)
4.1 marked override, but does not override
c++11中引入了override关键字,被override修饰的函数其派生类必须重载。
【运维经】第45章——marked override, but does not override_夏洛的克-CSDN博客
1.在函数比较多的情况下可以提示读者某个函数重写了基类虚函数(表示这个虚函数是从基类继承,不是派生类自己定义的);
2.强制编译器检查某个函数是否重写基类虚函数,如果没有则报错。
4.2 C++中父类的虚函数必须要实现吗?
参考文献:
【1】C++:为什么在继承关系中,父类的析构函数最好定义为虚函数?:C++:为什么在继承关系中,父类的析构函数最好定义为虚函数?_来信-CSDN博客_父类析构函数定义为虚函数
【2】C++ 重载、重写(覆盖)、隐藏的定义与区别:C++ 重载、重写(覆盖)、隐藏的定义与区别_YoungYangD的博客-CSDN博客_c++隐藏和覆盖的区别