一、文章来由
Bill又写文章来由了哇~~早就想好好搞清这个问题了,这是c++领域里面比较难搞定的一块知识点,而且最近在看设计模式,里面有涉及这块,之前学过的不用容易玩忘记,于是就干脆研究透一点,也好碰到、用到的时候不心慌~于是有了这篇文章。
二、从编译时和运行时说起
2.1 编译时:
顾名思义就是正在编译的时候。就是编译器帮你把源代码翻译成机器能识别的代码。(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言。比如Java只有JVM识别的字节码,C#中只有CLR能识别的MSIL)
编译时就是简单的作一些翻译工作。如果发现错误编译器就告诉你。比如点击微软的VS点下build,如果下面有errors或者warning信息,那都是编译器检查出来的。所谓这时的错误就叫编译时错误,这个过程中做类型检查叫编译时类型检查,或静态类型检查(所谓静态嘛就是没把真把代码放内存中运行起来,而只是把代码当作文本来扫描下)。所以说编译时还分配内存肯定是错误的。
2.2 运行时:
所谓运行时就是代码跑起来了,被装载到内存中去了。(你的代码保存在磁盘上没装入内存之前是死的,静态的;只有跑到内存中才变成活的)。而运行时类型检查就与前面讲的编译时类型检查(或者静态类型检查)不一样。不是简单的扫描代码。而是在内存中做些操作,做些判断。
例如,在C++中:
int arr[] = {1,2,3};
int result = arr[4];
cout<<result<<endl;
上面的代码错误很明显—数组越界了。但用编译器编译,是不会报错的。可见编译器其实还是挺笨的,然后开始Debug,可能会报错(也可能不报错,本人用的vs2012 debug模式就没有)。但实际上运行时做数组的越界检查不是C++里面支持的特性,这里你dubug是VS中的一些工具给你做的检查。你如果点运行时选的是release而不是debug的话会发现一切正常运行,但得到的结果不确定的(因为不知道arr[4]所指的内存里存放的是什么)。
2.3 c++多态在编译时和运行时【底层机制,很重要】:
那C++为什么不在运行时做数组越界检查呢?
这应该主要是考虑到性能问题吧,C++设计之初为了达到与C差不多的效率,就尽量不会在运行时多做些额外的检查,因为这样无疑会降低性能的,但有些地方却是必须得做运行时类型检查,比如多态,不在运行时做类型检查就无法确定类型。
举个简单例子,假如有父类Father,继承自Father的子类Son,这两个类中都有虚函数Fun
Father fa;
Son so;
fa = so;
fa.Fun(); //在编译时,实际上是把Fun当作Father类中的Fun看待
但在运行时实际上这里的Fun是调用的Son中的函数Fun,所以不做运行时类型检查是无法确定的
2.4 编译时多态和运行时多态
关于编译时和运行时,多态还有一个问题很重要—编译时多态和运行时多态
多态性是面向对象程序设计的重要特征之一。所谓多态性是指当不同的对象收到相同的消息时,产生不同的动作。C++的多态性具体体现在运行和编译两个方面,在程序运行时的多态性通过继承虚函数来体现,而在程序编译时多态性体现在函数和运算符的重载上。
C++支持两种多态性:
编译时多态:程序运行前发生的事件 —— 函数重载、运算符重载 —— 静态绑定
运行时多态:程序运行时发生的事件 —— 虚函数机制 —— 动态绑定
三、多态性小览
上面说了这么多,才开始重头戏。
3.1 多态定义
定义:多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。多态(polymorphism),字面意思多种形状。
C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。
这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性。
而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。
3.2 重载和重写
上面的定义,有了一个定性的认识,C++多态性是通过重写了虚函数实现的,有必要看看重载和重写。
其实这两个概念对于我们来说肯定并不陌生,但是有很多细节的地方容易被忽略。
方法重载:
- 对于面向对象而言,必须在同一个类里面
- 方法名相同
- 参数类型不同 如:public void test ( int i , int j ){} 和 public void test ( int i , float j ){}
- 参数数目不同 如:public void test ( int i ){} 和 public void test ( int i , int j ){}
- 和方法的返回值无关 如:public int test( int i ){} 和 public void test( int i , int j) {} 也属于方法重载
方法重写是指重写父类的方法
1.类必须继承了父类才可以重写父类的方法;
2.必须和父类的方法的返回值,参数列表和方法名一样才算重写!(这点很重要)
3.3 早绑定与晚绑定
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,地址就是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就是晚绑定。
那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。
3.4 代码分析
3.4.1 例1
#include<iostream>
using namespace std;
class A
{
public:
void foo()
{
printf("A类中:1\n");
}
virtual void fun()
{
printf("A类中:2\n");
}
};
class B : public A
{
public:
void foo()
{
printf("B类中:3\n");
}
void fun()
{
printf("B类中:4\n");
}
};
int main(void)
{
A a;
B b;
A *p = &a; //基类指针
p->foo();
p->fun();
cout<<endl;
p = &b;
p->foo();
p->fun();
cout<<endl;
B *ptr = (B *)&a;
ptr->foo();
ptr->fun();
return 0;
}
运行出来的结果如图:
分析:
第一个p->foo()和p->fun()都容易理解。
第二个输出结果是1、4。p->foo()和p->fun()是基类指针指向子类对象,完全体现了多态的用法,p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。
而p->fun()指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4。
还有一个很变态的东东
B *ptr = (B *)&a;
ptr->foo();
ptr->fun();
一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是3,2。
并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现。
所以这种情况只看指针类型。
而ptr->fun()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。
所以这种情况只看绑定对象类型。
由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。
3.4.2 例2
//小结:1、有virtual才可能发生多态现象
// 2、不发生多态(无virtual)调用就按原类型调用
#include<iostream>
using namespace std;
class Base
{
public:
virtual void f1(float x)
{
cout<<"Base::f1(float)"<< x <<endl;
}
virtual void f2(float x)
{
cout<<"Base::f2(float)"<< x <<endl;
}
void f3(float x)
{
cout<<"Base::f3(float)"<< x <<endl;
}
};
class Derived : public Base
{
public:
virtual void f1(float x)
{
cout<<"Derived::f1(float)"<< x <<endl; //多态、重写
}
void f2(int x)
{
cout<<"Derived::f2(int)"<< x <<endl; //隐藏
}
void f3(float x)
{
cout<<"Derived::f3(float)"<< x <<endl; //隐藏
}
};
int main(void)
{
Derived d;
Base *pbase = &d;
Derived *pderi = &d;
// Good : behavior depends solely on type of the object
pbase->f1(3.14f); // Derived::f1(float) 3.14
pderi->f1(3.14f); // Derived::f1(float) 3.14
cout<<endl;
// Bad : behavior depends on type of the pointer
pbase->f2(3.14f); // Base::f2(float) 3.14
pderi->f2(3.14f); // Derived::f2(int) 3
cout<<endl;
// Bad : behavior depends on type of the pointer
pbase->f3(3.14f); // Base::f3(float) 3.14
pderi->f3(3.14f); // Derived::f3(float) 3.14
return 0;
}
运行结果如图所示:
分析:
首先纠正一个错误,很多人认为重写和覆盖不同,其实就是一个东西。
先看第一组输出,这既是重写父类方法,又是实现多态,虽然子类中任然是一个virtual方法,它可以继续被它的子类所重写。
然后关于后面的两组,又一个令人迷惑的概念重磅登场了~~
3.5 令人迷惑的隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。
这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数
规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。第二组就是这样的例子,如果把子类Derived中的void f2( int x ) 改成 void f2( float x ),输出结果就是两个 Derived,此时就是重写父类方法而实现多态了
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
上面的程序中:
(1)函数Derived::f1(float)覆盖了Base::f1(float)。
(2)函数Derived::f2(int)隐藏了Base::f2(float),而不是重载。
(3)函数Derived::f3(float)隐藏了Base::f3(float),而不是覆盖。
四、C++纯虚函数
4.1 定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
如: virtual void funtion()=0
4.2 引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。
例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
4.3 相似概念
1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
a、编译时多态性:通过重载函数实现
b、运行时多态性:通过虚函数实现。
这个在上面也有说明
2、虚函数
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
3、抽象类
包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
五、C++多态的应用
来个首尾呼应,应用是在工程里面,涉及设计模式的复杂的架构会用到多态。
看到设计模式中有:
不必指明用哪一个对象,直接用父类根据指向的子类对象抽象调用不同的方法,这正是c++多态的应用场景。
—END—
参考文献
[1] http://blog.csdn.net/hackbuteer1/article/details/7475622(谢谢作者让我茅塞顿开)