C++的重要性质:虚函数和多态性

By | 04月09日
Advertisement

1. 封装、继承和this指针

1.1 封装(Encapsulation)

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

1.2 继承(Inheritance)

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

1.3 this指针

矩形类CRect如下:

class CRect { private:         int m_color; public:     void setcolor(int color)     {         m_color=color;     } };

有两个CRect对象rect1和rect2,各有各自的m_color成员变量。rect1.setcolor和rect2.setcolor调用的是唯一的CRect::setcolor成员函数,却处理了各自的m_color。

这是因为成员函数是属于类的而不是属于某个对象的,只有一个。成员函数都有一个隐藏参数,名为this指针,当你调用

rect1.setcolor(2); rect2.setcolor(3);

时,编译器实际上为你做出来的代码是:

CRect.setcolor(2,(CRect*) &rect1); CRect.setcolor(3,(CRect*) &rect2);

2. 虚函数与多态

2.1 多态性(Polymorphism)

以相同的指令却调用了不同的函数,这种性质成为Polymorphism,意思是“the ability to assume many forms”(多态)。有如下四个类:
C++的重要性质:虚函数和多态性

#include <string.h>  class CEmployee  //职员 {     private:         char m_name[30];      public:         CEmployee();         CEmployee(const char* nm) { strcpy(m_name, nm); } }; //----------------------------------// 时薪职员是一种职员 class CWage : public CEmployee {     private :         float m_wage;//钟点费         float m_hours;//每周工时      public :         CWage(const char* nm) : CEmployee(nm) { m_wage = 250.0; m_hours = 40.0; }         void setWage(float wg) { m_wage = wg; }         void setHours(float hrs) { m_hours = hrs; }         float computePay(); }; //----------------------// 销售员是一种时薪职员 class CSales : public CWage {     private :         float m_comm;//佣金         float m_sale;//销售额      public :         CSales(const char* nm) : CWage(nm) { m_comm = m_sale = 0.0; }         void setCommission(float comm)      { m_comm = comm; }         void setSales(float sale)          { m_sale = sale; }         float computePay(); }; //------------------------// 经理也是一种职员 class CManager : public CEmployee {     private :         float m_salary;//薪水     public :         CManager(const char* nm) : CEmployee(nm) { m_salary = 15000.0; }         void setSalary(float salary)             { m_salary = salary; }         float computePay(); }; //--------------------------------------------------------------- void main() {     CManager aManager("陳美靜");     CSales   aSales("侯俊傑");     CWage    aWager("曾銘源"); }

1)则aManageer,aSale和aWager含有的变量如下图:
C++的重要性质:虚函数和多态性

注意:子类确实继承了父类的private成员,只是没有访问的权限。要访问父类的成员函数,必须使用scope resolution operator(::)明白指出。
a)计算侯俊杰底薪应该是

a.Sales.CWage::computePay();

b)计算侯俊杰的全薪应该是

aSales.computePay();

2) 父类与子类的转换

//销售员是时薪职员之㆒,因此这样做是合理的: aWager = aSales; // 合理,销售员必定是时薪职员。 //这样就不合理: aSales = aWager; // 错误,时薪职员未必是销售员。 //如果你㆒定要转换,必须使用指标,并且明显做型别转换(cast)动作 : CWage* pWager; CSales* pSales; CSales aSales("侯俊杰"); pWager = &aSales; // 把一个基类指针指向子类的对象,合理且自然。 pSales = (CSales *)pWager; // 强迫转型。语法上可以,但不符合现实生活。

3)到底会调用那个函数?
看下面代码:

CSales aSales("侯俊杰"); CSales* pSales; CWage* pWager; pSales = &aSales; pWager = &aSales; // 以基类指针指向子类对象 pWager->setSales(800.0); // 错误(编译器会检测出来), // 因为 CWage 并没有定义 setSales 函数 pSales->setSales(800.0); // 正确,调用 CSales::setSales 函数

虽然pSales 和pWager 指向同一对象,但却因指针的原始类型不同而使两者之间有了差异。如果你一个“基类指针”指向派生类的对象,那么经由该指针你只能够调用基类所定义的函数。
再看下面的代码:

pWager->computePay(); // 调用 CWage::computePay() pSales->computePay(); // 调用 CSales::computePay()

虽然aSales和pWager实际上指向的是同一个对象,但是两者调用computePay却不同。到底应该调用哪个函数必须视指针的类型而定,与指针实际指向的对象无关。
4)
总结

  1. 如果你一个“基类指针”指向派生类的对象,那么经由该指针你只能够调用基类所定义的函数。
  2. 如果要用一个派生类指针指向一个基类对象,你必须做显式类型转换(explict cast),这种做法不推荐。
  3. 如果基类和派生类定义了相同名称的成员函数,那么通过指针调用成员函数时,到底会调用哪一个函数,必须视指针的类型而定,与指针实际指向的对象无关。

2.2 虚函数

如果将上述4个类中的computePay函数前都加上virtual保留字:

CEmployee* pEmp; CWage aWager("曾铭源"); CSales aSales("侯俊杰"); CManager aManager("陈美静"); pEmp = &aWager; cout << pEmp->computePay(); // 调用的是 CWage::computePay pEmp = &aSales; cout << pEmp->computePay(); // 调用的是 CSales::computePay pEmp = &aManager; cout << pEmp->computePay(); // 调用的是 CManager::computePay

我们通过相同的指令“pmp->computePay()”却调用了不同的函数,这就是虚函数的作用:实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数。

2.3 类与对象大解剖

为了达到动态绑定的目的,C++编译器通过某个表格,在执行时"间接"调用实际上欲绑定的函数。这样的表格成为虚函数表(常被称为vtable)。每一个内含虚函数的类,编译器都会为它做出一个虚函数表,表中的每一个元素都指向一个虚函数的地址。此外,编译器当然也会类加上一项成员变量,是一个指向该虚函数表的指针(常被成为vptr)。

#include <iostream.h> #include <stdio.h>  class ClassA {   public:         int m_data1;        int m_data2;        void func1() { }        void func2() { }        virtual void vfunc1() { }       virtual void vfunc2() { } };  class ClassB : public ClassA {    public:         int m_data3;        void func2() { }        virtual void vfunc1() { } };  class ClassC : public ClassB {    public:         int m_data1;        int m_data4;        void func2() { }        virtual void vfunc1() { } };  void main() {     cout << sizeof(ClassA) << endl;     cout << sizeof(ClassB) << endl;     cout << sizeof(ClassC) << endl;     ClassA a;   ClassB b;   ClassC c;   b.m_data1 = 1;  b.m_data2 = 2;  b.m_data3 = 3;  c.m_data1 = 11;     c.m_data2 = 22;     c.m_data3 = 33;     c.m_data4 = 44;     c.ClassA::m_data1 = 111;    cout << b.m_data1 << endl;  cout << b.m_data2 << endl;  cout << b.m_data3 << endl;  cout << c.m_data1 << endl;  cout << c.m_data2 << endl;  cout << c.m_data3 << endl;  cout << c.m_data4 << endl;  cout << c.ClassA::m_data1 << endl;      cout << &b << endl;     cout << &(b.m_data1) << endl;   cout << &(b.m_data2) << endl;   cout << &(b.m_data3) << endl;   cout << &c << endl;     cout << &(c.m_data1) << endl;   cout << &(c.m_data2) << endl;   cout << &(c.m_data3) << endl;   cout << &(c.m_data4) << endl;   cout << &(c.ClassA::m_data1) << endl; }

执行结果及说明:
C++的重要性质:虚函数和多态性

