多态作为面向对象的重要概念,在如何一门面向对象编程语言中都有着举足轻重的作用,学习多态,有助于更好地理多态的行为
多态性(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 << "\n"; } 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 << "\n"; } 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 << "\n"; } 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 << "\n"; } 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 << "\n"; } 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 << "\n"; } 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.\n"; } }; class B : public A { public: ~B() { cout << "B::~B() is called.\n"; } }; void main() { A *Ap = new B; B *Bp2 = new B; cout << "delete first object:\n"; delete Ap; cout << "delete second object:\n"; 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.\n"; } }; class B : public A { public: ~B() { cout << "B::~B() is called.\n"; } }; void main() { A *Ap = new B; B *Bp2 = new B; cout << "delete first object:\n"; delete Ap; cout << "delete second object:\n"; 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 << "\n"; } }; class square : public figure { public: void show_area() { cout << "Square with dimension " << x << "*" << y << " has an area of " << x * y << "\n"; } }; class circle : public figure { public: void show_area() { cout << "Circle with radius " << x; cout << " has an area of " << 3.14 * x * x << "\n"; } }; 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 语句是动态联编的例子。
- 理论联系实际
- C++与C相同,是静态编译型语言
- 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
- 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。
多态的c++实现
virtual关键字,告诉编译器这个函数要支持多态;不要根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用。冠以virtual关键字的函数叫虚函数;虚函数分为两类:一般虚函数、纯虚函数。
多态的实现效果
多态:同样的调用语句有多种不同的表现形态
多态实现的三个条件
有继承、有重写、有父类指针(引用)指向子类对象
重写与重载
函数重载
- 必须在同一个类中进行
- 子类无法重载父类的函数,父类同名函数将被名称覆盖
- 重载是在编译期间根据参数类型和个数决定函数调用
函数重写
- 必须发生于父类与子类之间
- 并且父类与子类中的函数必须有完全相同的原型
- 使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
- 多态是在运行期间根据具体对象的类型决定函数调用
编译器是如何实现多态的
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储类成员函数指针的数据结构
- 虚函数表是由编译器自动生成与维护的
- virtual成员函数会被编译器放入虚函数表中
- 存在虚函数时,每个对象中都有一个指向虚函数表的指针
- VPTR一般作为类对象的第一个成员
- 虚函数表指针是在构造函数执行之前被赋值,还是在构造函数被赋值之后被赋
总结
- 虚函数和多态性使软件设计易于扩充。
- 派生类重载基类接口相同的虚函数其虚特性不变。
- 如果代码关联在编译时确定,称为静态联编。代码在运行时关联称为动态联编。
- 基类指针可以指向派生类对象、基类中拥有虚函数,是支持多态性的前提。
- 虚析构函数可以正确释放动态派生类对象的资源。
- 纯虚函数由派生类定义实现版本。
- 具有纯虚函数的类称为抽象类。抽象类只能作为基类,不能建立对象。抽象类指针使得派生的具体类对象具有多态操作能力。
来源:https://www.cnblogs.com/cj5785/p/10664722.html