本文结合黑马程序员、C语言中文网以及《C++ Primer》对多态进行了总结

多态的基本概念

多态是C++面向对象三大特性之一。

多态分为两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态

静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址

成员函数、继承与虚函数(virtual)

在C++语言中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数(virtual)。当我们使用指针或者引用调用虚函数时,该调用将被动态绑定。根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。

借用C语言中文网的一个例子,要看懂这个例子,我们首先应该明白一个前提:基类指针可以指向一个派生类对象,但派生类指针不能指向基类对象。基类的指针指向派生类的对象,指向的是派生类中基类的部分。所以只能操作派生类中从基类中继承过来的数据和基类自身的数据。

//基类People
class People{
public:People(char *name, int age);void display();
protected:char *m_name;int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}
//派生类Teacher
class Teacher: public People{
public:Teacher(char *name, int age, int salary);void display();
private:int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){People *p = new People("王志刚", 23);p -> display();p = new Teacher("赵宏佳", 45, 8200);p -> display();return 0;
}

运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。

导致错误输出的原因是,调用函数 display() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 ——函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 display() 函数在程序编译期间就已经设置好了。

当在display()前加上virtual关键字后,就可以避免这个错误。因为此时,编译器看的是指针的内容,而不是它的类型。

虚函数的注意事项

1.只需要在虚函数的声明处加上virtual关键字,函数定义处可以加也可以不加

2.virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。

3.派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本

4.构造函数不能是虚函数(

从vptr角度解释

虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针(vptr可以参考C++的虚函数表指针vptr)指向,该指针存放在对象的内部空间中,需要调用构造函数完成初始化。如果构造函数是虚函数,那么调用构造函数就需要去找vptr,但此时vptr还没有初始化!

虚析构函数的必要性

//基类
class Base{
public:Base();~Base();
protected:char *str;
};
Base::Base(){str = new char[100];cout<<"Base constructor"<<endl;
}
Base::~Base(){delete[] str;cout<<"Base destructor"<<endl;
}
//派生类
class Derived: public Base{
public:Derived();~Derived();
private:char *name;
};
Derived::Derived(){name = new char[100];cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){delete[] name;cout<<"Derived destructor"<<endl;
}
int main(){Base *pb = new Derived();delete pb;cout<<"-------------------"<<endl;Derived *pd = new Derived();delete pd;return 0;
}

运行结果:
Base constructor
Derived constructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor

发现在delete指向派生类的父类指针时没有释放派生类对象占用的内存,造成了内存泄漏。因为这里的析构函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数,pb是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。

更改代码

class Base{
public:Base();virtual ~Base();
protected:char *str;
};

运行结果:
Base constructor
Derived constructor
Derived destructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor

将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。编译器根据指针的指向来选择函数。在实际开发中,应该将基类的析构函数声明为虚函数。

纯虚函数和抽象类

纯虚函数声明语法:  virtual 返回值类型 函数名 (函数参数) = 0;

纯虚函数没有函数体,只有函数声明。包含纯虚函数的类称为抽象类,它无法实例化,也无法创建对象。抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

#include <iostream>
using namespace std;//线
class Line{
public:Line(float len);virtual float area() = 0;virtual float volume() = 0;
protected:float m_len;
};
Line::Line(float len): m_len(len){ }//矩形
class Rec: public Line{
public:Rec(float len, float width);float area();
protected:float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }//长方体
class Cuboid: public Rec{
public:Cuboid(float len, float width, float height);float area();float volume();
protected:float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){ }
float Cuboid::area(){ return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height); }
float Cuboid::volume(){ return m_len * m_width * m_height; }//正方体
class Cube: public Cuboid{
public:Cube(float len);float area();float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){ }
float Cube::area(){ return 6 * m_len * m_len; }
float Cube::volume(){ return m_len * m_len * m_len; }int main(){Line *p = new Cuboid(10, 20, 30);cout<<"The area of Cuboid is "<<p->area()<<endl;cout<<"The volume of Cuboid is "<<p->volume()<<endl;p = new Cube(15);cout<<"The area of Cube is "<<p->area()<<endl;cout<<"The volume of Cube is "<<p->volume()<<endl;return 0;
}

