虚函数的相关知识点
概念
联编:
1. 联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的操作调用(函数调用)与执行该操作(函数)的代码段之间的映射关系;按照联编所进行的阶段不同,可分为 态联编和动态联编;
2. 通常来说联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序彼此关联的过程。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。
静态联编和动态联编的定义:
静态联编是指在编译阶段就将函数实现和函数调用关联起来,因此静态联编也叫早绑定,在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型,C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。
动态联编是指在程序执行的时候才将函数实现和函数调用关联,因此也叫运行时绑定或者晚绑定,动态联编对函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。C++中一般情况下联编也是静态联编,但是一旦涉及到多态和虚拟函数就必须要使用动态联编了。下面将介绍一下多态。
多态的实现:
字面的含义是具有多种形式或形态。C++多态有两种形式,动态多态和静态多态;动态多态是指一般的多态,是通过类继承和虚函数机制实现的多态;静态多态是通过模板来实现,因为这种多态实在编译时而非运行时,所以称为静态多态。
实现过程:
静态联编:主要是通过函数重载和操作数重载来实现
动态联编:主要是靠虚函数来实现
代码理解和分析:(链接)
1 #include <iostream> 2 using namespace std; 3 4 class Shape { 5 protected: 6 int width, height; 7 public: 8 Shape( int a=0, int b=0) 9 { 10 width = a; 11 height = b; 12 } 13 int area() 14 { 15 cout << "Parent class area :" <<endl; 16 return 0; 17 } 18 }; 19 class Rectangle: public Shape{ 20 public: 21 Rectangle( int a=0, int b=0):Shape(a, b) { } 22 int area () 23 { 24 cout << "Rectangle class area :" <<endl; 25 return (width * height); 26 } 27 }; 28 class Triangle: public Shape{ 29 public: 30 Triangle( int a=0, int b=0):Shape(a, b) { } 31 int area () 32 { 33 cout << "Triangle class area :" <<endl; 34 return (width * height / 2); 35 } 36 }; 37 // 程序的主函数 38 int main( ) 39 { 40 Shape *shape; 41 Rectangle rec(10,7); 42 Triangle tri(10,5); 43 44 // 存储矩形的地址 45 shape = &rec; 46 // 调用矩形的求面积函数 area 47 shape->area(); 48 49 // 存储三角形的地址 50 shape = &tri; 51 // 调用三角形的求面积函数 area 52 shape->area(); 53 54 return 0; 55 }
输出:
Parent class area
Parent class area
代码中没有使用virtual关键字,这是静态联编的过程,因为 area() 函数在程序编译期间就已经设置好了,参考下面的解释更好理解。
基类的指针指向派生类的对象,当调用同名的成员函数时:(原文链接)
1)如果在基类中成员函数为虚函数,那么基类指针调用的就是派生类的同名函数。virtual void display();
可以这么理解:因为该函数是虚的,所以会找真正实现的那个函数,所以调用派生类B中的 B class virtual display.
2)如果基类中成员函数为非虚函数,则调用的是基类的成员函数。void show();
因为基类是非虚的,已经完全实现了,所以没有必要再调用派生类的了,就调用基类的A class show()
如果改为下面代码:
1 class Shape { 2 protected: 3 int width, height; 4 public: 5 Shape( int a=0, int b=0) 6 { 7 width = a; 8 height = b; 9 } 10 virtual int area() 11 { 12 cout << "Parent class area :" <<endl; 13 return 0; 14 } 15 };
输出:
Rectangle class area
Triangle class area
虚函数的实现机制:
动态绑定:
动态绑定是通过虚函数表实现的对于含有虚函数的多态,编译器为每个对象生成一个虚表指针,即在每个对象的内存映像中增加了一个_vfptr指针,它指向一个虚函数vtable。
例如:在基类的虚函数表中列出基类所有虚函数的入口地址。
每个类提供一个虚函数表为每个对象提供一个虚表指针,虚表指针指向虚函数表。
实现条件:
- 公共继承是基础,
- 基类有说明的虚函数
- 调用虚函数操作的是指向对象的指针或者对象的引用,或由成员函数调用虚函数
限制:
- 如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。
- 只有类的成员函数才能说明为虚函数,因为虚函数仅适合用于有继承关系的类对象,所以普通函数不能说明为虚函数。
- 静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。
- 内联(inline)函数不能是虚函数,因为内联函数不能再运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。
- 构造函数不能是虚函数,因为构造的时候,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。
- 析构函数可以是虚函数,而且通常声明为虚函数
总结:
- 基类指针加virtual关键字,所有的派生类都会默认自动加上virtual关键字。
- 若类中成员函数被说明为虚函数,改成原函数在派生类中可能有不同的实现
- 使用该成员函数操作指针或者引用所标识的对象时,对该成员函数调用可以采用动态联编方式
函数重载,重写和隐藏:
函数重载:
- 在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样);
- 不能通过访问权限、返回类型、抛出的异常进行重载
- 方法的异常类型和数目不会对重载造成影响;
函数重写:
- 被重写的函数不能是static的。必须是virtual的
- 重写函数必须有相同的类型,名称和参数列表
- 重写函数的访问修饰符可以不同。
函数隐藏:
- 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏(如果相同有Virtual就是重写覆盖了)。
虚析构函数:
- 为了解决:基类的指针指向派生类对象,并用基类的指针删除派生类对象的问题。
- 一般来说当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
- 抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
- 抽象类:
- 在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象;
- 抽象类只能作为基类使用,其纯虚函数的实现由派生类给出;但派生类仍可不给出纯虚函数的定义,继续作为抽象类存在
- 抽象类是不能实例化的,不能生成抽象基类的对象,因此一般将该类的构造函数说明为保护的访问控制权限。抽象类的作用:
- 只能作为别的类的基类。
- 抽象类的作用:
- 用作基类:在一个继承层次结构中,提供一个公共的根,并基于抽象类的操作,设计出对抽象类所描述的一类对象进行操作的公共接口,其完整的实现由派生类完成。
- 用作指针或引用的基类型:保证进入继承层次的每个类都具有(提供)纯虚函数所要求的行为。
- 在成员函数内可以调用纯虚函数,但在构造函数或析构函数内不能调用纯虚函数(纯虚函数没有实现代码)
纯虚函数:
纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
作用:为派生类提供一个一致的接口。
声明:
class<类名> { virtual <类型><函数名>(<参数表>) = 0; ..... };
- 用作基类:在一个继承层次结构中,提供一个公共的根,并基于抽象类的操作,设计出对抽象类所描述的一类对象进行操作的公共接口,其完整的实现由派生类完成。
- 用作指针或引用的基类型:保证进入继承层次的每个类都具有(提供)纯虚函数所要求的行为。
- 在成员函数内可以调用纯虚函数,但在构造函数或析构函数内不能调用纯虚函数(纯虚函数没有实现代码)
class A { public : virtual void f() = 0; void g() {f();} //正确 A() {f();} //错误 }