1、静态类型和动态类型
如果大家希望学好C++的话那请务必多学习一下c++内部的对象模型,前面有很多博客都是关于这方面的不过都不是自己写的(因为自己很懒)。
举个例子:类A,类B,如果B没有继承A那这就没什么关系了,不过我这里主要说的就是说它们有关系,所以我这里就假如B继承了A。那B将继承A的某些特性(比如成员函数,成员变量等等)。那假如没有什么虚函数什么的,那这种关系如下:
B b;
而如果有虚函数的话(A有,B无)(A无,B有)(A有,B有并重写)模型如下。
B b
这里我就给出最简单的图,详细的可找相关资料看下,该图说明一下,只要A或者B类有虚函数那编译器就会为他生成vtable和type_info其中都是为了动态检查用的。vtable是虚函数用的,而type_info为了支持RTTI用的。
那我们看重点静态类型和动态类型
静态多态性:函数多态性——函数重载
模板多态性——C++模板(类模板、函数模板)
动态多态性:虚函数(只有用地址才能实现动态多态性)
只有采用“指针->函数()”或“引用变量.函数()”的方式调用C++类中的虚函数才会执行动态绑定。对于C++中的非虚函数,因为其不具备动态绑定的特征,所以不管采用什么样的方式调用,都不会执行动态绑定。
代码形式 | 对于虚函数 | 对于非虚函数 | ||
作用 | 绑定方式 | 作用 | 绑定方式 | |
类名::函数() | 调用指定类的指定函数 | 静态绑定 | 调用指定类的指定函数 | 静态绑定 |
对象名.函数() | 调用指定对象的指定函数 | 静态绑定 | 调用指定对象的指定函数 | 静态绑定 |
引用变量.函数() | 调用被引用对象所属类的指定函数 | 动态绑定 | 调用引用变量所属类的指定函数 | 静态绑定 |
指针->函数() | 调用被引用对象所属类的指定函数 | 动态绑定 | 调用指针变量所属类的指定函数 | 静态绑定 |
注:被引用对象所属类 是 指针 或 引用 指向的对象的实际类型;
引用变量所属类、指针变量所属类 是 定义 引用变量、指针变量的类型;
以上两种类型可能相同,也可能不同。
下面看一个例子:
- <span style="font-size:18px">#include <iostream>
- using namespace std;
- class A
- {
- public:
- void fun()
- {
- cout<<"A::fun"<<endl;
- }
- void fun2()
- {
- cout<<"A::fun2"<<endl;
- }
- };
- class B:public A
- {
- public:
- void fun()
- {
- cout<<"B::fun"<<endl;
- }
- void fun3()
- {
- cout<<"B::fun3()"<<endl;
- }
- };
- void main()
- {
- A a;
- a.fun();//静态检查
- B b;
- b.fun();//静态检查
- b.fun2();//继承会将函数也继承
- A *x = &b;
- x->fun();
- x->fun2();
- x->fun3();
- }
- </span>
上面解释一下,第一句和第三句以为都是对象所以进行静态检查所以调用对应的自己函数,而第五句因为B继承了A的成员函数所以也可以调用(但此时要注意调用权限这里为public),到第六句出现了指针所以后三句都将进行动态检查但由于该程序没有虚函数所以不会进行动态绑定。首先编译器知道x静态为A类所以x->fun();即为A::fun,同理fun2(),但最后一句就会出现错误了,因为fun3()不是父类的就相当于父类无法继承子类。
那如果程序改一下,将A类中fun()声明为虚函数。B类fun3()声明为虚函数,那执行过程将是x静态为A*,不过编译器有type_info所以知道指向的是B对象,因此执行虚函数fun的时候将通过vptr绑定到B的fun,而fun2不是虚函数也就不会去用vptr到vtable里找执行函数所以不会动态绑定。
所以我们也就知道动态绑定需要额外的时间开销的。
另外如果B中fun3为虚函数那x->fun3()是否成功呢?答案是不可以的,因为首先x是A*此时A中并没有fun3的类型说明所以就报错说A中没有该成员函数。
那我们就可以总结下:c++实现并没有将对虚函数的调用采用运行时进行而是派生类定义中的名字(对象或函数名)将义无反顾地遮蔽(即隐藏)掉基类中任何同名的对象或函数(跟普通函数一样的),只是说虚函数的调用会进行转换(通过vptr到vtable里面查找),而普通函数只进行简单的调用。
上面简单来说就是说比如父类指针指向子类对象,那首先指针进行静态检查时A*的,然后看看调用的函数是虚函数还是普通成员函数,如果是虚函数那该指针通过vptr到vtable里面找改函数对应得函数指针如果该函数被子类修改过了那该槽被改写了那将调用子类的该函数,而如果没有被修改也就是说该槽没有被重写那就直接调用父类自己的该虚函数。第二,如果该函数是父类的普通成员函数的话那直接进行调用不管子类有没重写过,因为进行静态检查该指针只是父类型的。
所以最后说一句:虚函数调用语句的参数类型检查并没有采用推迟到运行时进行的。其实一开始就已经静态检查好了,只是调用的时候进行动态寻址而已。
再说一句,呵呵:为什么父类要是虚析构函数。我想应该清楚了吧!
因为首先怕出现这样的情况:A* a = new B();
那么当delete a;的时候因为a静态检查时A*那如果不是虚函数的直接调用~a了。因为vtable的槽没有被子类改写所以会出现无法调用~B()。而假如写了呢那将出现就相当于~B()改写了~A()对应的槽变为了~B(),所有delete a先调用~B()然后调用父类~A()(这个调用子类析构函数然后调用父类析构函数是编译器规定的)。怎么样大家清楚了吧~