C++ virtual虚函数总结

c++是一门面向对象的语言,但是它和c#,java不同,它没有反射机制。没有反射机制使得c++在语言的一些设计方面与其他语言有点不一样,主要体现在智能化方面,许多东西得程序员明确指定,例如本文要讲的virtual关键字。virtual是在运行时才体现的,而c++在运行时无法使用反射来确定当前类的父类是否有此方法,此方法是否被重载等信息,所以必须在写代码时用virtual来明确指明,然后通过编译器做一些特殊处理(也就是使用虚表)。

我们见到virtual最多的地方是在c++里面的多态实现。



[cpp]  view plain copy
  1. // dynamic_poly.h  
  2. #include <iostream>  
  3. // 公共抽象基类Vehicle  
  4. class Vehicle  
  5. {  
  6. public:  
  7.     virtual void run() const = 0;  
  8. };  
  9. // 派生于Vehicle的具体类Car  
  10. class Car: public Vehicle  
  11. {  
  12. public:  
  13.     virtual void run() const  
  14.     {  
  15.         std::cout << "run a car/n";  
  16.     }  
  17. };  
  18. // 派生于Vehicle的具体类Airplane  
  19. class Airplane: public Vehicle  
  20. {  
  21. public:  
  22.     virtual void run() const  
  23.     {  
  24.         std::cout << "run a airplane/n";  
  25.     }  
  26. };   

[cpp]  view plain copy
  1. // dynamic_poly_1.cpp  
  2. #include <iostream>  
  3. #include <vector>  
  4. #include "dynamic_poly.h"  
  5. // 通过指针run任何vehicle  
  6. void run_vehicle(const Vehicle* vehicle)  
  7. {  
  8.     vehicle->run();            // 根据vehicle的具体类型调用对应的run()  
  9. }  
  10. int main()  
  11. {  
  12.     Car car;  
  13.     Airplane airplane;  
  14.     run_vehicle(&car);         // 调用Car::run()  
  15.     run_vehicle(&airplane);    // 调用Airplane::run()  
  16. }  

上面的例子来自于http://www.vckbase.com/document/viewdoc/?id=948,这篇文章里面还提到了多态的一些别的例子。关于多态的文章还有孟岩写的一篇http://blog.csdn.net/wuliming_sc/archive/2009/01/31/3855906.aspx,可以看看。

在很多多态的例子中,我们都可以看到将基类的方法声明为纯虚函数(virtual void run() const = 0;),这样可以要求子类必须实现这个方法,同时可以体现面向接口编程。

对于使用了virtual之后编译器会做些什么,我觉得用代码更容易说明问题。下面的例子来自http://www.cppblog.com/zhangyq/archive/2009/06/13/87597.html

1 编译器会为这个类的虚函数添加一个虚表,类似下面的: 
// Pseudo-code (not C++, not C) for a static table defined within file Base.cpp 
// Pretend FunctionPtr is a generic pointer to a generic member function 
// (Remember: this is pseudo-code, not C++ code) 
FunctionPtr Base::__vtable[5] = { 
   &Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4 
};

2 然后增加一个指向虚表的指针为每一个类对象,这个指针是隐藏的 
// Your original C++ source code 
class Base { 
public: 
   ... 
   FunctionPtr* __vptr;  ← supplied by the compiler, hidden from the programmer 
   ... 
}; 
3 编译器在构造中初始化这个指针 
Base::Base(...arbitrary params...) 
   : __vptr(&Base::__vtable[0])  ← supplied by the compiler, hidden from the programmer 
   ... 

   ... 

在派生类中,它也会增加一个隐藏的虚表,但是它可以overrides基类的虚函数如: 
// Pseudo-code (not C++, not C) for a static table defined within file Der.cpp 
// Pretend FunctionPtr is a generic pointer to a generic member function 
// (Remember: this is pseudo-code, not C++ code) 
FunctionPtr Der::__vtable[5] = { 
   &Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4 
};   

