【C++】静态多态和动态多态,虚函数的调用一定是动态绑定吗?
我们知道,
多态指的意思就是一个函数名有多种状态,同样的函数名有通过函数重载,函数模板,虚函数,可以有不同的代码实现。
也知道,
静态多态—主要是指函数重载和函数模板。在编译时的多态。
动态多态—主要指虚函数的使用。在运行时的多态。
实际上我还有疑问
- 问题1: 什么叫编译时的多态和运行时的多态?静态绑定和动态绑定是是什么?
- 问题2:为什么函数重载和模板就能够在编译的时候实现多态,**虚函数的调用一定是动态绑定吗?**答案是否
一、首先,我们要知道 怎么实现多态的?
1.函数重载 和 函数模板 实现多态
实际上这两种我们接触的非常多,简单讲一下就是
- 函数重载 就是 一个 相同名字的函数,由于参数 类型和个数不同,对应这不同的函数内容。
- 函数模板 就是利用了模板,我们不指定参数类型,提前实现函数内容。
这里确实就很简单的实现了多态(一种函数多种方法)
2.虚函数 实现多态
一开始,我对虚函数的理解非常浅,我的理解就是
- 基类会 使用virtual关键字,指明一个函数为虚函数,然后子类 可以重写这个虚函数。实际上理解就是虚函数好像是一种提供接口的作用。好像并没有体现多态。
那,虚函数的多态的怎么实现的呢? 有三点要求
- 1.基类要有虚函数
- 2.要有父类指针指向子类对象,或者对子类引用。
- 3.子类对虚函数进行重写
我们需要知道一点,一个子类对象是可以给一个父类对象赋值的。也就是对应上面的第二点。
#include <iostream>
#include <string>
#include <vector>
//这里 我们先构造 一个基类Base
class Base
{
public:
Base(){
};
~Base(){
};
public:
virtual void fun1(){
std::cout<< "fun1--base--virtual" <<std::endl;};
void fun2(){
std::cout<< "fun2--base" <<std::endl;};
};
//子类1
class Child:public Base
{
public:
Child(){
};
~Child(){
};
public:
void fun1(){
std::cout<< "fun1--child--overwrite" <<std::endl;};
void fun2(){
std::cout<< "fun2--child" <<std::endl;};
};
int main()
{
Base* pBase = new Base;
Child* pChild = new Child;
pBase = pChild;//将一个子类对象 赋值 给基类
pBase->fun1();//基类调用 虚函数
pBase->fun2();//基类调用 非虚函数
pChild->fun1();//子类调用 重写后的虚函数
pChild->fun2();//子类调用 重写的非虚函数
getchar();
return 0 ;
}
执行结果:
fun1--child--overwrite
fun2--base
fun1--child--overwrite
fun2--child
我们可以看到, 当我们把一个基类对象 赋值 给一个父类对象,由于我们的基类对虚函数进行了重写,
虚函数从结果1,3行来看,无论输出的结果都是重写以后的。都执行的结果是子类重写当中的fun2。
非虚函数 从2,4行来看,还是执行原来类内的函数,基类对象执行基类内的fun2,子类对象执行子类内的fun2.
到此,我们考虑一个问题,
对于一个基类对象,如果我们用不同的衍生类(子类)对其进行赋值,那是不是这个对象就会调用不同的重写函数呢。这样,基类当中的函数,是不是就起到了多态的效果。
二、为什么叫动态联编。动态
实际上,到此,我还有一个疑问,就是为什么这样的实现就是叫做动态联编呢?或者说什么情况下才叫做动态联编呢?
我们开始引入两个概念。
1.静态联编和动态联编(binding)也称为静态绑定和动态绑定。
所谓绑定, 就是我们要通过一个函数名找到对应的代码块。
程序调用函数时,将使用哪个可执行代码块?编译器负责回答这个问题。将源代码中的函数调用解释为执行的函数代码被称为函数名联编(绑定)。在c语言中,因为每个函数名都对应一个不同的函数,很容易找到对应的代码块(进行联编)。
在C++中,由于引入了多态的概念。导致,通过一个函数名找到一个对应函数的方法比较困难。
-
由于函数重载和函数模板引入的多态问题,编译器就能够解决,编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而C/C++编译器可以在编译过程中完成这种联编。在编译过程中进行联编被称为静态联编,又称为早期联编。
-
虚函数的出现,使这项工作变得十分困难,使用哪个函数是不能在编译时确定的。只有在函数运行当中才能够选择正确的虚方法的代码。
至此,**问题1: 什么叫编译时的多态和运行时的多态?静态绑定和动态绑定是是什么?**我们差不多可以理解了。一个是编译时就能找到对应代码块,一个是只有执行时才能够找到代码块。
2.用虚函数一定是动态联编吗?不一定
虚函数在编译时进行联编,静态绑定
Base* pBase_1 = new Base;
Child_1* pChild_1 = new Child_1;
pBase_1 = pChild_1;//将一个子类对象 赋值 给基类
pBase_1->fun1();调用虚函数
Base* pBase_2 = new Base;
Child_2* pChild_2 = new Child_2;
pBase_2 = pChild_2;//将一个子类对象 赋值 给基类
pBase_2->fun1();
对于这个代码。
- 第一行 创建一个基类对象,pBase_1
- 第二行 创建一个子类对象,pChild_1
- 第三行 将子类对象 赋值 给基类 对象。
- 第四行 调用虚函数fun1 ,至此基类对象pBase_1,调用那个函数块,是很确定的,就是子类1中的fun1()。
- 即使我们继续,
- pBase_2->fun1();pBase_2调用的函数块就是子类2当中的fun1()。
- 所以,既然能够找到对应的代码块,那他就应该在编译的时候,就能够联编或者绑定。这种情况下应该是静态联编。
虚函数在运行时进行联编,动态绑定
还存在一种情况,就是用一个基类对象来接收 子类对象 参数。这个参数类型在编译的时候不确定。
void CallVirtualFun(Base* Base){
Base->fun1();
return ;
}
把这个函数当作对外接口,只有在程序运行的时候,再输入一个基类的派生类,用基类进行接收,这个时候,Base->fun1();才会绑定对应的代码块。
所以,这个时候才体现出虚函数的动态联编。