zoukankan      html  css  js  c++  java
  • C++学习笔记-多态

    多态作为面向对象的重要概念,在如何一门面向对象编程语言中都有着举足轻重的作用,学习多态,有助于更好地理多态的行为
    多态性(Polymorphism)是指一个名字,多种语义;或界面相同,多种实现。
    重载函数是多态性的一种简单形式。
    虚函数允许函数调用与函数体的联系在运行时才进行,称为动态联编

    静态联编

    联编是指一个程序模块、代码之间互相关联的过程
    静态联编,是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编
    动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编。switch语句和if语句是动态联编的例子
    普通成员函数重载可表达为两种形式:

    • 在一个类说明中重载
    Show(int , char);
    Show(char * , float);
    
    • 基类的成员函数在派生类重载。有 3 种编译区分方法:
      • 根据参数的特征加以区分
      Show(int , char);
      Show(char * , float);
      
      • 使用::加以区分
      A :: Show();
      B :: Show();
      
      • 根据类对象加以区分
      //这其实是通过this指针区分
      Aobj.Show();//调用	A :: Show()
      Bobj.Show();//调用	B :: Show()
      

    类指针的关系

    基类指针和派生类指针与基类对象和派生类对象4种可能匹配:

    • 直接用基类指针引用基类对象
    • 直接用派生类指针引用派生类对象
    • 用基类指针引用一个派生类对象
    • 用派生类指针引用一个基类对象

    基类指针引用派生类对象

    class B : public A

    A    * p ;// 指向类型 A 的对象的指针
    A    A_obj ;// 类型 A 的对象
    B    B_obj ;// 类型 B 的对象
    p = &A_obj ;// p 指向类型 A 的对象
    p = &B_obj ;// p 指向类型 B 的对象,它是 A 的派生类
    

    利用 p,可以通过 B_obj 访问所有从 A 类继承的元素,但不能用 p访问 B 类自定义的元素(除非用了显式类型转换)

    派生类指针引用基类对象

    派生类指针只有经过强制类型转换之后,才能引用基类对象
    class DateTime : public Data

    ((Date *)this) -> Print();
    

    虚函数和动态联编

    冠以关键字 virtual 的成员函数称为虚函数
    实现运行时多态的关键首先是要说明虚函数,另外,必须用基类指针调用派生类的不同实现版本

    虚函数和基类指针

    基类指针虽然获取派生类对象地址,却只能访问派生类从基类继承的成员

    #include <iostream>
    using namespace std;
    
    class Base
    {
    public:
    	Base(char xx)
    	{
    		x = xx;
    	}
    	void who()
    	{
    		cout << "Base class: " << x << "
    ";
    	}
    protected:
    	char x;
    };
    
    class First_d : public Base
    {
    public:
    	First_d(char xx, char yy) : Base(xx)
    	{
    		y = yy;
    	}
    	void who()
    	{
    		cout << "First derived class: " << x << ", " << y << "
    ";
    	}
    protected:
    	char y;
    };
    
    class Second_d : public First_d
    {
    public:
    	Second_d(char xx, char yy, char zz) : First_d(xx, yy)
    	{
    		z = zz;
    	}
    	void who()
    	{
    		cout << "Second derived class: " << x << ", " << y << ", " << z << "
    ";
    	}
    protected:
    	char z;
    };
    
    void main()
    {
    	Base B_obj('A');
    	First_d F_obj('T', 'O');
    	Second_d S_obj('E', 'N', 'D');
    	Base * p;
    
    	p = &B_obj;
    	p->who();
    
    	p = &F_obj;
    	p->who();
    
    	p = &S_obj;
    	p->who();
    
    	F_obj.who();
    
    	((Second_d *)p)->who();
    
    	system("pause");
    }
    

    输出结果为

    Base class: A
    Base class: T
    Base class: E
    First derived class: T, O
    Second derived class: E, N, D
    请按任意键继续. . .

    修改程序,定义虚函数

    #include <iostream>
    using namespace std;
    
    class Base
    {
    public:
    	Base(char xx)
    	{
    		x = xx;
    	}
    	virtual void who()
    	{
    		cout << "Base class: " << x << "
    ";
    	}
    protected:
    	char x;
    };
    
    class First_d : public Base
    {
    public:
    	First_d(char xx, char yy) : Base(xx)
    	{
    		y = yy;
    	}
    	void who()
    	{
    		cout << "First derived class: " << x << ", " << y << "
    ";
    	}
    protected:
    	char y;
    };
    
    class Second_d : public First_d
    {
    public:
    	Second_d(char xx, char yy, char zz) : First_d(xx, yy)
    	{
    		z = zz;
    	}
    	void who()
    	{
    		cout << "Second derived class: " << x << ", " << y << ", " << z << "
    ";
    	}
    protected:
    	char z;
    };
    
    void main()
    {
    	Base B_obj('A');
    	First_d F_obj('T', 'O');
    	Second_d S_obj('E', 'N', 'D');
    	Base * p;
    
    	p = &B_obj;
    	p->who();
    
    	p = &F_obj;
    	p->who();
    
    	p = &S_obj;
    	p->who();
    
    	system("pause");
    }
    

    输出结果为:

    Base class: A
    First derived class: T, O
    Second derived class: E, N, D
    请按任意键继续. . .

    结论:
    由于who()的虚特性随着p指向不同对象,this指针作类型转换执行不同实现版本

    注意事项:

    • 一个虚函数,在派生类层界面相同的重载函数都保持虚特性
    • 虚函数必须是类的成员函数
    • 不能将友员说明为虚函数,但虚函数可以是另一个类的友员
    • 析构函数可以是虚函数,但构造函数不能是虚函数

    虚函数的重载特性

    • 在派生类中重载基类的虚函数要求函数名、返回类型、参数个数、参数类型和顺序完全相同
    • 如果仅仅返回类型不同,C++认为是错误重载
    • 如果函数原型不同,仅函数名相同,丢失虚特性
    class base
    {
    public:
    	virtual void vf1();
    	virtual void vf2();
    	virtual void vf3();
    	void f();
    };
    
    class derived : public base
    {
    public:
    	void vf1();// 虚函数
    	void vf2(int);// 重载,参数不同,虚特性丢失
    	//char vf3();// error,仅返回类型不同
    	void f();// 非虚函数重载
    };
    
    void  g()
    {
    	derived d;
    	base *bp = &d;// 基类指针指向派生类对象
    	bp->vf1();// 调用 deriver :: vf1()
    	bp->vf2();// 调用 base :: vf2()
    	bp->f();// 调用 base::f()
    };
    

    虚析构函数

    • 构造函数不能是虚函数。建立一个派生类对象时,必须从类 层次的根开始,沿着继承路径逐个调用基类的构造函数
    • 析构函数可以是虚的。虚析构函数用于指引 delete 运算符正 确析构动态对象
      普通析构函数在删除动态派生类对象的调用情况:
    #include <iostream>
    using namespace std;
    
    class A
    {
    public:
    	~A()
    	{
    		cout << "A::~A() is called.
    ";
    	}
    };
    
    class B : public A
    {
    public:
    	~B()
    	{
    		cout << "B::~B() is called.
    ";
    	}
    };
    
    void main()
    {
    	A *Ap = new B;
    	B *Bp2 = new B;
    	cout << "delete first object:
    ";
    	delete Ap;
    	cout << "delete second object:
    ";
    	delete Bp2;
    	system("pause");
    }
    

    输出结果:

    delete first object:
    A::~A() is called.
    delete second object:
    B::~B() is called.
    A::~A() is called.
    请按任意键继续. . .

    虚析构函数在删除动态派生类对象的调用情况:

    #include <iostream>
    using namespace std;
    
    class A
    {
    public:
    	~A()
    	{
    		cout << "A::~A() is called.
    ";
    	}
    };
    
    class B : public A
    {
    public:
    	~B()
    	{
    		cout << "B::~B() is called.
    ";
    	}
    };
    
    void main()
    {
    	A *Ap = new B;
    	B *Bp2 = new B;
    	cout << "delete first object:
    ";
    	delete Ap;
    	cout << "delete second object:
    ";
    	delete Bp2;
    	system("pause");
    }
    

    输出结果:

    delete first object:
    B::~B() is called.
    A::~A() is called.
    delete second object:
    B::~B() is called.
    A::~A() is called.
    请按任意键继续. . .

    • 定义了基类虚析构函数,基类指针指向的派生类动态对象也可以正确地用delete析构
    • 设计类层次结构时,提供一个虚析构函数,能够使派生类对象在不同状态下正确调用析构函数

    纯虚函数和抽象类

    • 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本
    • 纯虚函数为各派生类提供一个公共界面
    • 纯虚函数说明形式:virtual 类型 函数名(参数表)= 0 ;
    • 一个具有纯虚函数的基类称为抽象类
    class point
    {
        /*……*/
    };
    
    class shape;// 抽象类
    {
        point center;
        ···
    public:
      point where()
    {
        return center;
    }
      void move(point p )
    {
        enter = p;
        draw ();
    }
      virtual void rotate(int) = 0;// 纯虚函数
      virtual void draw () = 0;// 纯虚函数
    };
    ···
    shape x;// error,抽象类不能建立对象
    shape *p;// ok,可以声明抽象类的指针
    shape f();// error, 抽象类不能作为返回类型
    void g(shape);// error, 抽象类不能作为参数类型
    shape & h(shape &);// ok,可以声明抽象类的引用
    

    简单图形类例子:

    #include <iostream>
    using namespace std;
    
    class figure
    {
    protected:
    	double x, y;
    public:
    	void set_dim(double i, double j = 0)
    	{
    		x = i;
    		y = j;
    	}
    	virtual void show_area() = 0;
    };
    
    class triangle : public figure
    {
    public:
    	void show_area()
    	{
    		cout << "Triangle with high " << x << " and base " << y << " has an area of " << x * 0.5 * y << "
    ";
    	}
    };
    
    class square : public figure
    {
    public:
    	void show_area()
    	{
    		cout << "Square with dimension " << x << "*" << y << " has an area of " << x * y << "
    ";
    	}
    };
    
    class circle : public figure
    {
    public:
    	void show_area()
    	{
    		cout << "Circle with radius " << x;
    		cout << " has an area of " << 3.14 * x * x << "
    ";
    	}
    };
    
    void main()
    {
    	triangle t;	//派生类对象
    	square s;
    	circle c;
    	t.set_dim(10.0, 5.0);
    	t.show_area();
    	s.set_dim(10.0, 5.0);
    	s.show_area();
    	c.set_dim(9.0);
    	c.show_area();
    	system("pause");
    }
    

    输出结果:

    Triangle with high 10 and base 5 has an area of 25
    Square with dimension 10*5 has an area of 50
    Circle with radius 9 has an area of 254.34
    请按任意键继续. . .

    void main()
    {
    	figure *p;	// 声明抽象类指针
    	triangle t;
    	square s;
    	circle c;
    	p = &t;
    	p->set_dim(10.0, 5.0); 	// triangle::set_dim()
    	p->show_area();
    	p = &s;
    	p->set_dim(10.0, 5.0);	// square::set_dim()
    	p->show_area();
    	p = &c;
    	p->set_dim(9.0);		// circle::set_dim()
    	p->show_area();
    	system("pause");
    }
    

    输出结果为:

    Triangle with high 10 and base 5 has an area of 25
    Square with dimension 10*5 has an area of 50
    Circle with radius 9 has an area of 254.34
    请按任意键继续. . .

    使用抽象类引用:

    #include <iostream>
    using namespace std;
    
    class Number
    {
    public:
    	Number(int i)
    	{
    		val = i;
    	}
    	virtual void Show() = 0;
    protected:
    	int val;
    };
    class Hex_type : public Number
    {
    public:
    	Hex_type(int i) : Number(i)
    	{
    	}
    	void Show()
    	{
    		cout << "Hexadecimal:" << hex << val << endl;
    	}
    };
    class Dec_type : public Number
    {
    public:
    	Dec_type(int i) : Number(i)
    	{
    	}
    	void Show()
    	{
    		cout << "Decimal: " << dec << val << endl;
    	}
    };
    class Oct_type : public Number
    {
    public:
    	Oct_type(int i) : Number(i)
    	{
    	}
    	void Show()
    	{
    		cout << "Octal: " << oct << val << endl;
    	}
    };
    void fun(Number & n)// 抽象类的引用参数
    {
    	n.Show();
    }
    void main()
    {
    	Dec_type n1(50);
    	fun(n1);	// Dec_type::Show()
    	Hex_type n2(50);
    	fun(n2);	// Hex_type::Show()
    	Oct_type n3(50);
    	fun(n3);	// Oct_type::Show()
    	system("pause");
    }
    

    输出结果为:

    Decimal: 50
    Hexadecimal:32
    Octal: 62
    请按任意键继续. . .

    虚函数与多态的应用

    • 虚函数和多态性使成员函数根据调用对象的类型产生不同的动作
    • 多态性特别适合于实现分层结构的软件系统,便于对问题抽象时定义共性,实现时定义区别

    一个实例

    计算雇员工资
    Employee:

    class Employee
    {
    public:
    	Employee(const long, const char*);
    	virtual ~Employee();//虚析构函数
    	const char * getName() const;
    	const long getNumber() const;
    	virtual double earnings() const = 0;//纯虚函数,计算月薪
    	virtual void print() const;//虚函数,输出编号、姓名
    protected:
    	long number;//编号
    	char * name;//姓名
    };
    

    Manager:

    class Manager : public Employee
    {
    public:
    	Manager(const long, const char *, double = 0.0);
    	~Manager() { }
    	void setMonthlySalary(double);//置月薪
    	virtual double earnings() const;//计算管理人员月薪
    	virtual void print() const;//输出管理人员信息
    private:
    	double monthlySalary;//私有数据,月薪
    };
    

    HourlyWorker:

    class HourlyWorker : public Employee
    {
    public:
    	HourlyWorker(const long, const char *, double = 0.0, int = 0);
    	~HourlyWorker(){}
    	void setWage(double);//置时薪
    	void setHours(int);//置工时
    	virtual double earnings() const;//计算计时工月薪
    	virtual void print() const;//输出计时工月薪
    private:
    	double wage;//时薪
    	double hours;//工时
    };
    

    PieceWorker:

    class PieceWorker : public Employee
    {
    public:
    	PieceWorker(const long, const char *, double = 0.0, int = 0);
    	~PieceWorker() { }
    	void setWage(double);//置每件工件薪金
    	void setQuantity(int);//置工件数
    	virtual double earnings() const;//计算计件薪金
    	virtual void print() const;//输出计件薪金
    private:
    	double wagePerPiece;//每件工件薪金
    	int quantity;//工件数
    };
    

    测试用例:

    void test()
    {
    	cout << setiosflags(ios::fixed | ios::showpoint) << setprecision(2);
    	Manager m1(10135, "Cheng ShaoHua", 1200);
    	Manager m2(10201, "Yan HaiFeng");
    	m2.setMonthlySalary(5300);
    	HourlyWorker hw1(30712, "Zhao XiaoMing", 5, 8 * 20);
    	HourlyWorker hw2(30649, "Gao DongSheng");
    	hw2.setWage(4.5);
    	hw2.setHours(10 * 30);
    	PieceWorker pw1(20382, "Xiu LiWei", 0.5, 2850);
    	PieceWorker pw2(20496, "Huang DongLin");
    	pw2.setWage(0.75);
    	pw2.setQuantity(1850);
    	// 使用抽象类指针,调用派生类版本的函数
    	Employee *basePtr;
    
    	basePtr = &m1;
    	basePtr->print();
    
    	basePtr = &m2;
    	basePtr->print();
    
    	basePtr = &hw1;
    	basePtr->print();
    
    	basePtr = &hw2;
    	basePtr->print();
    
    	basePtr = &pw1;
    	basePtr->print();
    
    	basePtr = &pw2;
    	basePtr->print();
    
    	system("pause");
    }
    

    异质链表

    程序中,用基类类型指针,可以生成一个连接不同派生类对象的动态链表,即每个结点指针可以指向类层次中不同的派生类对象。这种结点类型不相同链表称为异质链表
    e.g.

    class Employee
    {
    public:
    	Employee(const long, const char*);
    	virtual ~Employee();
    	const char * getName() const;
    	const long getNumber() const;
    	virtual double earnings() const = 0;
    	virtual void print() const;
    	Employee *next;// 增加一个指针成员
    protected:
    	long number;
    	char * name;
    };
    
    void AddFront(Employee * &h, Employee * &t)//在表头插入结点
    {
    	t->next = h;
    	h = t;
    }
    void test()//测试函数
    {
    	Employee * empHead = NULL, *ptr;
    	ptr = new Manager(10135, "Cheng ShaoHua", 1200);//建立第一个结点
    	AddFront(empHead, ptr);//插入表头
    	ptr = new HourlyWorker(30712, "Zhao XiaoMing", 5, 8 * 20);//建立第二个结点
    	AddFront(empHead, ptr);//插入表头
    	ptr = new PieceWorker(20382, "Xiu LiWei", 0.5, 2850);//建立第三个结点
    	AddFront(empHead, ptr);//插入表头
    	ptr = empHead;
    	while (ptr)
    	{
    		ptr->print();
    		ptr = ptr->next;
    	}//遍历链表,输出全部信息
    	ptr = empHead;
    	while (ptr)//遍历链表,输出姓名和工资
    	{
    		cout << ptr->getName() << "  " << ptr->earnings() << endl;
    		ptr = ptr->next;
    	}
    }
    

    多态

    多态的理论基础

    • 联编是指一个程序模块、代码之间互相关联的过程。
    • 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
    • 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch 语句和 if 语句是动态联编的例子。
    • 理论联系实际
    1. C++与C相同,是静态编译型语言
    2. 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
    3. 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
      从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

    多态的c++实现

    virtual关键字,告诉编译器这个函数要支持多态;不要根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用。冠以virtual关键字的函数叫虚函数;虚函数分为两类:一般虚函数、纯虚函数。

    多态的实现效果

    多态:同样的调用语句有多种不同的表现形态

    多态实现的三个条件

    有继承、有重写、有父类指针(引用)指向子类对象

    重写与重载

    函数重载

    • 必须在同一个类中进行
    • 子类无法重载父类的函数,父类同名函数将被名称覆盖
    • 重载是在编译期间根据参数类型和个数决定函数调用

    函数重写

    • 必须发生于父类与子类之间
    • 并且父类与子类中的函数必须有完全相同的原型
    • 使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
    • 多态是在运行期间根据具体对象的类型决定函数调用

    编译器是如何实现多态的

    1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表
    2. 虚函数表是一个存储类成员函数指针的数据结构
    3. 虚函数表是由编译器自动生成与维护的
    4. virtual成员函数会被编译器放入虚函数表中
    5. 存在虚函数时,每个对象中都有一个指向虚函数表的指针
    6. VPTR一般作为类对象的第一个成员
    7. 虚函数表指针是在构造函数执行之前被赋值,还是在构造函数被赋值之后被赋

    总结

    • 虚函数和多态性使软件设计易于扩充。
    • 派生类重载基类接口相同的虚函数其虚特性不变。
    • 如果代码关联在编译时确定,称为静态联编。代码在运行时关联称为动态联编。
    • 基类指针可以指向派生类对象、基类中拥有虚函数,是支持多态性的前提。
    • 虚析构函数可以正确释放动态派生类对象的资源。
    • 纯虚函数由派生类定义实现版本。
    • 具有纯虚函数的类称为抽象类。抽象类只能作为基类,不能建立对象。抽象类指针使得派生的具体类对象具有多态操作能力。
  • 相关阅读:
    数据结构(四十)平衡二叉树(AVL树)
    数据结构(三十九)二叉排序树
    数据结构(三十八)静态查找表(顺序查找、二分查找、插值查找、斐波那契查找、线性索引查找)
    数据结构(三十七)查找的基本概念
    数据结构(三十六)关键路径
    数据结构(三十五)拓扑排序
    数据结构(三十四)最短路径(Dijkstra、Floyd)
    数据结构(三十三)最小生成树(Prim、Kruskal)
    字符串匹配算法之KMP
    最长公共子序列(Longest common subsequence)
  • 原文地址:https://www.cnblogs.com/cj5785/p/10664723.html
Copyright © 2011-2022 走看看