对象a.b.c中的内容如下图所示:
C++的重要性质:虚函数和多态性

  1. C++类的成员函数可以想象为C语言中的函数。它时被编译器改过名称(加入了类名::,如上图中灰色框内),并加了一个this指针的参数。所以成员函数并不在对象的内存区块种,成员函数为该类所有的对象共享。
  2. 如果基类中含有虚函数,那么每一个由此类派生出来的类的对象都一个这么一个vptr。当我们通过这个对象调用虚函数时,事实上是通过vptr找到虚函数表,再找出虚函数的真正地址。
  3. 派生类会继承基类的虚函数表,当我们再派生类中改写虚函数时,虚函数表就受了影响:表中元素所指的函数地址将不再是基类的函数地址,而是派生类的函数地址。

上文说到“如果基类和派生类定义了相同名称的成员函数,那么通过指针调用成员函数时,到底会调用哪一个函数,必须视指针的类型而定,与指针实际指向的对象无关。”而虚函数实现了,要调用哪一个函数不是视指针的类型而定,而是跟具体指向的对象有关。这是因为,虚函数在基类和派生类都增加了一个虚函数指针vptr,当派生类改写虚函数时,改变了虚函数中实际指向的函数。一言以蔽之,虚函数的巧妙之处在于通过虚函数指针间接的改变了要调用函数。

2.4 虚析构函数

基类的析构函数一般写成虚函数,这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。否则会造成内存泄露。

class ClxBase { public:     ClxBase() {};     virtual ~ClxBase() {};      virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; }; };  class ClxDerived : public ClxBase { public:     ClxDerived() {};     ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };       void DoSomething() { cout << "Do something in class ClxDerived!" << endl; }; }; ClxBase *pTest = new ClxDerived; pTest->DoSomething(); delete pTest;

输出:

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

参考资料:

主要参考 侯俊杰,《深入浅出MFC》第二章 C++的重要性质