从上面的代码我们可以非常容易的划出class的结构图,当然可以用/d1reportSingleClassLayout来显示class的布局图(http://blog.csdn.net/chief1985/archive/2009/10/23/4720191.aspx)。

使用virtual的地方还有虚拟继承和虚拟析构函数。

虚拟继承的例子如下,来自http://blog.csdn.net/wuliming_sc/archive/2009/01/31/3855607.aspx


[html]  view plain copy
  1. class Point2d{  
  2. public:  
  3. //...  
  4. protected:  
  5. float _x;  
  6. float _y;  
  7. };  
  8. class Vertex : public virtual Point2d{  
  9. public:  
  10. //...  
  11. protected:  
  12. Vertex *next;  
  13. };  
  14. class Point3d : public virtual Point2d{  
  15. public:  
  16. //...  
  17. protected:  
  18. float _z;  
  19. };  
  20. class Vertex3d: public Vertex, public Point3d{  
  21. public:  
  22. //...  
  23. protected:  
  24. float mumble;  
  25. };  

 

image

使用虚拟继承一般都是出现了菱形继承(c++允许多重继承),这种情况下若不使用虚拟继承,便会导致以下情况:

1.公共基类子对象的重复创建问题。

2.成员函数的名字冲突问题

3. 数据成员的名字冲突问题

出现虚拟析构函数的地方也是在多态的时候,如果不使用虚拟析构函数,将会导致子类的析构函数没被调用,例子可以参考http://blog.csdn.net/starlee/archive/2006/03/09/619827.aspx


C++中虚析构函数的作用

  我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:    
    有下面的两个类:

[cpp]  view plain copy
  1. class ClxBase  
  2. {  
  3. public:  
  4.     ClxBase() {};  
  5.     virtual ~ClxBase() {};  
  6.   
  7.     virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };  
  8. };  
  9.   
  10. class ClxDerived : public ClxBase  
  11. {  
  12. public:  
  13.     ClxDerived() {};  
  14.     ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };   
  15.   
  16.     void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };  
  17. };  

代码

[cpp]  view plain copy
  1. ClxBase *pTest = new ClxDerived;  
  2. pTest->DoSomething();  
  3. delete pTest;  

的输出结果是:

Do something in class ClxDerived!
Output from the destructor of class ClxDerived!

    这个很简单,非常好理解。
    但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:

Do something in class ClxDerived!

    也就是说,类ClxDerived的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。
    所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
    当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

1.为什么要引入虚拟继承

虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下:

class A

class B1:public virtual A;

class B2:public virtual A;

class D:public B1,public B2;

虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

 

2.引入虚继承和直接继承会有什么区别呢

由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。

2.1时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。

2.2空间:由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。

 

3.笔试,面试中常考的C++虚拟继承的知识点

第一种情况:         第二种情况:          第三种情况            第四种情况:
class a           class a              class a              class a
{              {                {                 {
    virtual void func();      virtual void func();       virtual void func();        virtual void func();
};              };                  char x;              char x;
class b:public virtual a   class b :public a           };                };
{              {                class b:public virtual a      class b:public a
    virtual void foo();        virtual void foo();     {                 {
};              };                  virtual void foo();        virtual void foo();
                               };                };

如果对这四种情况分别求sizeof(a),  sizeof(b)。结果是什么样的呢?下面是输出结果:(在vc6.0中运行)
第一种:4,12 
第二种:4,4
第三种:8,16
第四种:8,8

想想这是为什么呢?

因为每个存在虚函数的类都要有一个4字节的指针指向自己的虚函数表,所以每种情况的类a所占的字节数应该是没有什么问题的,那么类b的字节数怎么算呢?看“第一种”和“第三种”情况采用的是虚继承,那么这时候就要有这样的一个指针vptr_b_a,这个指针叫虚类指针,也是四个字节;还要包括类a的字节数,所以类b的字节数就求出来了。而“第二种”和“第四种”情况则不包括vptr_b_a这个指针,这回应该木有问题了吧。

 

4.c++重载、覆盖、隐藏的区别和执行方式

既然说到了继承的问题,那么不妨讨论一下经常提到的重载,覆盖和隐藏
4.1成员函数被重载的特征
(1)相同的范围(在同一个类中); 
(2)函数名字相同; 
(3)参数不同; 
(4)virtual 关键字可有可无。 
4.2“覆盖”是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类); 
(2)函数名字相同; 
(3)参数相同; 
(4)基类函数必须有virtual 关键字。 
4.3“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,特征是:

(1)如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 
(2)如果派生类的函数与基类的函数同名,但是参数相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

小结:说白了就是如果派生类和基类的函数名和参数都相同,属于覆盖,这是可以理解的吧,完全一样当然要覆盖了;如果只是函数名相同,参数并不相同,则属于隐藏。


4.4 三种情况怎么执行:

4.4.1 重载:看参数。

4.4.2 隐藏:用什么就调用什么。

4.4.3 覆盖:调用派生类。

        


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值