虚函数

原文地址:http://blog.csdn.net/lonfee88/article/details/7462430



1. 封装、继承和this指针

1.1 封装(Encapsulation)

把数据成员声明为private,不允许外界随意存取,只能通过特定的接口来操作,这就是面向对象的 封装特性。

1.2 继承(Inheritance)

子类“暗自(implicit)”具备了父类的所有成员变量和成员函数, 包括private属性的成员(虽然没有访问权限)

1.3 this指针

矩形类CRect如下:
[cpp]  view plain copy print ?
  1. class CRect  
  2. {  
  3. private:      
  4.     int m_color;  
  5. public:  
  6.     void setcolor(int color)  
  7.     {  
  8.         m_color=color;  
  9.     }  
  10. };  
有两个CRect对象rect1和rect2,各有各自的m_color成员变量。rect1.setcolor和rect2.setcolor调用的是唯一的CRect::setcolor成员函数,却处理了各自的m_color。
这是因为成员函数是属于类的而不是属于某个对象的,只有一个。成员函数都有一个隐藏参数,名为 this指针,当你调用
[cpp]  view plain copy print ?
  1. rect1.setcolor(2);  
  2. rect2.setcolor(3);  

时,编译器实际上为你做出来的代码是:
[cpp]  view plain copy print ?
  1. CRect.setcolor(2,(CRect*) &rect1);  
  2. CRect.setcolor(3,(CRect*) &rect2);  

2. 虚函数与多态

2.1 多态性(Polymorphism)

以相同的指令却调用了不同的函数,这种性质成为Polymorphism,意思是“the ability to assume many forms”(多态)。有如下四个类:
虚函数_第1张图片
[cpp]  view plain copy print ?
  1. #include <string.h>  
  2.   
  3. class CEmployee  //职员  
  4. {  
  5.     private:  
  6.         char m_name[30];  
  7.   
  8.     public:  
  9.         CEmployee();  
  10.         CEmployee(const char* nm) { strcpy(m_name, nm); }  
  11. };  
  12. //----------------------------------// 时薪职员是一种职员  
  13. class CWage : public CEmployee  
  14. {  
  15.     private :  
  16.         float m_wage;//钟点费  
  17.         float m_hours;//每周工时  
  18.   
  19.     public :  
  20.         CWage(const char* nm) : CEmployee(nm) { m_wage = 250.0; m_hours = 40.0; }  
  21.         void setWage(float wg) { m_wage = wg; }  
  22.         void setHours(float hrs) { m_hours = hrs; }  
  23.         float computePay();  
  24. };  
  25. //----------------------// 销售员是一种时薪职员  
  26. class CSales : public CWage  
  27. {  
  28.     private :  
  29.         float m_comm;//佣金  
  30.         float m_sale;//销售额  
  31.   
  32.     public :  
  33.         CSales(const char* nm) : CWage(nm) { m_comm = m_sale = 0.0; }  
  34.         void setCommission(float comm)      { m_comm = comm; }  
  35.         void setSales(float sale)          { m_sale = sale; }  
  36.         float computePay();  
  37. };  
  38. //------------------------// 经理也是一种职员  
  39. class CManager : public CEmployee  
  40. {  
  41.     private :  
  42.         float m_salary;//薪水  
  43.     public :  
  44.         CManager(const char* nm) : CEmployee(nm) { m_salary = 15000.0; }  
  45.         void setSalary(float salary)             { m_salary = salary; }  
  46.         float computePay();  
  47. };  
  48. //---------------------------------------------------------------  
  49. void main()  
  50. {  
  51.     CManager aManager("陳美靜");  
  52.     CSales   aSales("侯俊傑");  
  53.     CWage    aWager("曾銘源");  
  54. }  
1)则aManageer,aSale和aWager含有的变量如下图:
虚函数_第2张图片
注意:子类确实继承了父类的private成员,只是没有访问的权限。要访问父类的成员函数,必须使用scope resolution operator(::)明白指出。
    a)计算侯俊杰底薪应该是
[cpp]  view plain copy print ?
  1. a.Sales.CWage::computePay();  
    b)计算侯俊杰的全薪应该是
[cpp]  view plain copy print ?
  1. aSales.computePay();  
2)  父类与子类的转换
[cpp]  view plain copy print ?
  1. //销售员是时薪职员之㆒,因此这样做是合理的:  
  2. aWager = aSales; // 合理,销售员必定是时薪职员。  
  3. //这样就不合理:  
  4. aSales = aWager; // 错误,时薪职员未必是销售员。  
  5. //如果你㆒定要转换,必须使用指标,并且明显做型别转换(cast)动作 :  
  6. CWage* pWager;  
  7. CSales* pSales;  
  8. CSales aSales("侯俊杰");  
  9. pWager = &aSales; // 把一个基类指针指向子类的对象,合理且自然。  
  10. pSales = (CSales *)pWager; // 强迫转型。语法上可以,但不符合现实生活。  
3)到底会调用那个函数?
看下面代码:
[cpp]  view plain copy print ?
  1. CSales aSales("侯俊杰");  
  2. CSales* pSales;  
  3. CWage* pWager;  
  4. pSales = &aSales;  
  5. pWager = &aSales; // 以基类指针指向子类对象  
  6. pWager->setSales(800.0); // 错误(编译器会检测出来),  
  7. // 因为 CWage 并没有定义 setSales 函数  
  8. pSales->setSales(800.0); // 正确,调用 CSales::setSales 函数  
虽然pSales 和pWager 指向同一对象,但却因指针的原始类型不同而使两者之间有了差异。 如果你一个“基类指针”指向派生类的对象,那么经由该指针你只能够调用基类所定义的函数。
再看下面的代码:
[cpp]  view plain copy print ?
  1. pWager->computePay(); // 调用 CWage::computePay()  
  2. pSales->computePay(); // 调用 CSales::computePay()  
虽然aSales和pWager实际上指向的是同一个对象,但是两者调用computePay却不同。 到底应该调用哪个函数必须视指针的类型而定,与指针实际指向的对象无关。
4)
总结
  1. 如果你一个“基类指针”指向派生类的对象,那么经由该指针你只能够调用基类所定义的函数。
  2. 如果要用一个派生类指针指向一个基类对象,你必须做显式类型转换(explict cast),这种做法不推荐。
  3. 如果基类和派生类定义了相同名称的成员函数,那么通过指针调用成员函数时,到底会调用哪一个函数,必须视指针的类型而定,与指针实际指向的对象无关。

2.2 虚函数

如果将上述4个类中的computePay函数前都加上virtual保留字:
[cpp]  view plain copy print ?
  1. CEmployee* pEmp;  
  2. CWage aWager("曾铭源");  
  3. CSales aSales("侯俊杰");  
  4. CManager aManager("陈美静");  
  5. pEmp = &aWager;  
  6. cout << pEmp->computePay(); // 调用的是 CWage::computePay  
  7. pEmp = &aSales;  
  8. cout << pEmp->computePay(); // 调用的是 CSales::computePay  
  9. pEmp = &aManager;  
  10. cout << pEmp->computePay(); // 调用的是 CManager::computePay  
我们通过相同的指令“pmp->computePay()”却调用了不同的函数,这就是虚函数的作用: 实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数。

2.3 类与对象大解剖

为了达到动态绑定的目的,C++编译器通过某个表格,在执行时"间接"调用实际上欲绑定的函数。这样的表格成为 虚函数表(常被称为vtable)。每一个内含虚函数的类,编译器都会为它做出一个虚函数表,表中的每一个元素都指向一个虚函数的地址。此外,编译器当然也会类加上一项成员变量,是一个指向该虚函数表的指针(常被成为vptr)。
[cpp]  view plain copy print ?
  1. #include <iostream.h>  
  2. #include <stdio.h>  
  3.   
  4. class ClassA  
  5. {  
  6.     public:  
  7.         int m_data1;  
  8.         int m_data2;  
  9.         void func1() { }  
  10.         void func2() { }  
  11.         virtual void vfunc1() { }  
  12.         virtual void vfunc2() { }  
  13. };  
  14.   
  15. class ClassB : public ClassA  
  16. {  
  17.     public:  
  18.         int m_data3;  
  19.         void func2() { }  
  20.         virtual void vfunc1() { }  
  21. };  
  22.   
  23. class ClassC : public ClassB  
  24. {  
  25.     public:  
  26.         int m_data1;  
  27.         int m_data4;  
  28.         void func2() { }  
  29.         virtual void vfunc1() { }  
  30. };  
  31.   
  32. void main()  
  33. {  
  34.     cout << sizeof(ClassA) << endl;  
  35.     cout << sizeof(ClassB) << endl;  
  36.     cout << sizeof(ClassC) << endl;  
  37.   
  38.     ClassA a;  
  39.     ClassB b;  
  40.     ClassC c;  
  41.   
  42.     b.m_data1 = 1;  
  43.     b.m_data2 = 2;  
  44.     b.m_data3 = 3;  
  45.     c.m_data1 = 11;  
  46.     c.m_data2 = 22;  
  47.     c.m_data3 = 33;  
  48.     c.m_data4 = 44;  
  49.     c.ClassA::m_data1 = 111;  
  50.   
  51.     cout << b.m_data1 << endl;  
  52.     cout << b.m_data2 << endl;  
  53.     cout << b.m_data3 << endl;  
  54.     cout << c.m_data1 << endl;  
  55.     cout << c.m_data2 << endl;  
  56.     cout << c.m_data3 << endl;  
  57.     cout << c.m_data4 << endl;  
  58.     cout << c.ClassA::m_data1 << endl;  
  59.   
  60.     cout << &b << endl;  
  61.     cout << &(b.m_data1) << endl;  
  62.     cout << &(b.m_data2) << endl;  
  63.     cout << &(b.m_data3) << endl;  
  64.     cout << &c << endl;  
  65.     cout << &(c.m_data1) << endl;  
  66.     cout << &(c.m_data2) << endl;  
  67.     cout << &(c.m_data3) << endl;  
  68.     cout << &(c.m_data4) << endl;  
  69.     cout << &(c.ClassA::m_data1) << endl;  
  70. }  
执行结果及说明:
虚函数_第3张图片
对象a.b.c中的内容如下图所示:
虚函数_第4张图片
  1. C++类的成员函数可以想象为C语言中的函数。它时被编译器改过名称(加入了类名::,如上图中灰色框内),并加了一个this指针的参数。所以成员函数并不在对象的内存区块种,成员函数为该类所有的对象共享。
  2. 如果基类中含有虚函数,那么每一个由此类派生出来的类的对象都一个这么一个vptr。当我们通过这个对象调用虚函数时,事实上是通过vptr找到虚函数表,再找出虚函数的真正地址。
  3. 派生类会继承基类的虚函数表,当我们再派生类中改写虚函数时,虚函数表就受了影响:表中元素所指的函数地址将不再是基类的函数地址,而是派生类的函数地址。
上文说到“如果基类和派生类定义了相同名称的成员函数,那么通过指针调用成员函数时,到底会调用哪一个函数, 必须视指针的类型而定,与指针实际指向的对象无关。”而虚函数实现了,要调用哪一个函数不是视指针的类型而定,而是 跟具体指向的对象有关。这是因为,虚函数在基类和派生类都增加了一个虚函数指针vptr,当派生类改写虚函数时,改变了虚函数中实际指向的函数。 一言以蔽之,虚函数的巧妙之处在于通过虚函数指针间接的改变了要调用函数。

2.4 虚析构函数

基类的析构函数一般写成虚函数,这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。否则会造成内存泄露。
[cpp]  view plain copy print ?
  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. };  
  18. ClxBase *pTest = new ClxDerived;  
  19. pTest->DoSomething();  
  20. delete pTest;  
输出:
[plain]  view plain copy print ?
  1. Do something in class ClxDerived!  
  2. Output from the destructor of class ClxDerived!  

你可能感兴趣的:(虚函数)