C++纯虚函数virtual=0
天配良缘之商君C++纯虚函数 virtual =0
=========================================================================
bureaucracy
C++中的纯虚函数
在C++中的⼀种函数申明被称之为:纯虚函数(pure virtual function).它的申明格式如下:
class CShape
{
public:
virtual void Show() =0;
};
注意红⾊部分,在普通的虚函数后⾯加上" =0"这样就声明了⼀个pure virtual function.
在什么情况下使⽤纯虚函数(pure vitrual function)?
1,当想在基类中抽象出⼀个⽅法,且该基类只做能被继承,⽽不能被实例化;
2,这个⽅法必须在派⽣类(derived class)中被实现;
如果满⾜以上两点,可以考虑将该⽅法申明为pure virtual function.r h
我们来举个例⼦,我们先定义⼀个形状的类(Cshape),但凡是形状我们都要求其能显⽰⾃⼰。所以我们定义了⼀个类如下:
class CShape
{
virtual void Show() {};
};
experimentation但没有CShape这种形状,因此我们不想让CShape这个类被实例化,我们⾸先想到的是将Show函数的定义(实现)部分删除如下:class CShape
{
virtual void Show();
};
当我们使⽤下⾯的语句实例化⼀个CShape时:
CShape cs; //这是我们不允许的,但仅⽤上⾯的代码是可以通过编译(但link时失败)。
manually怎么样避免⼀个CShape被实例化,且在编译时就被发现?
答案是:使⽤pure virtual funcion.
我们再次修改CShape类如下:
class CShape
{
public:
virtual void Show() =0;
};
这时在实例化CShape时就会有以下报错信息:
error C2259: 'CShape' : cannot instantiate abstract class due to following members:
warning C4259: 'void __thiscall CShape::Show(void)' : pure virtual function was not defined
我们再来看看被继承的情况,我们需要⼀个CPoint2D的类,它继承⾃CShape.他必须实现基类(CShape)中的Show()⽅法。
其实使⽤最初的本意是让每⼀个派⽣⾃CShape的类,都要实现Show()⽅法,但时常我们可能在⼀个派⽣类中忘记了实现Show(),为了避免这种情况,pure virtual funcion发挥作⽤了。
我们看以下代码:
class CPoint2D:public CShape
{
public:
CPoint2D()
{
printf("CPoint2D ctor is invoked/n");
};
void Msg()
{
printf("CPoint2D.Msg() is invoked/n");
};
/*---I'm sorry to forget implement the Show()---
void Show()
guard{
printf("Show() from CPoint2D/n");
};
------------------------------------------------*/
};
当我们实例化CPoint2D时,在编译时(at the compiling)也会出现如下的报错:
error C2259: 'CShape' : cannot instantiate abstract class due to following members: warning C4259: 'void __thiscall CShape::Show(void)' : pure virtual function was not defined 如上,我们预防了在派⽣类中忘记实现基类⽅法。
也许compiler会说:
哼!如果不在派⽣类的中实现在Show⽅法,我编译都不会让你通过。
//--------------------------------------------------------
//now,show the completed code,
//Platform:Winxp+VC6.0
//--------------
#include <iostream>
#include <stdio.h>
using namespace std;
亲切的什么class CShape
{
public:
virtual void Show()=0;
};
class CPoint2D:public CShape
{
public:
void Msg()corgi
{
printf("CPoint2D.Msg() is invoked/n");
};
/*---I'm sorry to forget implementation of the Show()--- */
void Show()
{
printf("Show() from CPoint2D/n");
};
/*------------------------------------------------------*/
};
void main()
{
CPoint2D p2d; //如果派⽣类(CPoint2D)没有实现Show(),则编译不通过
p2d.Msg();
//
CShape *pShape = &p2d;
pShape->Show();
//不能实例化基类
/
/CShape cs;
}
=========================================================================
C++中的虚函数(virtual function)
1.简介
虚函数是C++中⽤于实现多态(polymorphism)的机制。核⼼理念就是通过基类访问派⽣类定义的函数。假设我们有下⾯的类层次:
class A
{
public:
virtual void foo() { cout << "A::foo() is called" << endl;}
};
class B: public A
{
public:
virtual void foo() { cout << "B::foo() is called" << endl;}
};
那么,在使⽤的时候,我们可以:
A * a = new B();
a->foo(); // 在这⾥,a虽然是指向A的指针,但是被调⽤的函数(foo)却是B的!
这个例⼦是虚函数的⼀个典型应⽤,通过这个例⼦,也许你就对虚函数有了⼀些概念。它虚就虚在所谓“推迟联编”或者“动态联
编”上,⼀个类函数的调⽤并不是在编译时刻被确定的,⽽是在运⾏时刻被确定的。由于编写代码的时候并不能确定被调⽤的是基类的函数还是哪个派⽣类的函数,所以被成为“虚”函数。
虚函数只能借助于指针或者引⽤来达到多态的效果,如果是下⾯这样的代码,则虽然是虚函数,但它不是多态的:
class A
{
public:
virtual void foo();
};
class B: public A
{
virtual void foo();
};
void bar()
void bar()
{
A a;
a.foo(); // A::foo()被调⽤
}
1.1 多态
在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上⾯的类层次,但是使⽤的⽅法变的复杂了⼀些:
void bar(A * a)
{
a->foo(); // 被调⽤的是A::foo() 还是B::foo()?
}
因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,⽆从确定这⾥被调⽤的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调⽤,如果a指向的是B类的实例,则B::foo()被调⽤。
这种同⼀代码可以产⽣不同效果的特点,被称为“多态”。
1.2 多态有什么⽤?
多态这么神奇,但是能⽤来做什么呢?这个命题我难以⽤⼀两句话概括,⼀般的C++教程(或者其它⾯向对象语⾔的教程)都⽤⼀个画图的例⼦来展⽰多态的⽤途,我就不再重复这个例⼦了,如果你不知道这个例⼦,随便找本书应该都有介绍。我试图从⼀个抽象的⾓度描述⼀下,回头再结合那个画图的例⼦,也许你就更容易理解。
在⾯向对象的编程中,⾸先会针对数据进⾏抽象(确定基类)和继承(确定派⽣类),构成类层次。这个类层次的使⽤者在使⽤它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派⽣类的时候写针对派⽣类的代码,就等于类层次完全暴露在使⽤者⾯前。如果这个类层次有任何的改变(增加了新类),都需要使⽤者“知道”(针对新类写代码)。这样就增加了类层次与其使⽤者之间的耦合,有⼈把这种情况列为程序中的“bad smell”之⼀。
多态可以使程序员脱离这种窘境。再回头看看1.1中的例⼦,bar()作为A-B这个类层次的使⽤者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是⼀样可以很好的⼯作,当有⼀个C类从A类派⽣出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产⽣了可以在运⾏时刻确定被调⽤函数的代码。
1.3 如何“动态联编”
编译器是如何针对虚函数产⽣可以再运⾏时刻确定被调⽤函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman 在深度探索C++对象模型[1]中的不同章节讲到了⼏种⽅式,这⾥把“标准的”⽅式简单介绍⼀下。
我所说的“标准”⽅式,也就是所谓的“VTABLE”机制。编译器发现⼀个类中有被声明为virtual的函数,就会为其搞⼀个虚函数表,也就是VTABLE。VTABLE实际上是⼀个函数指针的数组,每个虚函数占⽤这个数组的⼀个slot。⼀个类只有⼀个VTABLE,不管它有多少个实例。派⽣类有⾃⼰的VTABLE,但是派⽣类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加⼀个vptr字段,该字段指向本类的VTABLE。通过这些⼿段,编译器在看到⼀个虚函数调⽤的时候,就会将这个调⽤改写,针对1.1中的例⼦:
void bar(A * a)
{
a->foo();
}
会被改写为:
void bar(A * a)
{
(a->vptr[1])();
}
因为派⽣类和基类的foo()函数具有相同的VTABLE索引,⽽他们的vptr⼜指向不同的VTABLE,因此通过这样的⽅法可以在运⾏时刻决
因为派⽣类和基类的foo()函数具有相同的VTABLE索引,⽽他们的vptr⼜指向不同的VTABLE,因此通过这样的⽅法可以在运⾏时刻决定调⽤哪个foo()函数。
虽然实际情况远⾮这么简单,但是基本原理⼤致如此。
1.4 overload和override
虚函数总是在派⽣类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类
C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清⼀下:
override是指派⽣类重写基类的虚函数,就象我们前⾯B类中重写了A类中的foo()函数。重写的函数必须有⼀致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器⽀持这个feature)。这个单词好象⼀直没有什么合适的中⽂词汇来对应,有⼈译为“覆盖”,还贴切⼀些。
overload约定成俗的被翻译为“重载”。是指编写⼀个与已有函数同名但是参数表不同的函数。例如⼀个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
2. 虚函数的语法
虚函数的标志是“virtual”关键字。
2.1 使⽤virtual关键字
考虑下⾯的类层次:
class A
{
public:出售无形资产
virtual void foo();
};
class B: public A
{
public:
void foo(); // 没有virtual关键字!
};
class C: public B // 从B继承,不是从A继承!
{
public:
void foo(); // 也没有virtual关键字!
};
这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派⽣类中也是虚函数,即使不再使⽤virtual关键字。
2.2 纯虚函数
如下声明表⽰⼀个函数为纯虚函数:
class A
{
西安网站开发培训
public:
virtual void foo()=0; // =0标志⼀个虚函数为纯虚函数
};
⼀个函数声明为纯虚后,纯虚函数的意思是:我是⼀个抽象类!不要把我实例化!纯虚函数⽤来规范派⽣类的⾏为,实际上就是所谓
的“接⼝”。它告诉使⽤者,我的派⽣类都会有这个函数。
2.3 虚析构函数
析构函数也可以是虚的,甚⾄是纯虚的。例如: