赞
踩
虚函数是应在派生类中重新定义的函数。当使用指针或者对基类的引用来引用派生类的对象时,可以为该对象调用虚函数并执行该派生类的版本。
虚函数的“虚”,虚在“推迟联编/动态联编”上,一个类函数的调用不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为"虚"函数。
虚函数的一般实现模型:
vptr
,指向相应的虚函数表为了支持虚函数机制,必须首先能够对于多态对象有某种形式的执行期类型判断方法。
也就是说,以下的调用操作将需要ptr
在执行期的某些相关信息:
ptr->z();
这样才能够找到并调用z()的适当实体
最直接的策略是把必要的信息加到ptr
身上。此时,一个指针(或者引用)含有两项信息:
这个方法带来了两个问题:
那要把这些必要的信息放在类对象本身而不是指针上吗?还是会有上面两个问题。
从上面可以看出,只要在那些类真正需要这些额外信息的时候才存在比较好。那么,到底何时才需要这些信息呢?很明显在必须支持某种形式的执行期多态的时候。
在C++中,多态表示以一个public基类的指针或引用,寻址出一个派生类对象的意思。 比如对于:
Point *ptr;
可以指定ptr寻址出一个Point2d或者Point3d对象:
ptr = new Point2d;
// ...
ptr = new Point3d;
对积极动态,C++有两个支持:
ptr->z();
if(Point3d *pd3d = dynamic_cast<Point3d *>(ptr)){
}
综上:
下一个问题:什么样的额外信息是需要我们存储起来的?也就是说,对于调用:
ptr->z(); // z()是虚函数
什么信息才能让我们在执行期调用正确的z()实体?
在实现上,首先我们可以在每一个多态类对象上增加两个成员:
一个类只会有一个虚函数表,每个表内含其对应的类对象中是由激活的虚函数函数实体的地址。
pure_virtual_called()
函数实体,它既可以扮演纯虚函数的空间保卫者角色,也可以当作执行期异常处理函数class Point{
public:
virtual ~Point();
virtual Point& mult(float) = 0;
float x() const { return _x;}
virtual float y() const {return 0; }
virtual float z() const {return 0; }
protected:
Point(float x = 0.0);
float _x;
};
每一个虚函数都被指定一个固定的索引值。比如:
pure_virtual_called()
的函数地址会被放在slot 2中。如果该函数意外被调用,通常的操作是结束掉这个程序当一个类派生自Point时:
slot
中class Point2d : public Point{
public:
Point2d(float x = 0.0, float y = 0.0)
: Point(x), _y(y){}
~Point2d();
//改写 基类虚函数
Point2d& mult(float);
float y() const { return _y;}
protected:
float _y;
};
Point2d的虚函数表:
同样,Point3d派生自Point2d:
class Point3d : public Point2d{
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
: Point2d(x, y), _z (z) {}
~Point3d();
//改写 基类虚函数
Point3d& mult(float);
float z() const { return _z;}
protected:
float _z;
};
其虚函数表:
现在对于:
ptr->z();
应该怎么在编译期设定虚函数的调用呢?
这些信息使得编译器可以将调用转换为:
(*ptr->vptr[4])(ptr)
在这个转换中,vptr表示编译器所安插的指针,指向虚函数表;4表示z()被赋予的slot编号(关联到Point体系的虚函数表)。唯一一个在执行期才能够知道的东西是:slot4所指的到底是哪一个z()函数实体。
class Point{
public:
Point(float x = 0.0, float y = 0.0)
: _x(x), _y(y){}
virtual float z();
protected:
float _x, _y;
};
虚函数的引用将初始每一个Point object拥有一个虚函数表。
我们来看看虚函数是怎么引发类的代码膨胀的。
// 代码膨胀
Point* Point::Point(Point *this, float x, float y)
: _x(x), _y(y){
//设定object的virtual table pointer(vptr)
this->__vptr_Point = __vptr_Point ;
// 扩展member initialization list
this->_x = x;
this->_y = y;
//传回this对象
return this;
}
// 拷贝构造函数的内部合成
inline Point* Point::Point(Point *this, const Point &rhs){
//设定object的virtual table pointer(vptr)
this->__vptr_Point = __vptr_Point ;
// 将rhs坐标中的连续位拷贝到this对象,
// 或者是经由member assignmnt提供给一个member
//传回this对象
return this;
}
在单一继承体系中,虚函数机制的行为有效率而且很容易实现。但是在多重继承和虚拟继承中,对虚函数的支持就没那么好了
在多重继承中支持的虚函数中,其复杂度主要在第二个以及后继的基类上,以及"必须在执行期调整this指针"这一点。
class Base1{ public: Base1(); virtual ~Base1(); virtual void speakClearly(); virtual Base1 *clone() const; protected: float data_Base1; }; class Base2{ public: Base2(); virtual ~Base2(); virtual void mumble(); virtual Base2*clone() const; protected: float data_Base2; }; class Derived : public Base1, public Base2{ public: Derived(); virtual ~Derived(); virtual Derived *clone() const; protected: float data_Derived; };
对于上面的Derived而言,会有两个虚函数表被编译期产生:
针对每一个虚函数表,Derived对象中有对应的vptr
。vptr将在构造函数中设置初值(由编译期产生的码)
用以支持一个类拥有多个虚函数表的传统方法是:将每一个表以外部对象的形式产生出来,并给与独一无二的名称。比如,派生类所关联的两个表可能由这样的名称:
vtbl_Derived; // 主要表格
vtvl_Base2_Derived; // 次要表格
所以将一个Derived对象地址指定给一个Base1指针或者Derived指针式,被处理的虚函数表是主要表格vtbl_Derived。当将一个Derived对象指定给一个Base2指针式,被处理的虚函数表是次要表格vtvl_Base2_Derived
多个表的符号链接可能会很慢,为了调节执行期链接器的效率,可以将多个虚函数表连锁成一个:指向次要表格的指针,可以由主要表格名称加上一个offset获得。在这样的策略下,每个类只有一个具名虚函数表
有三种情况,第二或后继的基类对影响对虚函数的支持。
Base2 *ptr = new Derived; // ptr指向Derived对象中的Base2子对象
// 调用Derived::~Derived, ptr必须调整指向Derived对象的起始处,也就是说,ptr必须向后调整sizeof(Base1)个bytes
delete ptr;
Derived *ptr = new Derived;
// ptr必须向前调整sizeof(Base1)个bytes
ptr->mumble();
Base2 *pb1 = new Derived;
// 调用 Derived *Derived::clone()
// 返回值必须调整,以指向Base2子对象
Base2 *pb2 = pbl->clone();
当pb1->clone()时,pb1会被调整指向Derived对象的起使地址,于是clone()的Derived版被调用:它会传回一个指针,指向一个新的Derived对象,该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject
多继承经常导致对虚基类的需求。没有虚基类,如果一个派生类有一个以上从基类的继承路径,基类的数据成员被复制到每一个继承类对象里,继承类和基类间的每条路径都有一个拷贝,程序员一般不会发生这种复制,而把基类定义为虚基类可以消除这种复制。
如果在vtbl加入这个对象的图片,那么D的内存布局如下:
上面虽然存在四个类,但是只有三个vptr,这是因为B和D共享了一个vptr。
class Point{ public: Point(float x = 0.0, float y = 0.0) : _x(x), _y(y){} Point(const Point&); Point& operator=(const Point&); virtual ~Point(); virtual float z() {return 0.0;}; protected: float _x, _y; }; class Point3d : public virtual Point{ public: Point3d(float x = 0.0, float y = 0.0, float z = 0.0) : Point(x, y), _z(z) {} Point3d(const Point3d& rhs) : Point(rhs), _z(rhs._z){} ~Point3d(); Point3d& operator=(const Point3d&); virtual float z(){return _z;} protected: float _z; }; class Vertex : virtual public Point{}; class Vertex3d : public Point3d, public Vertex{}; class PVertex : public Vertex3d {};
当Point3d和Vertex同为Vertex3d的subobject时它们对Point构造函数的调用一定不可以发生,取而代之的是,作为一个最底层的类,Vertext3d有责任将Point初始化。而更后面的继承,则有PVertex来负责完成被共享的Point subobject的构造。
因此,Point3d的构造函数的扩充内容:
// 在虚基类情况下的构造函数的扩充内容
Point3d * Point3d::Point3d(Point3d *this, bool __most_derived, float x, float y, float z){
if(__most_derived != false){
this->Point::Point(x, y);
}
this->__vptr_Point3d = __vtbl_Point3d;
this->__vptr_Point3d_Point = __vptr_Point3d_Point ;
this->_z = z;
return this;
}
在更深层的继承情况下,比如Vertex3d,当调用Point3d和Vertex的constructor时,总会把__most_derived参数设为false,于是就压制了两个constructors中对Point constructor的调用操作:
// 在虚基类情况下的构造函数的扩充内容 Vertex3d * Vertex3d::Vertex3d (Vertex3d *this, bool __most_derived, float x, float y, float z){ if(__most_derived != false){ this->Point::Point(x, y); } //调用上一层的基类 // 设定__most_derived = false; this->Point3d::Point3d(false, x, y, z); this->Vertex::Vertex(flase, x, y); // 设定vptrs // 安插用户代码 return this; }
举个例子,当我们定义:
Point3d origin;
时,Point3d可以正确调用其Point virtual base class subobject
。
当:
Vertex3d cv;
时,Vertex3d 构造函数会正确的调用Point virtual base class subobject
。Point3d和Vertex的构造函数会做每一个该做的事情—对Point的调用除外
综上: virtual base class constructor
的调用有明确的定义:只有当一个完整的类对象被定义出来是,就被调用。如果object只是某个完整object的subobject,他就不会被调用
因此,某些编译器会把每一个constructor一分为二,一个针对完整的object,另一个针对subobject。完整object版无条件调用virtual base constructor,设置所有的vptr等。subobject版不调用virtual base constructor,也可能不设置vptrs
vptr初始化
当我们定义一个PVertex对象时,构造函数的调用顺序是:
Point (x, y);
Point3d(x, y, z);
Vertex(x, y, z);
Vertex3d(x, y, z);
PVertex(x, y, z);
假如这个继承体系中给每一个类都定义了virtual function size()
,该函数负责传回类的大小,那么:
PVertex pv;
Point3d p3d;
Point *pt = &pv;
pt->size(); //将传回PVertex 的大小
pt = &p3d;
pt->size(); //将传回Point3d 的大小
假如这个继承体系中每一个构造函数内带一个调用操作,像这样:
Point3d::Point3d(float x, float y, float z)
: _x(x), _y(y), _z(z){
if(spyOn)
std::cerr << "Within Point3d::Point3d() size:" << size() << "\n";
}
当我们定义PVertex对象时,前述的五个构造函数将会怎样?每一次size()调用都会被决议为PVertex::size()吗?或者每次调用会被决议为"当前正在指向的构造函数所对应的类"的size()函数实体?
由于各个构造函数的调用顺序的原因,上面情况是必要的
这就意味着,当每一个PVertex base class constructor被调用时,编译系统必须保证有适当的size()函数实体被调用。怎样才能做到这一点呢?
如果size()中又调用一个虚函数,会发生什么事情呢?
另一个可以采取的方法是:在构造函数或者析构函数内设置一个flag(必须以静态方式来决议),然后我们就可以根据flag作为判断依据,产生条件式的调用
上面解决方法可能比较麻烦,最根本的解决方法是:在执行一个constructor时,必须限制一组虚函数候选名单
那怎么决定一个类的虚函数名单呢?
vptr
那么vptr初始化操作应该如何处理?本质上,这需要看vptr在构造函数中的应该在何时被初始化
而定
那vptr应该在何时被初始化呢?在基类构造函数调用操作之后,在程序员提供的码或者成员初始化列表
中所列的成员初始化操作之前
vptr
,那么每次它都能够调用正确的虚函数实体。每一个基类构造函数设定其对象的vptr使它指向相关的虚函数表之后,构造中的对象就可以正确的编程"构造过程中所幻化出来的每一个类"的对象。也就是说,一个PVertex对象会先形成一个Point对象、一个Poin3d对象,一个Verte对象,一个Vertex3d对象,然后才成为一个PVertex对象。在每一个基类构造函数中,对象可以和构造类的完整对象做比较。构造函数的执行算法如下:
vptr(s)
被初始化,指向相关的virtual table(s)
看个例子:
PVertex::PVertex(float x, float y, float z)
:_ next(0), Vertex3d(x, y, z), Point(x, y){
if(spyOn)
std::cerr << "Within PVertex::PVertex() size:" << size() << "\n";
}
它可能被扩展为:
// PVertex构造函数的扩展码 PVertex* PVertex::PVertex(PVertex * this, bool __most__derived, float x, float y, float z){ // 条件式的调用virtual base constructor if(__most__derived != false){ this->Point::Point(x, y); } // 无条件的调用上一层的base this->Vertex3d::Vertex3d(x, y, z); // 将相关的vptr初始化 this->__vptr_PVertex = __vtbl_PVertex; this->__vptr_Point_PVertex = __vptr_Point_PVertex ; // 程序员所写的码 if(spyon) std::cerr << "Within PVertex::PVertex() size:" << (*this->__vptr_Point_PVertex[3].faddr)(this) << "\n"; // 传回被调用的对象 return this; }
这就解决了有关限制虚拟机制的问题。但真的解决了吗?假设Point构造函数定义为:
Point: Point(float x, float y)
: _x(x), _y(y) {}
Point3d构造函数定位为:
Point3d::Point3d(float x, float y, float z)
: Point(x, y), _z(z){}
怎么办?
下面是vptr必须被设定的两种情况:
如果我们声明一个PVertex对象,然后由于我们对其base class constructor的最新定义,其vptr将不再需要在每一个base class constructor中被设定。解决方法是把constructor分裂成一个完整的object实体和一个subobject实体。在subobject实体中,vptr的设定可以省略(如果可能的话)
问题:在类的构造函数的成员初始化列表中调用该类的一个虚函数,安全么?
当调用一个虚函数时,被执行的代码必须与调用函数的对象的动态类型一致,指向对象的指针或引用的类型是不重要的。编译器如何能够高效的提供这种行为呢?大多数编译器是使用virtual pointer(vtbl)和virtual table pointer(vptr)实现的。
一个vtbl通常是一个函数指针数组。在程序中每个类只要声明了虚函数或者继承了虚函数,它就有自己的vtbl,并且类种vtbl的项目是指向虚函数实现体的指针。
class C1{ public: C1(); virtual ~C1(); virtual void f1(); virtual int f2(char c) const; virtual void f3(const string &s); void f4() const; }; class C1{ public: C2(); virtual ~C2(); virtual void f1(); virtual void f5(*str); }
对于C1的vtbl:
对于C2的vtbl:指向与对象相适合的函数。这些项目包括指向没有被C2重定义的C1虚函数的指针。
这个论述引出了虚函数所需的第一个代价:你必须为每个包含虚函数的类的虚函数表留出空间。类的vtbl的大小与类种声明的虚函数数量成正比(包括从基类继承的虚函数)、每个类应该只有一个虚函数表,所以虚函数表所需的空间不会太大。
虚函数表只实现了虚函数的一般机制,还需要用某种方法指出每个对象的vtbl时,它们才能使用,这就是vritual table pointer的工作,它来建立这种联系。
每个声明了虚函数的对象都带有它,它是一个看不见的数据成员,指向对应类的virtual table。是编译器自己添加的。
这张图片表示vptr位于对象的底部,但是实际上不同的编译器防止它的位置也不同。存在继承的情况下,一个对象的vptr经常被数据成员所包围。现在,我们只需要记住虚函数所需的第二个代价:在每个包含虚函数的类的对象里,你必须为额外的指针付出代价
如果对象很小,这是一个很大的代价,额外的vptr可能会使成员数据大小增加一倍。在内存受到限制的系统里,这意味这你必须减少建立对象的数量。即使在内存没有限制的系统里,你也会发现这会降低软件的性能,因为较大的对象有可能不适合放在cache或者虚拟内存页(virtual memory page),这就可能使得系统换页操作增多。
如果我们有一个程序,保护几个C1和C2对象。那么
void makeACall(C1 *pC1){
pC1->f1();
}
因为pC1可能指向C1对象或者C2对象,因此你不会知道它调用的是哪一个f1-------C1::f1()或者C2::f1()。编译器必须确保无论pC1指向什么对象,函数的调用必须正确。编译器生成的代码会做如下事情:
//pC1->f1();
(*pC1->vptr[i])(pC1);
这几乎与调用非虚函数效率一样。在大多数计算机上它多执行了很少的一些指令。调用虚函数的代价基本上和通过函数指针调用函数一样。虚函数本身通常不是性能瓶颈。
在实际运行中,虚函数所需的代价与内联函数有关。实际上虚函数不能是内联的,这是因为”内联“是指”在编译期被调用的函数体本身来代替函数调用的指令“。但是虚函数的”虚“是指”直到运行时才能直到要调用哪一个函数“。如果编译期在某个函数的调用者不知道具体哪个函数被调用,你就能知道为什么它不会内联该函数的调用。这是虚函数的第三个代价:你实际上放弃了使用内联函数(当通过对象调用虚函数时,它可以被内联,但是大部分虚函数是通过指针或者引用被调用的,这种调用不能被内联。因为这种调用是标准的调用方式,所以虚函数实际上不能被内联)
先说结论:C++中,构造函数不可以是虚函数,而析构函数可以且常常是虚函数。
当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表时一个存储成员函数指针的数据结构
虚函数表是由编译器自动生成与维护的,virtual成员函数会被编译器放入虚函数表中,当存在虚函数时,每个对象都有一个指向虚函数的指针(vptr指针)。在实现多态的过程中,父类和派生类都有vptr指针。
vptr的初始化:当对象在创建时,由编译器对vptr指针进行初始化。在定义子类对象时,vptr先指向父类的虚函数表,在父类构造完成之后,子类的vptr才指向自己的虚函数表。
如果构造函数时虚函数,那么调用构造函数就需要去找vptr,而此时vptr还没有初始化。
因此,构造函数不可以是虚函数。
与构造函数不同,vptr已经完成初始化,析构函数可以声明为虚函数,且类有继承时,析构函数常常必须为虚函数。
#include <iostream> using namespace std; class base { public: base() { cout << "base constructor" << endl; } ~base() { cout << "base destructor" << endl; } }; class derived : public base { public: derived() { cout << "derived constructor" << endl; } ~derived() { // virtual ~derived() 效果相同 cout << "derived destructor" << endl; } }; int main() { base *pBase = new derived; cout << "---" << endl; delete pBase; return 0; }
#include <iostream> using namespace std; class base { public: base() { cout << "base constructor" << endl; } virtual ~base() { cout << "base destructor" << endl; } }; class derived : public base { public: derived() { cout << "derived constructor" << endl; } virtual ~derived() { //~derived() 效果相同 cout << "derived destructor" << endl; } }; int main() { base *pBase = new derived; cout << "---" << endl; delete pBase; return 0; }
在派生类对象的基类构造期间,对象的类型是基类而不是派生类。不只是虚函数会被编译器解析至基类,如使用运行期类型类型(比如dynamic_cast),也会把对象视为基类。
因此,如果在派生类构造函数中调用虚函数,那么对象的作为就像隶属于基类一样,非正式的说法是:在基类构造期间,虚函数不是虚函数。
相同道理也适用于析构函数。一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值。所以C++视它们彷佛不存在。进入基类析构函数后对象也就成为了一个基类对象,而C++的任何部分包括虚函数、dynamic_cast等待也就那么看待它。
比如:
class Transaction{ // 所有交易的基类 public: Transaction(); virtual void logTransaction() const = 0; // 做出一份因类型不同而不同的日志记录 }; Transaction::Transaction(){ //... logTransaction(); // 最后动作是记录这笔交易 } class BugTransaction : public Transaction{ public: virtual void logTransaction() const ; // 记录这种类型的交易 } class SellTransaction : public Transaction{ public: virtual void logTransaction() const ; // 记录这种类型的交易 }
现在,当如下被执行,会发生什么事情?
BugTransaction b;
这时被调用的是Transaction内的版本,而不是BugTransaction 内的版本—即使目前即将建立的对象类型是BugTransaction。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:
virtual void funtion1()=0
对于类:
class Abstract_base{
public:
virtual ~Abstract_base() = 0; // 必须实现这个,编译器不可能自己合成一个虚析构函数
virtual void interface() const = 0; // 纯虚函数
virtual const char * mumble() const { return _mumble; }
protected:
char *_mumble;
};
问题:虽然类被设计为一个抽象的基类(其中有纯虚函数,使得Abstract_base类不可能拥有实体),但它仍需要一个明确的构造函数以初始化其数据成员_mumble。如果没有这个初始化操作,其派生类的局部对象_mumble将无法决定初值:
class Concrete_derived : public Abstract_base{
public:
Concrete_derived();
//...
};
void foo(){
// Abstract_base::_mumble尚未初始化
Concrete_derived concreteDerived;
}
也许你可能会认为Abstract_base的设计者视图让每一个派生类提供_mumble的初值。那么如果是这样,派生类将要求Abstract_base必须提供一个唯一参数的protected 构造函数:
// 一般来说,类的数据成员必须初始化,而且只能通过构造函数或者成员函数设置或者修改。
Abstract_base::Abstract_base(char *mumble = 0) : _number(mumble ){}
也许你可能会认为设计者的错误并不在于没有提供一个显式构造函数,而是不应该在抽象的基类中声明类成员。但是将被共享的数据
抽取出来放在基类中,是理所当然的设计
可以定义和调用一个纯虚函数,不过它只能被静态的调用,不能经由虚拟机制调用
inline void Abstract_base::interface() const {
printf("call Abstract_base::interface\n");
}
inline void Concrete_derived::interface() const {
Abstract_base::interface(); // 静态调用
printf("call Concrete_derived::interface\n");
}
一定要实现纯虚函数。
Abstract_base::~Abstract_base(){}
编译器不可能合成一个纯虚函数:
一个比较好的替代方案是:
另外:
面对一个抽象基类,决定一个虚函数是否需要const,不是件容易的事情。做这件事情,意味着需要假设子类实体可能被无限次调用。
推荐,不要使用const
class Abstract_base{
public:
virtual ~Abstract_base() ; // no purt
virtual void interface() = 0; // no const
const char * mumble() const { return _mumble; } // no vritual
protected:
Abstract_base(char *pc){_mumble = pc;} // add parm constructor
char *_mumble;
};
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
1、纯虚函数声明如下: virtual void funtion1()=0;
纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter)
虚函数必须实现,如果不实现,编译器将报错,错误提示为:
error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
至少包含一个纯虚函数的类叫做纯虚类
virtual void Test(int *p) = 0;
纯虚类主要作为基类使用,被其他子类继承
指定不能实现的虚函数,使得每个子类中该函数都被定义。该类主要是用作接口规范,使得子类的行为有一个参考。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
纯虚类不能实例化
如果有:
class B{
public:
void mf();
}
class D : public B {...};
那么对于:
D x;
如果以下行为:
B * pB = &x;
pB->mf();
和以下行为:
D * pD = &x;
pD->mf();
表现不同,这是不对的。但是这是有可能发生的。更明确的说,如果mf是个non-virtual函数而D定义有自己的函数,就会行为两面:
class D : public B{
public:
void mf(); // 隐藏了B::mf()
};
pB->mf(); // 调用B::mf
pD->mf(); //调用D::mf
造成行为两面的原因是,non-virtual函数比如B::mf和D::mf都是静态绑定的。也就是说,由于pB被声明为一个pointer-to-B,通过pB调用non-virtual函数永远是B所定义的版本,即使pB指向一个类型为”B派生的类“
另一方面,虚函数是动态绑定,所以它们不受这个问题之苦。如果mf是个虚函数,不管是通过pB或者pD调用mf,都会调用D::mf,因为pB和pD真正指的都是一个类型为D的对象
我们知道,所谓public继承意味着is-a关系;在类内声明一个non-virtual函数会为该类建立起一个不变性,凌驾其特异性。如果你将这两个观点施行于两个类B和D以及non-virtual成员函数B::mf身上,那么:
现在,如果D重新定义mf,你的设计就出现矛盾。如果D真的有必要实现出与B不同的mf,并且如果每一个B对象—不管多么特化—真的必须使用B所提供的mf实现码,那么”每个D都是一个B“就不为真,既然如此D就不该以public形式继承B。另一方面,如果D真的必须以public方式继承B,并且如果D真有需要实现出与B不同的mf,那么mf就无法为B反映出”不变性凌驾于特异性“的性质。既然这样mf应该声明为虚函数。
C/C++编程:为多态基类声明virtual析构函数这里解释了为什么多态基类内的析构函数应该是virtual。如果你违反这个准则(也就是在多态基类声明一个non-virtual函数),你也就违法了本准则,因为派生类绝不该重新定义一个继承而来的non-virtual函数(这里指基类析构函数)。本质而言,C/C++编程:为多态基类声明virtual析构函数
只不过是本条款的一个特殊案例
RTTI能够让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息让我们查询。这些信息被存储为type_info的对象里,你能够通过使用typeid操作符访问一个类的type_info对象。
在每个类仅仅需要一个RTTI的拷贝,但是必须保证有办法找到任何对象的类型信息。实际上上面的说明不是很准确。语言规范上这样描述:我们保证获得一个对象动态类型信息,如果该类型有至少一个虚函数。这使得RTTI有些像虚函数表,实际上RTTI是在类的vtbl上实现的。
比如,vtbl数组的索引0处可以包含一个type_info对象的指针,这个对象属于该vtbl相对应的类。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。