继承关系为:Line --> Rec --> Cuboid --> Cube,一直到Cuboid实现了所有的纯虚函数,才是一个完整的类,才可以被实例化。

还有一个细节:指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数。

在刚刚学习纯虚函数的时候我有一个疑问:既然在基类中无法实现纯虚函数,那为何要在基类中声明它呢?通过查阅网上的资料,我做了一个总结。

在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,"你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它"。个人感觉纯虚函数还可以防止不应被实例化的基类被示例化为对象,可以提高安全性。

虚函数表

前面介绍了虚函数,而编译器之所以可以找到虚函数,是因为在创建对象时额外地增加了虚函数表。如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。

//People类
class People{
public:People(string name, int age);
public:virtual void display();virtual void eating();
protected:string m_name;int m_age;
};
People::People(string name, int age): m_name(name), m_age(age){ }
void People::display(){cout<<"Class People:"<<m_name<<"今年"<<m_age<<"岁了。"<<endl;
}
void People::eating(){cout<<"Class People:我正在吃饭,请不要跟我说话..."<<endl;
}
//Student类
class Student: public People{
public:Student(string name, int age, float score);
public:virtual void display();virtual void examing();
protected:float m_score;
};
Student::Student(string name, int age, float score):People(name, age), m_score(score){ }
void Student::display(){cout<<"Class Student:"<<m_name<<"今年"<<m_age<<"岁了,考了"<<m_score<<"分。"<<endl;
}
void Student::examing(){cout<<"Class Student:"<<m_name<<"正在考试,请不要打扰T啊!"<<endl;
}
//Senior类
class Senior: public Student{
public:Senior(string name, int age, float score, bool hasJob);
public:virtual void display();virtual void partying();
private:bool m_hasJob;
};
Senior::Senior(string name, int age, float score, bool hasJob):Student(name, age, score), m_hasJob(hasJob){ }
void Senior::display(){if(m_hasJob){cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,并且顺利找到了工作,Ta今年"<<m_age<<"岁。"<<endl;}else{cout<<"Class Senior:"<<m_name<<"以"<<m_score<<"的成绩从大学毕业了,不过找工作不顺利,Ta今年"<<m_age<<"岁。"<<endl;}
}
void Senior::partying(){cout<<"Class Senior:快毕业了,大家都在吃散伙饭..."<<endl;
}
int main(){People *p = new People("赵红", 29);p -> display();p = new Student("王刚", 16, 84.5);p -> display();p = new Senior("李智", 22, 92.0, true);p -> display();return 0;
}

在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。

基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。

p -> display(),在编译器内部会发生类似下面的转换( *( *(p+0) + 0 ) )(p),( *(p+0) + 0 )也就是 display() 的地址,( *( *(p+0) + 0 ) )(p)也就是对 display() 的调用,这里的 p 就是传递的实参,它会赋值给 this 指针。

C++ 多态(动态多态)相关推荐

  1. 【C++ 语言】面向对象 ( 继承 | 重写 | 子类调用父类方法 | 静态多态 | 动态多态 | 虚函数 | 纯虚函数 )

    文章目录 类的继承 方法的重写 子类中调用父类方法 多态 虚函数 虚函数示例 纯虚函数 相关代码 类的继承 1. 继承表示 : C++ 中继承可以使用 ":" 符号 , 格式为 & ...

  2. C++_类和对象_C++多态_多态的基本语法_静态多态_动态多态_虚函数---C++语言工作笔记069

    然后我们再来看看C++中的多态,这里还要注意一点,就是在C++中是可以用多继承的, 但是java.不行,只能实现多个接口,不能继承多个类.这让c++会更加灵活一点. 可以看到,上面说了,在c++中,分 ...

  3. C++静态多态与动态多态

    静态多态(编译期/早绑定) 函数重载 class A {public:void do(int a);void do(int a, int b); }; 动态多态(运行期期/晚绑定) •虚函数:用 vi ...

  4. C++知识点 多态、静态多态、动态多态

    多态 有了继承才有多态的概念,首先说一下继承. 继承的概念是派生类可以调用基类的成员.常举的例子,动物是基类,它拥有所有动物共有的一些特征和方法.它会衍生出猫的类,狗的类,派生出的类除了有动物公共的特 ...

  5. C++多态——静态多态与动态多态

    多态 : 顾名思义,多态就是多种形态,也就是对不同对象发送同一个消息,不同对象会做出不同的响应. 并且多态分为静态多态和动态多态. 静态多态就是在系统编译期间就可以确定程序执行到这里将要执行哪个函数, ...

  6. 动态多态和静态多态(C++)

    多态:即一个接口多种实现方式:C++中的多态分为两种:静态多态与动态多态 一.静态多态:也成为编译时的多态:在编译时期就已经确定要执行了的函数地址了:主要有函数重载和函数模板(这里只写函数重载) (1 ...

  7. 【C++】动态多态与虚函数

    文章目录 一.动态多态概念 二.虚函数 2.1 虚函数的概念 2.2 虚函数的特点 三.动态多态实现的背后编译器帮我做了什么? 四.虚析构 4.1 解决多态中资源释放的问题 4.2 解决多重继承中的资 ...

  8. java多态口诀,Java之路---Day12(多态),多态Java

    Java之路---Day12(多态),多态Java 2019-10-26-22:40:09 目录: 1.多态的概念 2.多态的分类 3.实现多态的三个必要条件 4.多态的格式 5.多态成员变量的使用特 ...

  9. php父类继承子类_php有多态吗 多态都有什么好处?理解好多态往往能事半功倍...

    一.什么是多态? 多态其实就是根据参数不同而执行不同的方法. 二.php中的多态 多态的概念一般是强类型语言来谈的,因为强类型语言它必须要声明参数类型,比如一个手电筒对象的打开方法其参数申明了只能是蓝 ...

最新文章

  1. 【机器学习入门】(7) 线性回归算法:原理、公式推导、损失函数、似然函数、梯度下降
  2. ABP理论学习之通知系统
  3. java 复制一个数组_浅谈Java中复制数组的方式
  4. sts在文件夹中显示_工作中很多问题,巧用Windows 10任务栏,几分钟快速解决
  5. Ajax 实现无刷新分页
  6. 奖金(拓补排序的应用)
  7. DataSet之间的赋值
  8. JAVA环境变量配置方法(Windows)
  9. 人工智能6.1 -- 机器学习算法篇(一)数据清洗、回归(含实践)
  10. 【毕业论文】分享当年使用过的一些好用网站,包括论文去重,翻译,作图神器,免费文献查找 | 第 1 期
  11. win10下SVN图标不显示
  12. python 入门学习,52周存钱挑战
  13. 人像优化效果Lr预设
  14. Netbeans使用问题整理
  15. 从国际象棋与象棋的走法差异,再趣说IT人提升能力和增收方式
  16. linux任务调度crontab和at
  17. OSChina 周一乱弹 ——女人比代码复杂多了,搞不懂!
  18. Java程序验证五子棋先手必胜,五子棋怎样下最厉害_五子棋先手必胜开局图
  19. Python连接postgresql数据库入门
  20. 基于关系数据库彩票投注模拟系统设计与实现

热门文章

  1. java 图片相似搜索_JAVA比较两张图片相似度的方法
  2. Oracle OCP学习——Catalog的配置与使用
  3. opencv建立数学坐标系绘制函数曲线
  4. 利用浏览器缓存抓取网络资源:【炉石传说】所有卡牌png图片地址
  5. 线性子空间模型 linear subspace model
  6. MicroC实现Expr部分和for循环,无续算版本Comp.fs展示
  7. WPS如何快速输入随机姓名
  8. 测试小故事52:怎么测?
  9. 单总线(1-Wire BUS)通讯协议
  10. TCP粘包以及粘包处理