多态作为面向对象的重要概念,在如何一门面向对象编程语言中都有着举足轻重的作用,学习多态,有助于更好地理多态的行为

多态性(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 语句是动态联编的例子。
  • 理论联系实际
  1. C++与C相同,是静态编译型语言
  2. 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
  3. 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象

    从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

多态的c++实现

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

多态的实现效果

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

多态实现的三个条件

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

重写与重载

函数重载

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

函数重写

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

编译器是如何实现多态的

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

总结

  • 虚函数和多态性使软件设计易于扩充。
  • 派生类重载基类接口相同的虚函数其虚特性不变。
  • 如果代码关联在编译时确定,称为静态联编。代码在运行时关联称为动态联编。
  • 基类指针可以指向派生类对象、基类中拥有虚函数,是支持多态性的前提。
  • 虚析构函数可以正确释放动态派生类对象的资源。
  • 纯虚函数由派生类定义实现版本。
  • 具有纯虚函数的类称为抽象类。抽象类只能作为基类,不能建立对象。抽象类指针使得派生的具体类对象具有多态操作能力。

C++学习笔记-多态的更多相关文章

  1. java学习笔记 --- 多态

    一.多态 (1)定义:同一个对象在不同时刻体现出来的不同状态.父类的引用或者接口的引用指向了自己的子类对象.   Dog d = new Dog();//Dog对象的类型是Dog类型.  Animal ...

  2. C# 学习笔记 多态(二)抽象类

    多态是类的三大特性之一,抽象类又是多态的实现方法之一.抽象类是什么呢,如果把虚方法比作一个盛有纯净水的杯子,那么此时的“纯净水”就是事先定义好的方法,我们可以根据不同的需求来改变杯子中所事先盛放的是“ ...

  3. C# 学习笔记 多态(一)虚方法

    在面对对象编程中,类的三大特性分别为封装,继承,多态.其中多态的具体实现,依赖于三个方法,也就是虚方法,抽象类和接口. 多态的具体作用是什么呢?或者说多态的存在有什么意义呢?多态的存在有效的降低了程序 ...

  4. C++学习笔记-多态的实现原理

    深入了解多态的实现原理,有助于提高对于多态的认识 多态基础 多态的实现效果 多态:同样的调用语句有多种不同的表现形态 多态实现的三个条件 有继承.有virtual重写.有父类指针(引用)指向子类对象 ...

  5. SQL反模式学习笔记7 多态关联

    目标:引用多个父表 反模式:使用多用途外键.这种设计也叫做多态关联,或者杂乱关联. 多态关联和EAV有着相似的特征:元数据对象的名字是存储在字符串中的. 在多态关联中,父表的名字是存储在Issue_T ...

  6. C++ 学习笔记 (七)继承与多态 virtual关键字的使用场景

    在上一篇 C++ 学习笔记 (六) 继承- 子类与父类有同名函数,变量 中说了当父类子类有同名函数时在外部调用时如果不加父类名则会默认调用子类的函数.C++有函数重写的功能需要添加virtual关键字 ...

  7. 《Java编程思想》学习笔记(二)——类加载及执行顺序

    <Java编程思想>学习笔记(二)--类加载及执行顺序 (这是很久之前写的,保存在印象笔记上,今天写在博客上.) 今天看Java编程思想,看到这样一道代码 //: OrderOfIniti ...

  8. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

  9. 两千行PHP学习笔记

    亲们,如约而至的PHP笔记来啦~绝对干货! 以下为我以前学PHP时做的笔记,时不时的也会添加一些基础知识点进去,有时还翻出来查查. MySQL笔记:一千行MySQL学习笔记http://www.cnb ...

随机推荐

  1. (转发)Android 源码获取-----在Windows环境下通过Git得到Android源代码

    在学习Android的过程中,深入其源代码研究对我们来说是非常重要的,这里将介绍如何通过在Windows环境下使用Git来得到我们的Android源代码. 1.首先确保你电脑上安装了Git,这个通过  ...

  2. left join和right join和inner join

    此图仅限于理解他们之间的关系,下面还有举例,例子更好明白. left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录  right join(右联接) 返回包括右表中的所有记录 ...

  3. cogs908. 校园网

    908. 校园网 ★★   输入文件:schlnet.in   输出文件:schlnet.out   简单对比时间限制:1 s   内存限制:128 MB USACO/schlnet(译 by Fel ...

  4. 2019牛客暑期多校训练营(第二场)D bitset

    题意 给一个n个结点的带点权的图,找到第k小的团的权值 分析 用bitset表示团的状态,一个结点必须和团里的每个结点都连边才能加进去,所以可以直接用\(\&\)运算来判断一个结点是否能加进去 ...

  5. AcWing:172. 立体推箱子(bfs)

    立体推箱子是一个风靡世界的小游戏. 游戏地图是一个N行M列的矩阵,每个位置可能是硬地(用”.”表示).易碎地面(用”E”表示).禁地(用”#”表示).起点(用”X”表示)或终点(用”O”表示). 你的 ...

  6. 学习日记6、easyui datagrid 新增一行,编辑行,结束编辑和删除行操作记录

    1.新增一行并进入编辑状态 var index=$('#Numbers').datagrid('appendRow', { CardInformation: '开户行', CardNumber: '银 ...

  7. JavaWeb_(Struts2框架)使用Struts框架实现用户的登陆

    JavaWeb_(Struts2框架)使用Servlet实现用户的登陆 传送门 JavaWeb_(Struts2框架)Servlet与Struts区别 传送门 MySQL数据库中存在Gary用户,密码 ...

  8. flask 第五篇

    需求: 1. 用户名: oldboy 密码: oldboy123 2. 用户登录成功之后跳转到列表页面 3. 失败有消息提示,重新登录 4.点击学生名称之后,可以看到学生的详细信息 后端: from ...

  9. SpringMVC工作原理的介绍

    1.用户向服务器发送请求,请求被Spring前端控制Servlet DispatcherServlet捕获 2.DispatcherServlet对请求URL进行解析,得到请求资源标识符(URL).然 ...

  10. koa 应用生成器

    通过应用 koa 脚手架生成工具 可以快速创建一个基于 koa2 的应用的骨架 1.全局安装 npm install koa-generator -g 2.创建项目 koa koa_demo 3.安装 ...