• C++面向对象程序设计(第2版)第六章(多态性与虚函数)知识点总结


    C++面向对象程序设计

    参考书目:《C++面向对象程序设计》—— 谭浩强 《C++程序设计:思想与方法》—— 翁惠玉



    一、多态性的概念

           在C++ 程序设计中,多态性是指具有不同功能的函数可以用一个函数名,在面向对象方法中这样描述多态性:向不同的对象发送同一个消息,它们接收后会产生不同的行为(即方法)。函数的重载、运算符重载都是多态现象。具有不同功能的函数可以用同一个函数名,即用一个函数名调用不同内容的函数。
           从系统实现的观点看,多态性分为两类:静态多态和动态多态性。以前学过的函数重载和运算符重载属于静态多态性,在编译程序时系统就可以确定调用哪个函数,因此静态多态性又称编译时的多态性。静态多态性是通过函数重载实现的。动态多态性是在程序运行中才能确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数实现的。

    典型实例

    例:建立一个点类(point),有数据成员x,y(坐标)。以它为基类派生一个圆类,增加数据成员r(半径),再以圆为直接基类派生出一个圆柱体类,再增加数据成员h(高)要求重载运算符<<和>>使之能输出以上的类对象。

    (1)声明基类point类
    class Point
    {
        protected:
    	   float x, y;
        public:
    	   Point(float = 0, float = 0);
    	   void setPoint(float, float);
    	   float getX() const { return x; }
    	   float getY() const { return y; }
    	   friend ostream & operator<<(ostream &, const Point &);
    };
    Point::Point(float a, float b) { x = a; y = b; }
    void Point::setPoint(float a, float b) { x = a; y = b; }
    ostream & operator<<(ostream &output, const Point &p)
    {
    	output << "[" << p.x << "," << p.y << "]" << endl;
    	return output;
    }
    void main()
    {
    	Point p(3.5, 6.4);
    	cout << "x=" << p.getX() <<",y=" << p.getY() << endl;
    	p.setPoint(8.5, 6.8);
    	cout << "p(new):" << p << endl;
    }
    

    在这里插入图片描述

    声明派生类circle类
    class Circle :public Point
    {
        protected:
    	   float radius;
        public:
    	   Circle(float x = 0, float y = 0, float r = 0);
    	   void setRadius(float r) { radius = r; }
    	   float getRadius() const { return radius; }
    	   float area() const;
    	   friend ostream &operator<<(ostream &, const Circle &);
    };
    Circle::Circle(float a, float b, float r) :Point(a, b), radius(r) {}
    float Circle::area() const { return 3.14159*radius*radius; }
    ostream &operator<<(ostream &output, const Circle &c)
    {
    	output << "Center=[" << c.x << "," << c.y << "], Radius=" << c.radius << ", area=" << c.area() << endl;
    	return output;
    }
    void main()
    {
    	Circle c(3.5, 6.4, 5.2);
    	cout << "original circle:\nx="<< c.getX() << ", y="<< c.getY() << ", r=" << c.getRadius() <<", area="<<c.area()<<endl;
    		c.setRadius(7.5);
    	c.setPoint(5, 5);
    	cout << "new circle:\n" << c;
    	Point &pRef = c;
    	cout << "pRef:" << pRef;
    }
    

    在这里插入图片描述

    (3)声明circle 的派生类cylinder类
    class Cylinder :public Circle
    {
        public:
    	   Cylinder(float x = 0, float y = 0, float r = 0, float h = 0);
    	   void setHeight(float h) { height = h; }
    	   float getHeight() const { return height; }
    	   float area() const;
    	   float volume() const;
    	   friend ostream& operator<<(ostream&, const Cylinder&);
        protected:	  
    	   float height;
    };
    Cylinder::Cylinder(float a, float b, float r, float h) :Circle(a, b, r) { height = h; }
    float Cylinder::area() const
    {
    	return 2 * Circle::area() + 2 * 3.14159*radius*height;
    }
    float Cylinder::volume() const { return Circle::area()*height; }
    ostream &operator<<(ostream &output, const Cylinder& cy)
    {
    	output << "Center=[" << cy.x << "," << cy.y << "], r=" << cy.radius<< ", h=" << cy.height << " \narea=" << cy.area() <<", volume=" << cy.volume() << endl;
    	return output;
    }
    int main()
    {
    	Cylinder cy1(3.5, 6.4, 5.2, 10);
    	cout << "\n original cylinder:\n x=" << cy1.getX() << ", y="
    		<< cy1.getY() << ", r=" << cy1.getRadius() << ", h="
    		<< cy1.getHeight() << "\narea=" << cy1.area()
    		<< ", volume="<<cy1.volume()<<endl;
    	cy1.setHeight(15);
    	cy1.setRadius(7.5);
    	cy1.setPoint(5, 5);
    	cout << "\nnew cylinder:\n" << cy1;
    	Point &pRef = cy1;
    	cout << "\npRef as a point:" << pRef;
    	Circle &cRef = cy1;
    	cout << "\ncRef as a Circle:" << cRef;
    	return 0;
    }
    

    在这里插入图片描述

    注:
    可以在一个工程里设计多个头文件 “point.h”、“cylinder.h”、“circle.h”分别定义各个类。
    再设计 “point.cpp” 、“circle.cpp”, “cylinder.h”多个源程序文件分别定义各个类的成员函数。
    设计一个主函数,通过对象访问类的成员函数。
    

    二、虚函数

    1.虚函数的作用

           在一个类中不能定义两个名字相同,参数个数和类型都相同的函数。在类家族中,不同层次的类可以出现名字相同,参数个数和类型都相同而功能不同的函数。编译系统按照同名覆盖的原则决定调用哪个函数。
           虚函数的工作原理是在派生类中定义与基类函数同名的函数,通过基类指针或引用来访问基类或派生类中的同名函数。上一节所述的赋值兼容性规则是多态性的基础。同一个函数可以统一处理具有公有派生关系的基类的对象和派生类对象。

           虚函数的使用方法:

    • 在基类用virtual声明成员函数为虚函数。在派生类中重新定义同名函数,让它具有新的功能。
    • 在派生类中重新定义此函数时,要求函数名、函数类型、参数个数和类型与基类的虚函数相同,根据需要重新定义函数体。C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数自动成为虚函数。
    • 定义一个指向基类对象的指针变量,并让它获得同一类族中某个对象的地址。
    • 用该指针变量调用虚函数,调用的就是该对象所属类的虚函数。
    • 构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。
    • 使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。
    class Student
    {
        public:   	
    	   Student(int, string, float);
    	   virtual void display();
        protected:  	
    	   int num;   	
    	   string name;   	
    	   float score;
    };
    Student::Student(int n, string nam, float s)
    {
    	num = n; name = nam; score = s;
    }
    void Student::display()
    {
    	cout << "num:" << num << "\nname:" << name << "\nscore:"<< score << "\n\n";
    }
    class Graduate :public Student
    {
        public:   	
    	   Graduate(int, string, float, float);
    	   void display();
        private:	   
    	   float pay;
    };
    Graduate::Graduate(int n, string nam, float s, float p):Student(n, nam, s), pay(p) {}
    void Graduate::display()
    {
    	cout << "num:" << num << "\nname:" << name << "\nscore:"<< score << "\npay=" << pay << endl;
    }
    int main()
    {
    	Student stud1(1001, "Li", 87.5);
    	Graduate grad1(2001, "Wang", 98.5, 563.5);
    	Student *pt = &stud1;
    	pt->display();
    	pt = &grad1;
    	pt->display();
    	return 0;
    }
    

    在这里插入图片描述

    函数重载处理的是同一层次中的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题。前者要求函数名相同,但函数的参数或类型不同;后者要求的是不仅函数名相同,而且函数参数和类型也要相同。
    

    2.静态关联与动态关联

           多态性的两种理解:(1)具有不同功能的函数可以用同一个函数名,可以用一个函数名调用不同内容的函数。(2)发出同样的消息(对成员函数的调用)被不同类型的对象接收时导致完全不同的行为(不同的函数实现)。
           静态多态的实现方式有函数重载运算符重载
           动态多态的实现主要通过虚函数
           当类中任何函数前带virtual 时,编译程序将暗自在类中增加一个数据成员,在这称它为 VPTR ,它是一个指向函数指针表的指针,因此,With   virtual类对象占用的内存空间比No   virtual类的对象大。

    class base
    { 
        public:   
           virtual void Fn(int x){cout<< “在基类内x=<<x<<endl;  }
    };
    class subclass:public base
    { 
        public:    
           virtual void Fn( float x){cout<< “在派生类内x=<<x<<endl;  }
    };
    void test (base & b)
    {   
        int i=1;          
        b.Fn( i );
        float f=2.0;    
        b.Fn( f );  
    }
    void main()
    {   
        base  bc;    
        subclass  sc;
        cout<<“调用test(bc) \n”;    
        test(bc);
        cout<<“调用test(sc) \n”;    
        test(sc);
    }
    

    在这里插入图片描述

    注:只能将类的成员函数声明为虚函数。一个成员函数被声明为虚函数后,在同一类族不能再定义一个与该虚函数相同的非虚函数。
    

    3.什么情况下应当声明虚函数

           声明虚函数时,主要考虑以下几点:

    • 首先看成员函数的类是否会作为基类。然后看在派生类里看它是否会被改变功能,如要改变,一般应该将它声明为虚函数。否则不要将它声明为虚函数。
    • 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
    • 有时在定义虚函数时,函数体是空的,只是定义一个虚函数名,具体功能留给派生类去添加。
    class base
    {
        protected:
    	   int x;
        public:
    		base(int a) { x = a; }
    	   virtual void print(){ cout << "base = "<< x << endl; }
    };
    class subclass :public base
    {
        public:
    	   subclass(int a):base(a){}
    	   virtual void print(){ cout << "First derivation\n"<< x << endl; }
    };
    class sub2class :public base
    {
        public:
    	sub2class(int a) :base(a) {}
    	virtual void print() { cout << "Second derivation\n" << x << endl; }
    };
    int main()
    {
    	base  *p, obj1(1);
    	subclass obj2(2);
    	sub2class obj3(3);
    	p = &obj1;
    	p->print();
    	p = &obj2;
    	p->print();
    	p = &obj3;
    	p->print();
    	obj2.print();
    	obj3.print();
    	return 0;
    }
    

    在这里插入图片描述

    4.虚析构函数

           当派生类的对象撤销时一般先调用派生类的析构函数,然后调用基类的析构函数。当用new运算符建立一个动态对象时,如基类中有析构函数,并且定义了一个指向基类的指针变量。在程序中用带指针参数的delete运算符撤销对象时,系统只会执行基类的析构函数,而不执行派生类的析构函数。

    将基类的析构函数声明为虚函数:
    先调用派生类的析构函数,再调用基类的析构函数。当基类的析构函数为虚函数时无论指针指的是同一类族中的哪一个类对象,撤销对象时,系统会采用动态关联,调用相应的析构函数。
    如果基类的析构函数声明为虚函数,那么其所有派生类的析构函数也自动成为虚函数。
    构造函数不能声明为虚函数。
    
    class Point
    {
        public:
    	   Point() { cout << "Point Cons!" << endl; }
    	   virtual ~Point() { cout << "Executing Point destructor" << endl; }
    };
    class Circle :public Point
    {
        public:
    		Circle() { cout << "Circle Cons!" << endl; }
    		~Circle() { cout << "Executing Circle destructor" << endl; }
        private:
    		int radius;
    };
    int main()
    {
    	Point *p = new Circle;
    	delete p;
    	Circle *pc = new Circle;
    	delete pc;
    	return 0;
    }
    

    在这里插入图片描述

           何时需要虚析构函数:

    • 当你可能通过基类指针删除派生类对象时
    • 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数成为虚拟的。
    • 继承中,一般习惯声明虚析构函数,即使基类并不需要,也显示定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。

    三、纯虚函数与抽象类

    1.纯虚函数

           只声明,不定义的虚函数称为纯虚函数。
           纯虚函数一般格式:virtual 函数类型 函数名(参数表)=0;

           纯虚函数的作用:

    • 在许多情况下,基类中不能为虚函数给出一个有意义的定义,而将它说明为纯虚函数,其作用是:为派生类提供一个一致的接口(界面)。它的定义留给派生类来做,派生类根据需要来定义各自的实现。

    • 如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数进行定义(实现),则该虚函数在派生类中仍然为纯虚函数。

      注:1.一个类可以说明一个或多个纯虚函数。2.纯虚函数没有函数体,不能被调用,所在的抽象类,也不能直接进行实例化。3.空的虚函数,函数体为空。但所在的类可以实例化。
      

    2.抽象类

           带有纯虚函数的类是抽象类,抽象类刻画了一组子类的公共操作接口的通用语义,这些接口的语义也传给子类。一般而言,抽象类只描述这组子类共同操作接口,而完整的实现留给子类。

           抽象类的说明:

    • 抽象类是一个特殊的类,是为了抽象和设计的目的而建立的,它处于继承层次的结构的较上层,即只能用作其他类的基类,抽象类是不能定义对象的。
    • 从一个抽象类派生的类必须提供纯虚函数的实现代码或在该派生类中仍将它说明为纯虚函数,否则编译出错。
    • 抽象类不能用作参数类型、函数返回类型或显式转换的类型,但可以说明指向抽象类的指针和引用,此指针可以指向它的派生类,实现多态性。

    3.应用实例

    class Shape
    {
        public:
    	   virtual float area() const { return 0.0; }
    	   virtual float volume() const { return 0.0; }
    	   virtual void shapeName() const = 0;
    };
    class Point :public Shape
    {
        protected:	  
    	   float x, y;
        public:
    	   Point(float a = 0, float b = 0) { x = a; y = b; }
    	   void setPoint(float a , float b) { x = a; y = b; }
    	   float getX() const { return x; }
    	   float getY() const { return y; }
    	   virtual void shapeName() const { cout << "Point:"; }
    	   friend ostream & operator<<(ostream &, const Point &);
    };
    ostream & operator<<(ostream &output, const Point &p)
    { output << "[" << p.x << "," << p.y << "]"; return output;  }
    class Circle :public Point
    {
        protected:  
    	   float radius;
    	public:
    		Circle(float x = 0, float y = 0, float r = 0);
    		void setRadius(float r) { radius = r; }
    		float getRadius() const { return radius; }
    		virtual float area() const;
    		virtual void shapeName() const { cout << "Circle:"; }
    		friend ostream &operator<<(ostream &, const Circle &);
    };
    Circle::Circle(float a, float b, float r) :Point(a, b), radius(r) {}
    float Circle::area() const { return 3.14159*radius*radius; }
    ostream &operator<<(ostream &output, const Circle &c)
    {
    	output << "[" << c.x << "," << c.y << "], r=" << c.radius;
    	return output;
    }
    class Cylinder :public Circle
    {
    	 public:
    		Cylinder(float x = 0, float y = 0, float r = 0, float h = 0);
    		void setHeight(float h) { height = h; }
    		float getHeight() const { return height; }
    		virtual float area() const;
    		virtual float volume() const;
    		virtual void shapeName() const { cout << "Cylinder:"; }
    		friend ostream& operator<<(ostream&, const Cylinder&);
    	 protected:  float height;
    };
    Cylinder::Cylinder(float a, float b, float r, float h):Circle(a, b, r), height(h) {}
    float Cylinder::area() const
    {
    	return 2 * Circle::area() + 2 * 3.14159*radius*height;
    }
    float Cylinder::volume() const { return Circle::area()*height; }
    ostream &operator<<(ostream &output, const Cylinder& cy)
    {
    	output << "[" << cy.x << "," << cy.y << "], r=" << cy.radius<< ", h=" << cy.height;
    	return output;
    }
    int main()
    {
    		Point point(3.2, 4.5);
    		Circle circle(2.4, 12, 5.6);
    		Cylinder cylinder(3.5, 6.4, 5.2, 10.5);
    		point.shapeName();   //静态关联
    		cout << point << endl;
    		circle.shapeName();
    		cout << circle << endl;
    		cylinder.shapeName();
    		cout << cylinder << endl << endl;
    		Shape *pt;
    		pt = &point;    //动态关联
    		pt->shapeName();
    		cout << "x=" << point.getX() << ",y="<< point.getY() << "\narea="<< pt->area() << "\nvolume="<< pt->volume() << "\n\n";
    		pt = &circle;
    		pt->shapeName();
    		cout << "x=" << circle.getX()<< ",y=" << circle.getY() <<"\narea=" << pt->area() <<"\nvolume=" << pt->volume()<< "\n\n";
    		pt = &cylinder;
    		pt->shapeName();
    		cout << "x=" << cylinder.getX()<< ",y=" << cylinder.getY()<< "\narea=" << pt->area()<< "\nvolume=" << pt->volume() << "\n\n";
    		return 0;
    }
    
    

    在这里插入图片描述

  • 相关阅读:
    winform使用ProcessStartInfo打印,默认打印机,选择打印机等方式
    Dhtmlx Event Calendar 付费版使用
    遥感领域最热门的研究主题介绍
    Linux 常用命令
    七年之痒!一个 PHP 程序员职业生涯的自述
    java计算机毕业设计基于安卓Android的校园单车租赁App(源码+系统+mysql数据库+Lw文档)
    向已有项目添加LICENSE
    数据在计算机中的表示:原码 反码 补码 移码
    埃及分数 ← IDA*
    将3D MAX设计模型导入NX1988
  • 原文地址:https://blog.csdn.net/weixin_43312470/article/details/108045969