C++

登
C++中虚析构的作用
C++中虚析构函数的作用
《C++中虚析构函数的作用》
http://blog.csdn.net/rhzwan123/article/details/2151904
通过基类的指针来删除派生类的对象时,基类的析构函数应该是虚的。否则其删除效果将无法实现。
原因:
在公有继承中,基类对派生类及其对象的操作,只能影响到那些从基类继承下来的成员。如果想要用基类对非继承成员进行操作,则要把基类的这个操作(函数)定义为虚函数。
  那么,析构函数自然也应该如此:如果它想析构子类中的重新定义或新的成员及对象,当然也应该声明为虚的。
注意:
如果不需要基类对派生类及对象进行操作,则不能定义虚函数(包括虚析构函数),因为这样会增加内存开销。
自动调用基类部分的析构函数对基类的设计有重要影响。
删除指向动态分配对象的指针时,需要运行析构函数在释放对象的内存之前清除对象。处理继承层次中的对象时,指针的静态类型可能与被删除对象的动态类型不同,可能会删除实际指向派生类对象的基类类型指针。
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为,要保证运行适当的析构函数,基类中的析构函数必须是析构的。
例如:
 class A;
  class B  public A:
则 A* p = new B(), 是可以编译通过的,但在调用析构时需要调用B的析构函数,所以A必须定义为虚函数才能正确析构。
虚析构函数是为了解决这样的一个问题:基类的指针指向派生类对象,并用基类的指针删除派生类对象。
C++中虚析构函数的作用   
我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明: 有下面的两个类: 
class ClxBase { 
public: 
ClxBase() {}; 
virtual ~ClxBase() {};  
virtual void DoSomething() { cout < DosomethinginclassClxBaseendlspan>
class ClxDerived : public ClxBase { 
public: 
ClxDerived() {}; 
~ClxDerived() { cout < OutputfromthedestructorofclassClxDerivedendlspan>
void DoSomething() { cout < DosomethinginclassClxDerivedendlspan>
代码 
ClxBase *pTest = new ClxDerived; 
pTest-<DoSomething(); 
delete pTest; 
的输出结果是: 
Do something in class ClxDerived! 
Output from the destructor of class ClxDerived! 这个很简单,非常好理解。 
但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了: 
Do something in class ClxDerived!  
也就是说,类ClxDerived的析构函数根本没有被调用!(注:肯定不会被调用,因为动态联编,在运行时会检查有无派生类对象重载本函数,有则调用之,基类析构不会调用) 
一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。 所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。 
当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。
所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
纯虚析构函数必须有实现,不然会出现连接错误。
当然,若没有采用封装导出接口,直接对CTest类进行操作,自然不会有遗忘调用析构函数的情况。但无论从程序的健壮性,或者是可扩展性来说,对接口类添加纯虚的析构函数,是百利而无一害的。
究其原因,是在delete一个接口类时,由于析构函数不是虚的,系统直接将ITest的析构函数(编译器会自动生成)删除了之,而不去查找真正需要调用的析构函数。
静态数据成员
静态类成员包括静态数据成员和静态函数成员两部分。

一静态数据成员:

类体中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。同时,静态数据成员还具有以下特点:

        1.静态数据成员的定义。
静态数据成员实际上是类域中的全局变量。所以,静态数据成员的定义(初始化)不应该被放在头文件中。
其定义方式与全局变量相同。举例如下:

        xxx.h文件
        class  base{   
           private:   
           static   const   int   _i;//声明,标准c++支持有序类型在类体中初始化,但vc6不支持。
       };     

        xxx.cpp文件
const  int   base::_i=10;//定义(初始化)时不受private和protected访问限制.   

注:不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef  #define   #endif或者#pragma  once也不行。

        2.静态数据成员被类的所有对象所共享,包括该类派生类的对象。
即派生类对象与基类对象共享基类的静态数据成员。举例如下:
        class  base{   
             public   :   
             static   int   _num;//声明
        };   
        int   base::_num=0;//静态数据成员的真正定义

        class  derived:public   base{   
        };   

        main()   
        {   
           base   a;   
           derived   b;   
           a._num++;   
           cout< base class staticdata number _numisa_num>< endlspan>
           b._num++;   
           cout< derived class staticdata number _numisb_num>< endlspan>
        }   
        //   结果为1,2;可见派生类与基类共用一个静态数据成员。

      3.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。举例如下:
      class   base{   
          public  :   
          static  int   _staticVar;   
          int  _var;   
 void  foo1(int   i=_staticVar);//正确,_staticVar为静态数据成员
          void  foo2(int   i=_var);//错误,_var为普通数据成员
     };           

    4.★静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为所属类类型的指针或引用。举例如下:

      class   base{   
          public  :   
 static  base   _object1;//正确,静态数据成员
          base  _object2;//错误
          base  *pObject;//正确,指针
          base  &mObject;//正确,引用
      };   

    5.★这个特性,我不知道是属于标准c++中的特性,还是vc6自己的特性。
静态数据成员的值在const成员函数中可以被合法的改变。举例如下:

      class   base{   
         public:   
         base(){_i=0;_val=0;}   

          mutable  int   _i;   
          static  int   _staticVal;     
          int  _val;   
          void  test()   const{//const   成员函数

               _i++;//正确,mutable数据成员
               _staticVal++;//正确,static数据成员
 _val++;//错误

          }   
      };   
      int  base::_staticVal=0;   

二,静态成员函数
静态成员函数没有什么太多好讲的。

      1.静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用类成员函数指针来储存。举例如下:
          class  base{   
             static   int   func1();   
             int   func2();   
         };   

          int  (*pf1)()=&base::func1;//普通的函数指针
          int   (base::*pf2)()=&base::func2;//成员函数指针


      2.静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。

      3.静态成员函数不可以同时声明为  virtual、const、volatile函数。举例如下:
        class  base{   
             virtual   static   void   func1();//错误
             static   void   func2()   const;//错误
             static   void   func3()   volatile;//错误
        };   


最后要说的一点是,静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。
————————————————————————————————
【C++基础学习】关于C++静态数据成员
静态类成员包括静态数据成员和静态函数成员两部分。  
一、静态数据成员:
 类体中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。同时,静态数据成员还具有以下特点:  
 1.静态数据成员的定义。
静态数据成员实际上是类域中的全局变量。所以,静态数据成员的定义(初始化)不应该被放在头文件中。   其定义方式与全局变量相同。举例如下:  
xxx.h文件   :
[cpp]view plaincopyprint?
class   base{    
private:    
staticconstint   _i;//声明,标准c++支持有序类型在类体中初始化,但vc6不支持。
};      
xxx.cpp文件:[cpp]view plaincopyprint?
constint   base::_i=10;//定义(初始化)时不受private和protected访问限制. 
 注:不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef  #define   #endif或者#pragma   once也不行。 
 2.静态数据成员被类的所有对象所共享,包括该类派生类的对象。即派生类对象与基类对象共享基类的静态数据成员。举例如下:  [cpp]view plaincopyprint?
class   base{    
public   :    
staticint   _num;//声明
};    
int   base::_num=0;//静态数据成员的真正定义
class   derived:public   base{    
};    
int main()    
{    
     base   a;    
     derived   b;    
     a._num++;    
     cout< span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(255, 0, 0);' >"base   class   static   data   number   _num   is"< a numendl></span>
     b._num++;    
     cout< span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(255, 0, 0);' >"derived   class   static   data   number   _num   is"< b numendl></span>
}    
//   结果为1,2;可见派生类与基类共用一个静态数据成员。
3.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。举例如下:[cpp]view plaincopyprint?
class   base{    
public   :    
staticint   _staticVar;    
int   _var;    
void   foo1(int   i=_staticVar);//正确,_staticVar为静态数据成员
void   foo2(int   i=_var);//错误,_var为普通数据成员
     };        
4.★静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为所属类类型的指针或引用。举例如下: 
[cpp]view plaincopyprint?
class   base{    
public   :    
static   base   _object1;//正确,静态数据成员
          base   _object2;//错误
          base   *pObject;//正确,指针
          base   &mObject;//正确,引用
      };    
5.★这个特性,我不知道是属于标准c++中的特性,还是vc6自己的特性。静态数据成员的值在const成员函数中可以被合法的改变。举例如下:
[cpp]view plaincopyprint?
class   base{    
public:    
          base(){_i=0;_val=0;}    
mutableint   _i;    
staticint   _staticVal;      
int   _val;    
void   test()   const{//const   成员函数
                _i++;//正确,mutable数据成员
                _staticVal++;//正确,static数据成员
                _val++;//错误
          }    
      };    
int   base::_staticVal=0;    


二、静态成员函数
静态成员函数没有什么太多好讲的。   
1.静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用类成员函数指针来储存。举例如下:
[cpp]view plaincopyprint?
class   base{    
staticint   func1();    
int   func2();    
          };    
int   (*pf1)()=&base::func1;//普通的函数指针
int   (base::*pf2)()=&base::func2;//成员函数指针
2.静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。
3.静态成员函数不可以同时声明为   virtual、const、volatile函数。举例如下:
[cpp]view plaincopyprint?
class   base{    
virtualstaticvoid   func1();//错误
staticvoid   func2()   const;//错误
staticvoid   func3()   volatile;//错误
       };    
最后要说的一点是,静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。
C++面试知识点
==============================================================================
==============================================================================
C++类在内存中的分布,以及访问权限的控制机制(深入内存解答)
==============================================================================
==============================================================================
比较C++和Java
==============================================================================
==============================================================================
虚析构的作用(C++ PrimerP495)
Const
C语言中const详解
http://www.cnblogs.com/wangkangluo1/archive/2011/09/27/2193066.html
Const的用法:
如何解除const限制:
用const_cast解除
比如定义
    const char data[10]={"123"};
如果想要改变data的值为223就需要用如下方式
   const_cast(data)[0]='2';


http://blog.csdn.net/hackbuteer1/article/details/6550736《const_cast的应用》
对于const变量,我们不能修改它的值,这是这个限定符最直接的表现。但是我们就是想违背它的限定希望修改其内容怎么办呢?于是我们可以使用const_cast转换符是用来移除变量的const限定符。
const_cast类型转换能够剥离一个对象的const属性,也就是说允许你对常量进行修改。
[cpp]view plaincopy
#include
usingnamespace std; 
/*
用法:const_cast (expression)
  该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
  一、常量指针被转化成非常量指针,并且仍然指向原来的对象;
  二、常量引用被转换成非常量引用,并且仍然指向原来的对象;
  三、常量对象被转换成非常量对象。
  type_id 必须为指针或引用
*/
class B 
{ 
public: 
int m_iNum; 
    B() : m_iNum(50) 
    {   } 
}; 
void foo() 
{ 
const B *b1 = new B(); 
//b1-<m_iNum = 100;          // 编译错误
// 做如下转换,体现出转换为指针类型
    B *b2 = const_cast(b1); 
    b2-<m_iNum = 200; 
    cout< span>"b1: "< b->m_iNum < endl>
    cout< span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >"b2: "< b->m_iNum < endl></span>
const B b3; 
//b3.m_iNum = 100;     // 编译错误
    B b4 = const_cast< span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(255, 0, 0);' >B&<(b3);          // b4是另外一个对象(单纯对象,不是引用)</span>
    b4.m_iNum = 200; 
    cout< span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >"b3: "< bm iNum endl></span>
    cout< span>"b4: "< bm iNum endl>
const B b5; 
//b5.m_iNum = 100;     // 编译错误
// 或者左侧也可以用引用类型,如果对b6的数据成员做改变,就是对b5的值在做改变
    B &b6 = const_cast(b5); 
    b6.m_iNum = 200; 
    cout< span>"b5: "< bm iNum endl>
    cout< span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >"b6: "< bm iNum endl></span>
// force to convert 
constint x = 50; 
int* y = (int *)(&x);       // 同样的地址,但是内容是不一样的
    *y = 200; 
    cout < span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >"x: "< xspan style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >" address: "</span>< xendl></span>
    cout < span>"*y: "< yspan>" address: "< yendl>
    cout< endl>
constint xx = 50; 
int* yy = const_cast< span>int *< (&xx);     // 同样的地址,但是内容是不一样的
    *yy = 200; 
    cout < span>"xx: "< xxspan>" address: "< xxendl>
    cout < span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >"*yy: "< yyspan style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >" address: "</span>< yyendl></span>
    cout< endl>
// int
constint xxx = 50; 
int yyy = const_cast< span style='font-size:14px;font-style:normal;font-weight:700;color:rgb(0, 0, 0);' >int&< (xxx);     // yyy是另外一个int对象</span>
    yyy = 200; 
    cout < span style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >"xxx: "< xxxspan style='font-size:14px;font-style:normal;font-weight:400;color:rgb(0, 0, 255);' >" address: "</span>< xxxendl></span>
    cout < span>"yyy: "< yyyspan>" address: "< yyyendl>
} 
int main(void) 
{ 
    foo(); 
return 0; 
} 
运行结果如下:
const_cast只能修改变量的常引用的const属性,和变量的常指针的const属性,还有对象的const属性。要想改变常量本身的值是不可能的,也就是说,你改变的是引用的const属性,而不是常量本身的const属性。
概念
==============================================================================
==============================================================================
const的概念和使用 C++ Primer P110
1.const常量在定义后不能修改。
    定义时必须初始化。
2.const和#define的区别?
    const有变量,后者只是简单的替换而已
3.指向const对象的指针
const double* cptr;
cptr指向的对象类型。非cptr本身
无需对cptr初始化。允许对cptr重新赋值。
Const指针:本身的值不能修改
int errNumb = 0;
int * const curErr = &errNumb;
是一个不变指针。
==============================================================================
==============================================================================
float变量与零值
if( (x<=-EPSINON) &&(x< EPSINON span>
相当于if(x == 0) 浮点型变量
float大概精确到6位。EPSINON = 0.000001;
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
==============================================================================
多态虚函数
C++的虚函数的实现机制
C++多态的实现原理:
存在虚函数的类都有一个一维的虚函数表叫做虚表。
类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
多态性是一个接口多种实现,是面向对象的核心。分为类的多态性和函数的多态性。
多态用虚函数来实现,结合动态绑定。
纯虚函数是虚函数再加上= 0。
    抽象类是指包括至少一个纯虚函数的类。
C++编译器在编译的时候,要确定每个对象调用的函数(要求此函数是非虚函数)的地址,这称为早期绑定(early binding)
前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。
而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字(注意,这是必须的,很多学员就是因为没有使用虚函数而写出很多错误的例子),这样的函数我们称为虚函数。
一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。
编译器在编译的时候,发现animal类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址。对于例1-2的程序,animal和fish类都包含了一个虚函数breathe(),因此编译器会为这两个类都建立一个虚表,(即使子类里面没有virtual函数,但是其父类里面有,所以子类中也有了虚表。)
如何定位虚表呢?编译器另外还为每个类的对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。
在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。对于例1-2的程序,由于pAn实际指向的对象类型是fish,因此vptr指向的fish类的vtable,当调用pAn-<breathe()时,根据虚表中的函数地址找到的就是fish类的breathe()函数。
正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。那么虚表指针在什么时候,或者说在什么地方初始化呢?
答案是在构造函数中进行虚表的创建和虚表指针的初始化。还记得构造函数的调用顺序吗,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。对于例2-2的程序来说,当fish类的fh对象构造完毕后,其内部的虚表指针也就被初始化为指向fish类的虚表。在类型转换后,调用pAn-<breathe(),由于pAn实际指向的是fish类的对象,该对象内部的虚表指针指向的是fish类的虚表,因此最终调用的是fish类的breathe()函数。
要注意:对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。

总结(基类有虚函数):
1. 每一个类都有虚表。
2. 虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3. 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
这就是C++中的多态性。当C++编译器在编译的时候,发现animal类的breathe()函数是虚函数,这个时候C++就会采用迟绑定(latebinding)技术。也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型(在程序中,我们传递的fish类对象的地址)来确认调用的是哪一个函数,这种能力就叫做C++的多态性。
我们没有在breathe()函数前加virtual关键字时,C++编译器在编译时就确定了哪个函数被调用,这叫做早期绑定(earlybinding)。

C++的多态性是通过迟绑定技术来实现的。

C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

虚函数是在基类中定义的,目的是不确定它的派生类的具体行为。例:
定义一个基类:class Animal//动物。它的函数为breathe()//呼吸。
再定义一个类class Fish//鱼。它的函数也为breathe()
再定义一个类class Sheep //羊。它的函数也为breathe()
为了简化代码,将Fish,Sheep定义成基类Animal的派生类。
然而Fish与Sheep的breathe不一样,一个是在水中通过水来呼吸,一个是直接呼吸空气。所以基类不能确定该如何定义breathe,所以在基类中只定义了一个virtual breathe,它是一个空的虚函数。具体的函数在子类中分别定义。程序一般运行时,找到类,如果它有基类,再找它的基类,最后运行的是基类中的函数,这时,它在基类中找到的是virtual标识的函数,它就会再回到子类中找同名函数。派生类也叫子类。基类也叫父类。这就是虚函数的产生,和类的多态性(breathe)的体现。

这里的多态性是指类的多态性。
函数的多态性与函数的重载是指一个函数被定义成多个不同参数的函数,它们一般被存在头文件中,当你调用这个函数,针对不同的参数,就会调用不同的同名函数。例:Rect()//矩形。它的参数可以是两个坐标点(point,point)也可能是四个坐标(x1,y1,x2,y2)这叫函数的多态性与函数的重载。

类的多态性,是指用虚函数和延迟绑定来实现的。函数的多态性是函数的重载。

一般情况下(没有涉及virtual函数),当我们用一个指针/引用调用一个函数的时候,被调用的函数是取决于这个指针/引用的类型。即如果这个指针/引用是基类对象的指针/引用就调用基类的方法;如果指针/引用是派生类对象的指针/引用就调用派生类的方法,当然如果派生类中没有此方法,就会向上到基类里面去寻找相应的方法。这些调用在编译阶段就确定了。
当涉及到多态性的时候,采用了虚函数和动态绑定,此时的调用就不会在编译时候确定而是在运行时确定。不在单独考虑指针/引用的类型而是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数。
————————————————————————————————
C++虚函数表解析
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。
言归正传,让我们一起进入虚函数的世界。
虚函数表
对C++ 了解的人都应该知道虚函数(VirtualFunction)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了 这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。
在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。 没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:
class Base {
public:
virtual void f() { cout < Basef endl span>
virtual void g() { cout < Baseg endl span>
virtual void h() { cout < Baseh endl span>
};
按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout < intb endlspan>
cout < intintb endlspan>
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
实际运行经果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行把&b转成int*,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
这个时候你应该懂了吧。什么?还是有点晕。也是,这样的代码看着太乱了。没问题,让我画个图解释一下。如下所示:
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b-<f();
由b所指的内存中的虚函数表(派生类的实例,其虚函数表)的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1-<f(); //Derive::f()
b2-<f(); //Derive::f()
b3-<f(); //Derive::f()
b1-<g(); //Base1::g()
b2-<g(); //Base2::g()
b3-<g(); //Base3::g()
安全性
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,
但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1-<f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
如:
class Base {
private:
virtual void f() { cout < Basef endl span>
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
结束语
C++这门语言是一门Magic的语言,对于程序员来说,我们似乎永远摸不清楚这门语言背着我们在干了什么。需要熟悉这门语言,我们就必需要了解C++里面的那些东西,需要去了解C++中那些危险的东西。不然,这是一种搬起石头砸自己脚的编程语言。
——————————————————————————————————————
c++实现多态的方法

其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?
从《more effecive c++》其中一篇文章里面可以知道:是每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

class A
{
public:
    virtual void f();
    virtual void g();
private:
    int a
};

class B : public A
{
public:
    void g();
private:
    int b;
};

//A,B的实现省略

因为A有virtual void f(),和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:


A::f 的地址
A::g 的地址

B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:


A::f 的地址
B::g 的地址

注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。

然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:


对象的内存形式
vptr : 指向B的虚表vtableB
int a: 继承A的成员
int b: B成员

当如下语句的时候:
A *pa = &bB;

pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)

那么pa-<g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call*(pa-<vptr)[1](C语言的数组索引从0开始哈~)。

这一项放的是B::g()的入口地址,则就实现了多态。(注意bB的虚指针vptr指向的是B的虚表vtableB)

另外要注意的是,如上的实现并不是唯一的,C++标准只要求用这种机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。我以上的结果是根据g++4.3.4经过反汇编分析出来的。



2、两种多态实现机制及其优缺点

除了c++的这种多态的实现机制之外,还有另外一种实现机制,也是查表,不过是按名称查表,是smalltalk等语言的实现机制。这两种方法的优缺点如下:

(1)、按照绝对位置查表,这种方法由于编译阶段已经做好了索引和表项(如上面的call *(pa-<vptr[1]) ),所以运行速度比较快;缺点是:当A的virtual成员比较多(比如1000个),而B重写的成员比较少(比如2个),这种时候,B的vtableB的剩下的998个表项都是放A中的virtual成员函数的指针,如果这个派生体系比较大的时候,就浪费了很多的空间。

比如:GUI库,以MFC库为例,MFC有很多类,都是一个继承体系;而且很多时候每个类只是1,2个成员函数需要在派生类重写,如果用C++的虚函数机制,每个类有一个虚表,每个表里面有大量的重复,就会造成空间利用率不高。于是MFC的消息映射机制不用虚函数,而用第二种方法来实现多态,那就是:

(2)、按照函数名称查表,这种方案可以避免如上的问题;但是由于要比较名称,有时候要遍历所有的继承结构,时间效率性能不是很高。(关于MFC的消息映射的实现,看下一篇文章)

3、总结:

如果继承体系的基类的virtual成员不多,而且在派生类要重写的部分占了其中的大多数时候,用C++的虚函数机制是比较好的;

但是如果继承体系的基类的virtual成员很多,或者是继承体系比较庞大的时候,而且派生类中需要重写的部分比较少,那就用名称查找表,这样效率会高一些,很多的GUI库都是这样的,比如MFC,QT

PS. 其实,自从计算机出现之后,时间和空间就成了永恒的主题,因为两者在98%的情况下都无法协调,此长彼消;这个就是计算机科学中的根本瓶颈之所在。软件科学和算法的发展,就看能不能突破这对时空权衡了。呵呵

何止计算机科学如此,整个宇宙又何尝不是如此呢?最基本的宇宙之谜,还是时间和空间~
浮点数
http://blog.csdn.net/masefee/article/details/5272554
union概念
http://blog.csdn.net/yfkiss/article/details/6674348
什么是union?
union是一种特殊的类(struct/class),其特殊之处在于,union内的变量共享一段内存,并且union占用内存大小等于其内占用内存最大的变量的大小。
应用:
union最大的用处在于在某些场景下用来节省内存。
我们常见的一个场景是,有两个变量,但这两个变量我们不会同时需要。
    例如,一个人的属性是:姓名、工作类型、工作描述,在描述工作的时候,针对不同的职业,有不同的属性,但是,一个人不会同时拥有两个职业(一般情况下)
使用注意
如果类是union的成员,则成员类不能提供构造函数、析构函数。这是因为union的成员共享内存,编译器无法保证这些成员在构造时不被破坏,也无法保证离开时调用析够函数。
可以用union判断CPU是大端对齐还是小端对齐
若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1
int checkCPU( )
{
{
union w
{ 
int a;
char b;
} c;
c.a = 1;
return(c.b ==1);
}
}

解析:
大小端序是看cpu对操作数的存放方式不同
Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节
由于union的变量是共享内存,所以int型的a和char型的b在同一块内存上。并且,union的存放特征是:所有成员都从低地址开始存放。
所以:通过对a赋值1,那么b肯定是在低位,查看b的值,就能了解a低位的值,从而知道大小端序
rand7()函数
题目:给定一个函数rand5(),该函数可以随机生成1-5的整数,且生成概率一样。现要求使用该函数构造函数rand7(),使函数rand7()可以随机等概率的生成1-7的整数。
思路:
很多人的第一反应是利用rand5() + rand()%3来实现rand7()函数,这个方法确实可以产生1-7之间的随机数,但是仔细想想可以发现数字生成的概率是不相等的。rand()%3 产生0的概率是1/5,而产生1和2的概率都是2/5,所以这个方法产生6和7的概率大于产生5的概率。
正确的方法是利用rand5()函数生成1-25之间的数字,然后将其中的1-21映射成1-7,丢弃22-25。例如生成(1,1),(1,2),(1,3),则看成rand7()中的1,如果出现剩下的4种,则丢弃重新生成。
简单实现:
Java代码 public class Test {   
    public int rand7() {   
        int x = 22;   
        while(x < 21) {   
            x = rand5() + (rand5() - 1)*5;   
        }   
        return 1 + x%7;   
    }   
}   
 rand5() 它能够等概率生成 1-5 之间的整数。所谓等概率就是1,2,3,4,5 生产的概率均为 0.2 。现在利用rand5(), 构造一个能够等概率生成 1- 7 的方法。这里有两个特别重要的点,一是如果 rand5() +rand5(), 我们能够产生一个均匀分布的 1 - 10 吗?答案是否定的。比如对于 6来讲(4+2, 2+4, 3+3),它被生成的生成的概率比1 (1+0,0+1)要大。

第二个点就是我们不可能用rand5()直接产生 1- 7 的数,不管你用加减乘除都不行。所以,我们要构造一个更大的范围,使得范围里每一个值被生成的概率是一样的,而且这个范围是7的倍数。
先产生一个均匀分布的 0, 5, 10, 15, 20的数,再产生一个均匀分布的 0, 1, 2, 3, 4 的数。相加以后,会产生一个 0到24的数,而且每个数(除0外)生成的概率是一样的。我们只取 1 - 21 这一段,和7 取余以后+1就能得到完全均匀分布的1-7的随机数了。


   我的备注:
    这种思想是基于,rand()产生[0,N-1],把rand()视为N进制的一位数产生器,那么可以使用rand()*N+rand()来产生2位的N进制数,以此类推,可以产生3位,4位,5位...的N进制数。这种按构造N进制数的方式生成的随机数,必定能保证随机,而相反,借助其他方式来使用rand()产生随机数(如 rand5() +rand()%3 )都是不能保证概率平均的。
    此题中N为5,因此可以使用rand5()*5+rand5()来产生2位的5进制数,范围就是1到25。再去掉22-25,剩余的除3,以此作为rand7()的产生器.


给定一个函数rand()能产生0到n-1之间的等概率随机数,问如何产生0到m-1之间等概率的随机数?
int random(int m,int n){
    int k=rand();
    int max=n-1;
    while(k
        k=k*n+rand();
        max=max*n+n-1;
    }
    return k/(max/n);
}
如何产生如下概率的随机数?0出1次,1出现2次,2出现3次,n-1出现n次?
int random(int size){
    while(true){
        int m=rand(size);
        int n=rand(size);
        if(m+n
            return m+n;
    }
}
——————————————————————————————————
http://blog.csdn.net/fisher_jiang/article/details/6845833
//rand7 产生的数概率是一样的,即1~7出现概率一样,由于我们对结果做了一定的筛选只能通过 1~5,而1~5出现的概率也是一样的,又由于范围为1~5 所以 temp1 出现 1~5的概率为1/5 ,同理后面的出现 temp2 的概率为 1/2
//首先temp1出现在1~5的概率为1/5,而temp2出现 1~2 的概率为1/2,也就是说 5*(temp2-1) 出现5或0的概率为1/2,所以假如你要得到1~5的数的话那么 5*(temp2-1)必须0,所以因为你要保证 5*(temp2-1)=0,这个概率只有1/2,再加上你前面指定1~5 的概率为1/5 ,所以结果为 1/5*1/2=1/10
intrand10()
{
    inttemp1;
    inttemp2;
do
    {
        temp1 =rand7();
    }while(temp1<5);
do
    {
        temp2 =rand7();
    }while(temp2<2);
returntemp1+5*(temp2-1);
}
dypedef 和 define
typedef char *String_t; 和 #define String_dchar * 这两句在使用上有什么区别?
[cpp]view plaincopy
答:typedefchar *String_t 定义了一个新的类型别名,有类型检查。而#define String_d char * 只是做了个简单的替换,无类型检查,前者在编译的时候处理,后者在预编译的时候处理。 
同时定义多个变量的时候有区别,主要区别在于这种使用方式String_t  a,b;  String_d  c,d;    a,b ,c都是char*类型,而d为char类型
由于typedef还要做类型检查。。#define没有。。所以typedef比#define安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值