我是荔园微风,作为一名在IT界整整25年的老兵,今天来说说Visual C++中的虚函数和纯虚函数。该系列帖子全部使用我本人自创的对比学习法。也就是当C++学不下去的时候,就用JAVA实现同样的代码,然后再用对比的方法把C++学会。
直接说虚函数和纯虚函数有很多人会直接晕,但是来看这篇帖子的很多人是有JAVA或其他面象对象编程基础的,我要不就先作个类比,究竟什么是虚函数和纯虚函数,其实很简单,初学者可以直接把C++中的虚函数和纯虚函数理解成JAVA中的抽象函数。这样是不是瞬间就明白了?
我也是在实践中发现,如果你先理解了JAVA的抽象类和抽象函数,你再理解C++的虚函数和纯虚函数就会很简单。哈哈,这个方法好吧。
那么理解了我上面说的,我们只需要再区别一下C++中的虚函数和纯虚函数理解成JAVA中的抽象函数,找寻一些他们之间的不同点就可以了。
好了,下面代码部分是本文的精华,请仔细比对下面两段代码,一段是C++,一段是JAVA,他们在实现同一个事情,输出结果也一样,但表达不同。
某游戏公司现要开发一款模拟游戏,该游戏主要模拟现实世界中各种鸭子的发声特征、飞行特征和外观特征。游戏需要模拟的鸭子种类及其特征如下表所示:
鸭子种类 发声特征 飞行特征 外观特征
灰鸭(MallardDuck) 发出“嘎嘎”声(Quack) 用翅膀飞(FlyWithWings) 灰色羽毛
红头鸭(RedHeadDuck) 发出“嘎嘎”声(Quack) 用翅膀飞(FlyWithWings) 灰色羽毛、头部红色
棉花鸭(CottonDuck) 不发声(QuackNoWay) 不能飞行(FlyNoWay) 白色
橡皮鸭(RubberDuck) 橡皮声音(Squeak) 不能飞行(FlyNoWay) 黑白橡皮颜色
为支持将来能够模拟更多种类鸭子的特征,采用策略设计模式。其中,Duck 为抽象类,描述了抽象的鸭子,而类 RubberDuck、MallardDuck、CottonDuck 和RedHeadDuck分别描述具体的鸭子种类,方法fly()、quack()和display()分别表示不同种类的鸭子都具有飞行特征、发声特征和外观特征;接口FlyBehavior与QuackBehavior'分别用于表示抽象的飞行行为与发声行为;类 FlyNoWay与 FlyWithWings分别描述不能飞行的行为和用翅膀飞行的行为:类Quack,Squeak与QuackNoWay分别描述发出“嘎嘎"声的行为、发出橡皮与空气摩擦声的行为与不发声的行为。
我们先用JAVA代码来实现:
interface FlyBehavior{
public void fly();
};
interface QuackBehavior{
public void quack();
};
class FlyWithWings implements FlyBehavior{
public void fly() {System.out.println("使用翅膀飞行!");}
};
class FlyNoWay implements FlyBehavior{
public void fly() {System.out.println("不能飞行!");}
};
class Quack implements QuackBehavior {
public void quack() {System.out.printIn("发出嘎嘎声!"); }
};
class Squeak implements QuackBehavior{
public void quack(){System.out.println("发出空气与橡皮摩擦声 !");}
};
class QuackNoWay implements QuackBehavior{
public void quack() {System.out.println("不能发声!");}
};
abstract class Duck{
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;
public void fly() { flyBehavior.fly();};
public void quack() {quackBehavior.quack();}
public abstract void display();
};
class RubberDuck extends Duck{
public RubberDuck(){
flyBehavior=new FlyNoWay();
quackBehavior=new Squeak();
}
public void display(){/*此处省略显示橡皮鸭代码*/}
};
//其他代码省略
然后我们再用比较难的C++语言写一遍上面这个意思。
#include<iostream>
using namespace std;
class FlyBehavior{
public:virtual void fly()=0;
};
class QuackBehavior{
public:virtual void quack()=0;
};
class FlyWithWings:public FlyBehavior{
public:void fly(){cout<<"使用翅膀飞行 !" <<endl;}
};
class FlyNoWay:public FlyBehavior{
public:void fly() { cout<< "不能飞行!"<<endl;}
};
class Quack:public QuackBehavior{
public:void quack(){ cout<<"发出嘎嘎声 !"<<endl; }
};
class Squeak:public QuackBehavior{
public:void quack() {cout<<"发出空气与橡皮摩擦声!"<<endl;}
};
class QuackNoWay:public QuackBehavior{
public:void quack(){ cout<<"不能发声 !"<<endl; }
};
class Duck{
protected:
FlyBehavior *flyBehavior;
QuackBehavior *quackBehavior;
public:
void fly(){flyBehavior->fly();}
void quack(){quackBehavior->quack();}
virtual void display()=0;
};
class RubberDuck:public Duck{
public:
RubberDuck(){
flyBehavior=new FlyNoWay();
quackBehavior=new Squeak();
}
void display(){/*此省略显示橡皮鸭的代码*/}
};
//其他代码省略
Java抽象类和C++虚基类的不同点
java和C++都是面向对象编程语言,遵循面向对象的特性,继承,封装,多态。由于java的抽象类和C++虚基类很像,本文对二者在这两个概念上进行一些比较。从名称上来讲,标准的概念:
C++:虚函数,虚基类;
java:抽象方法,抽象类,接口。
C++中,虚函数的存在是为了实现多态。C++中用virtual关键字来标识虚函数,即普通成员函数加上virtual就成为虚函数。Java中没有虚函数的概念,它的普通函数就相当于C++的虚函数,动态绑定是Java的默认行为。java中,如果某个方法不想被子类实现,就用final关键字使其变成非虚函数。java抽象函数/C++纯虚函数,其实就是没有方法体的方法,即一个方法只有声明,没有定义(实现)。抽象函数或者说是纯虚函数的存在是为了定义接口。
C++中纯虚函数形式为:virtual void print() = 0;
Java中纯虚函数形式为:abstract void print();
Java抽象类的存在是因为父类中既包括子类共性函数的具体定义,也包括需要子类各自实现的函数接口。抽象类中可以有数据成员和非抽象方法。抽象类中可以没有抽象方法,但具有抽象方法的类必须定义为抽象类,抽象类不能实例化。C++中抽象类只需要包括纯虚函数,就是一个抽象类。如果仅仅包括虚函数,不能定义为抽象类,因为类中其实没有抽象的概念。Java抽象类是用abstract修饰声明的类,而C++中,需要用virtual void print() = 0;的形式来标识。
Java接口用interface来定义。接口中的变量自动具有public static final属性,接口中的方法自动具有public abstract属性,接口允许多继承。接口中不能有普通成员变量,也不能具有非纯虚函数。
C++中没有接口这个概念,如果所有的方法都是纯虚函数,即全虚基类,可以将其视为和java中的接口是同等概念。这些纯虚函数必须要由子类重写,就像java中的接口中的方法必须被实现一样。
虚基类同样不能实例化。纯虚函数不能有自己的函数体,但是纯虚析构函数除外。
JAVA中的抽象函数与C++中的虚函数比较
java中没有虚函数的概念,但是有抽象函数的概念,用abstract关键字表示,java中抽象函数必须在抽象类中,而且抽象函数不能有函数体,抽象类不能被实例化,只能由其子类实现抽象函数,如果某个抽象类的子类仍是抽象类,那么该子类不需要实现其父类的抽象函数。
C++中的有虚函数的概念,用virtual 关键字来表示,每个类都会有一个虚函数表,该虚函数表首先会从父类中继承得到父类的虚函数表, 如果子类中重写了父类的虚函数(不管重写后的函数是否为虚函数),要调用哪个虚函数,是根据当前实际的对象来判断的(不管指针所属类型是否为当前类,有可能是父类型),指针当前指向的是哪种类型的对象,就调用哪个类型中类定义的虚函数。每个类只有一张虚拟函数表,所有的对象共用这张表。C++的函数多态就是通过虚函数来实现的。
C++中,如果函数不是虚函数,则调用某个函数,是根据当前指针类型来判断的,并不是根据指针所指向对象的类型。Java中,如果函数不是抽象函数,而是一个普通函数,它是默认实现类似C++中虚函数功能的,也就是说,调用某个函数,是根据当前指针所指向对象的类型来判断的,而不是根据指针类型判断。正好与C++中的普通函数相反。即JAVA里自动实现了虚函数。
纯虚函数: 主要特征是不能被用来声明对象,是抽象类,是用来确保程序结构与应用域的结构据具有直接映射关系的设计工具。带有纯虚函数的类称为抽象类,抽象类能被子类 继承使用,在子类中必须给出纯虚函数的实现,如果子类未给出该纯虚函数的实现,那么该子类也是抽象类,只有在子类不存在纯虚函数时,子类才可以用来声明对 象!抽象类也能用于声明指针或引用,或用于函数声明中。具有抽象类特性的类还有构造函数和析构函数,全部是保护的类。如果没有给出纯虚函数的实现,则在它所在的类的构造函数或析构函数中不能直接或间接的调用它。纯虚函数的实现可以在类声明外进行定义。
C++中一般都是把析构函数声明为虚函数。因为虚函数可以实现动态绑定,也就是到底调用哪个函数是根据指针当前指向哪个对象来确定的,不是根据指针的类型来确定。如果C++中不把析构函数声明为虚函数,那么其有个子类,重写了虚函数,那么当父类指针指向一个子类对象时,当调用析构函数时,只调用父类的析构函数,而无法调用子类的析构函数,所以一般情况是把析构函数声明为虚函数,实现动态绑定。当然如果一个类不包含虚函数,这经常预示不打算将它作为基类使用。当一个类不打算作为基类时,将析构函数声明为虚拟通常是个坏主意。比如:标准 string 类型不包含虚函数,如果把String作为基类继承得到子类会出问题。
总之:多态基类应该声明虚析构函数。如果一个类有任何虚函数,它就应该有一个虚析构函数;如果不是设计用于做基类或不是设计用于多态,这样的类就不应该声明虚析构函数。
关于接口与抽象类:c++中没有接口的概念,与之对应的是纯虚类,即只含有纯虚函数的类,c++抽象类的概念是含有纯虚函数成员的类。这是因为c++提供多继承,而像java、c#这些只提供单继承(避免多继承的复杂性和低效性)的语言为了模拟多继承功能就提供了接口概念,接口可以继承多个。abstract class是抽象类,至少包含一个纯虚函数的类就叫做抽象类。但是如果一个类,所有的成员都是纯虚函数,那么它和一般的抽象类在用法上是有区别的。至少microsoft给的com接口定义全部都是仅由纯虚函数构成的类。因此把这样的类定义叫做纯虚类也不算错。 纯虚函数和虚函数的区别在于前者不包含定义,而后者包含函数体。 那么纯虚类就是不包含任何实现(包括成员函数定义和成员变量定义。前者代表算法,后者代表结构)。不包含任何算法和结构的类叫做纯虚类,应该没有问题。
在java里面的确没有纯虚类的概念,因为java里没有纯虚函数这个概念。java管虚函数叫做abstract function,管抽象类叫做abstract class,直接说来,java根本没有virtual这个关键字,都用abstract代替,因此java里面根本就没有pure这个概念。有那就是interface。在interface里面定义的函数都不能有函数体,这个在java里面叫做接口。那么c++里面与interface等同的概念就是纯虚类了,c++用纯虚类来模拟interface这个抽象概念,因此这里说的“纯虚类”与java的abstract class不同,与c++的一般抽象类也不同。“纯虚类”与c++一般抽象类的区别就好比java里面interface 和 abstract class的区别。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。抽象类是不能定义对象的。
总结
C++中的虚函数就是JAVA中的普通函数, C++ 中的纯虚函数就是JAVA中的抽象函数, C++ 中的抽象类就是JAVA中的抽象类, C++ 中的虚基类就是JAVA中的接口。
作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。