Similar Posts:

  • 第十三周实验--任务1--我对虚函数、多态性和抽象类的理解

    /* (程序头部注释开始) * 程序的版权和版本声明部分 * Copyright (c) 2011, 烟台大学计算机学院学生 * All rights reserved. * 文件名称:我对虚函数.多态性和抽象类的理解 * 作 者: 雷恒鑫 * 完成日期: 2012 年 05月11 日 * 版 本 号: V1.0 * 对任务及求解方法的描述部分 * 输入描述: * 问题描述:1. 进一步多态性的基本概念2. 学会利用虚函数实现多态性3. 学会在设计中利用纯虚函数构造抽象基类 * 程序输出: *

  • c++笔记(9):联编、虚函数和多态性、异质表

    1. 联编是指一个程序模块.代码之间互相关联的过程. 静态联编,是程序的匹配.连接在编译阶段实现,也称为早期匹配.重载函数使用静态联编. 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编.switch 语句和 if 语句是动态联编的例子. 2. 静态联编: 普通成员函数重载可表达为两种形式: <1>在一个类说明中重载 <2>基类的成员函数在派生类重载:根据参数的特征加以区分;使用" :: "加以区分;根据类对象加以区分 基类指针和派生类指针与基类对象和派

  • c++虚函数、多态性与虚表

    多态 多态性就是指同样的消息被类的不同的对象接收时导致的完全不同的行为的一种现象.这里所说的消息即对类成员函数的调用.多态实质是一个函数名称的多种形态. C++支持两种不同类型的多态:一种是编译时的多态,另一种是运行时的多态.在编译时的多态是通过静态联编实现的:而在运行时的多态则是通过动态联编实现的. 函数联编:对一个函数的调用,在编译或运行时确定将其连接到相应的函数体的代码,实质是把一个标示名与一个存储地址联系在一起的过程. 在程序中可以把一个公有派生类对象当作其基类对象来处理,一个公有派生类

  • 第十三周实验报告(一)用自己的话总结对虚函数、多态性和抽象类的理解

    理解: 虚函数:使用一种调用方式,即能调用派生类又能调用基类,使程序更加清晰,用起来也方便,好东西! 多态:使不同功能的函数使用同一个 函数名调用不同内容的函数,从而有多种性能: 抽象类顾名思义只有成员函数没有数据成员,然后用派生类将函数具体化,方便派生类函数使用.

  • 第13周实验报告1 :虚函数、多态性和抽象类的理解

    虚函数的作用就是使派生类能够通过基类的指针变量调用自己的与基类重名的成员,但是基类的内容还能够通过基类调用. 多态性分动静多态,静态的是指函数的重载,重载函数需要程序员在编码期就对函数的各种功能选择进行足够的预计和设计,并且功能实现具有局限性.动态多态既是以虚函数的方式,让一个派生类的族群之间产生符合现实社会的一些逻辑关系,在这些逻辑关系下在对各个对象进行调用更加方便,和符合人的逻辑思维,简化了软件开发的复杂度,增加了程序的可维护性等. 抽象类可以把它看成是我们建房子时候的设计图,通过这些设计图

  • 8.6 多态性与虚函数

    多态性是面向对象程序设计的关键技术之一.利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能.若程序设计语言不支持多态性,不能称为面向对象的语言. 在C++中有两种多态性: 编译时的多态性:通过函数的重载和运算符的重载来实现的. 运行时的多态性:在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据具体情况来动态地确定.它是通过类继承关系和虚函数来实现的,目的也是建立一种通用的程序. 虚函数的定义 ◆ 1.定义格式 虚函数是一个类的成员函数,定义格式如下:

  • C++中虚函数学习笔记

    C++中虚函数学习笔记 文/heiyeluren 因为最近学习C++的面向对象,所以了解了面向对象的三大特点: 封装.继承.多态性,学习多态性的时候,首先涉及的就是虚函数,我就把我学习虚函数的一些想法记录下来. 虚函数是为了实现某种功能而假设的函数,虚函数只能是类中的一个成员函数,不能是静态成员,使用关键字virtual用于在类中说明改函数是虚函数. 虚函数更是为了实现面向对象的多态性而产生的,使用虚函数和多态性能够简化代码长度,支持更简单的顺序,便于程序的调试,维护. 虚函数的定义方法: cl

  • C++中的虚函数(一)

    虽然很难找到一本不讨论多态性的C++书籍或杂志,但是,大多数这类讨论使多态性和C++虚函数的使用看起来很难.我打算在这篇文章中通过从几个方面和结 合一些例子使读者理解在C++中的虚函数实现技术.说明一点,写这篇文章只是想和大家交流学习经验因为本人学识浅薄,难免有一些错误和不足,希望大家批评 和指正,在此深表感谢! 一. 基本概念 首先,C++通过虚函数实现多态."无论发送消息的对象属于什么类,它们均发送具有同一形式的消息,对消息的处理方式可能随接手消息的对象而变"的处理方 式被称为多态

  • 构造 析构 虚函数

    构造函数能否声明为虚函数 否 子类继承父类,考虑到构造函数的顺序,对于子类的构造,先是调用父类的构造函数生成父类数据结构,然后再调用子类定义的构造函数,补充其他初始化工作 而虚函数主要用在多态里面,父类的指针或引用指向子类结构,相同的函数声明,体现不同的实现过程,根本上是虚函数表指针机制决定的,虚函数表指针是虚函数的唯一入口 如果父类构造函数声明为虚函数,子类的构造函数和父类同名,那么子类在构造的时候不会调用父类的构造函数,因为声明为虚函数嘛,那么父类的构造将没有执行,父类都没有构造,子类如何生

  • 一个关于多态之虚函数的例子

    程序实践多态性工资发放管理系统 目录(?)[+] 引言 本博文通过包含了一个公司支付系统中各种雇员情况的一个继承层次来讨论基类和派生类之间的关系.佣金雇员(表示为一个基类对象)的薪水完全是销售提成,带底薪佣雇员(表示为一个派生类的对象)的薪水由底薪和销售提成组成.以及创建一个增强的雇员类层次结构,以解决下面的问题: 假设某家公司按周支付雇员工资,雇员一共有有4类: 定薪雇员:不管每周工作多长时间都领取固定的周薪 钟点雇员:按工作的小时数领取工资,并且可以领取超过40个小时之外的加班费 佣金雇员:

Tags: