1.多态

在C++中由两种多态性:

• 编译时的多态性:通过函数的重载和运算符的重载来实现的

• 运行时的多态性:通过类继承关系和虚函数来实现的

特别注意:

a.运行时的多态性是指程序执行前,无法根据函数名和函数的参数来确定调用哪一个函数,必须在程序执行的过程中,根据执行的具体情况来动态地确定。其目的是追求程序的通用性,建立一种通用的程序

b.运行时的多态,简而言之就是用父类型的指针或引用指向其子类的实例,然后通过父类的指针或引用调用实际子类的成员函数,从而使父类的指针或引用拥有“多种形态”。这是一种泛型技术(如:模版技术、RTTI技术),其目的是使用不变的代码来实现可变的算法

示例:

 #include<iostream>
using namespace std;
class Animal{ //基类
public:
virtual void eat(){ //虚函数
cout<<"Animal eat"<<endl;
}
virtual void sleep(){
cout<<"Animal sleep"<<endl;
}
}; class Person:public Animal{ //子类1
public:
void eat(){
cout<<"Person eat"<<endl;
}
void sleep(){
cout<<"Person sleep"<<endl;
}
}; class Dog:public Animal{ //子类2
public:
void eat(){
cout<<"Dog eat"<<endl;
}
void sleep(){
cout<<"Dog sleep"<<endl;
}
}; class Bird:public Animal{ //子类3
public:
void eat(){
cout<<"Bird eat"<<endl;
}
void sleep(){
cout<<"Bird sleep"<<endl;
}
}; void func(Animal &a){ //函数,注意参数是基类的引用
a.eat();
a.sleep();
} int main(){
Person p; //Person类的对象实例
Dog d; //Dog类的对象实例
Bird b; //Bird类的对象实例
func(p);
cout<<"-----------分界线---------------"<<endl;
func(d);
cout<<"-----------分界线---------------"<<endl;
func(b);
return ;
}

2.虚函数

2.1 虚函数的定义

虚函数是一个类的成员函数,它的定义语法如下:

语法:virtual 返回值类型 函数名(参数表);

特别注意:

a.当一个类的一个成员函数被定义为虚函数时,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征

b.当在派生类中重新定义虚函数时,不必加关键字virtual。但重新定义时不仅要求函数同名,而且要求函数的参数列表与返回值类型也必须和基类中的虚函数相同,否则编译器会报错

c.虚函数可以在先在类内进行声明,然后在类外定义。但在类内声明时需要在返回值类型之前加上关键字virtual,在类外定义时则不需要在添加关键字virtual

2.2 虚函数使用的注意事项

1.派生类中重定义虚函数时,虚函数的函数名必须与其基类中的虚函数的函数名相同,除此之外要求参数列表和函数的返回值类型也必须相同

[特例]:当基类中的虚函数的返回值类型是基类类型的指针时,允许在派生类中重定义该虚函数时将返回值类型改写为派生类类型的指针

 #include<iostream>
using namespace std;
class Animal{
public:
int value;
Animal():value(){}
Animal(int v):value(v){}
virtual Animal* show(){ //返回值类型是Animal类型的指针
cout<<"Animal类中的value值是:"<<value<<endl;
return this;
}
}; class Person:public Animal{
public:
int value;
Person():value(){}
Person(int v):value(v){}
Person* show(){ //返回值类型是Person类型的指针
cout<<"Person类中的value值是:"<<value<<endl;
return this;
}
}; int main(){
Animal animal();
Person person();
animal.show();
cout<<"------------分界线----------------"<<endl;
person.show();
return ;
}

2.只有类中的成员函数才有资格成为虚函数。这是因为虚函数仅适用于有继承关系的类对象(建议成员函数尽可能地设置为虚函数)

3.类中的静态成员函数是该类所有对象所共有的,不受限于某个对象个体,因此不能作为虚函数

4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的

5.类中的析构函数可以作为虚函数,但构造函数不能作为虚函数。这是因为在调用构造函数时对象还没有完成实例化,而调用析构函数时对象已经完成了实例化

[注]:在基类中及其派生类中都动态分配内存空间时,必须把析构函数定义为虚函数,从而实现“销毁”对象时的多态性。例如在C++中用new运算符建立临时对象时,若基类中有析构函数并且同时定义了一个指向该基类的指针变量,但指针变量指向的对象却是该基类的派生类对象,那么在程序执行delete操作时,系统只会执行基类中的析构函数,而不会执行派生类中的析构函数,从而造成内存泄漏

 #include<iostream>
using namespace std;
class Father{
public:
Father()=default;
~Father(){
cout<<"调用Father类的析构函数"<<endl;
}
};
class Son:public Father{
public:
Son()=default;
~Son(){
cout<<"调用Son类的析构函数"<<endl;
}
}; int main(){
Father* ptr=new Son;
delete ptr;
return ;
}

这是因为这里的指针本质上指向的其实是派生类对象中隐藏包含的基类子对象。将基类的析构函数定义为虚函数可以解决这个问题:

 #include<iostream>
using namespace std;
class Father{
public:
Father()=default;
virtual ~Father(){
cout<<"调用Father类的析构函数"<<endl;
}
};
class Son:public Father{
public:
Son()=default;
~Son(){
cout<<"调用Son类的析构函数"<<endl;
}
}; int main(){
Father* ptr=new Son;
delete ptr;
return ;
}

当基类中的析构函数为虚函数时,无论指针指向的是同一类族中的哪一个对象,系统都会采用动态关联,调用相应的析构函数,对该对象进行清理工作。因此最好将基类中的析构函数声明为虚函数,这将使该基类的所有派生类的析构函数自动成为虚函数

6.使用虚函数会使程序的运行速度降低,这是因为为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现,但程序的通用性会变得更高

7.如果虚函数的定义放在类外,virtual关键字只加在函数的声明的前面,不能再添加在函数定义的前面。正确的定义必须不包括关键字virtual

2.3 虚表(参考来源:http://www.xuebuyuan.com/1485876.html

2.3.1 什么是虚表

在C++中,虚函数是通过虚表(Virtual Table)来实现的虚表本质上是一个类的虚函数的地址表,它解决了继承、覆盖的问题,保证其容量真实反映实际的函数。而对虚表的利用,往往需要通过指向虚表的指针来实现在C++中,编译器必须保证虚表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量),这样我们便可以通过对象实例的地址得到虚表,然后利用指向虚表的指针遍历虚表中的函数指针,并调用相应的函数

示例:

 #include<iostream>
using namespace std;
class Father{
public:
virtual void show(){ //虚函数1
cout<<"调用Father类的成员方法show()"<<endl;
}
virtual void func(){ //虚函数2
cout<<"调用Father类的成员方法func()"<<endl;
}
void print(){ //普通成员函数
cout<<"调用Father类的成员方法print()"<<endl;
}
}; class Son:public Father{
public:
void show(){ //虚函数1
cout<<"调用Son类的成员方法show()"<<endl;
}
void func(){ //虚函数2
cout<<"调用Son类的成员方法func()"<<endl;
}
virtual void list(){ //虚函数3
cout<<"调用Son类的成员方法list()"<<endl;
}
}; int main(){
Father father;
father.show();
father.func();
father.print();
cout<<"-----------分界线------------"<<endl;
Son son;
son.show();
son.func();
son.print();
son.list();
return ;
}

图解说明:

当Father类创建对象father后,其内存分布大致如下:

Father类中的成员函数print()是普通成员函数,因此其不再虚表中。

当Son类创建对象son后,其内存分布大致如下:

2.3.2 虚表的四种情况

2.3.2.1 一般继承(无虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive没有重定义任何父类Base的虚函数,而是在其继承父类Base的基础上添加了三个新的虚函数,其代码主要如下(摘要):

 class Base{//基类
public:
virtual void f();
virtual void g();
virtual void h();
}; class Derive{ //子类
public:
virtual void f1();
virtual void g1();
virtual void h1();
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看出:

• 虚函数按照其声明顺序放于虚表中

• 父类的虚函数在子类的虚函数的前面

2.3.2.2 一般继承(由虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive重定义了部分父类Base的虚函数,其代码主要如下(摘要):

 class Base{//基类
public:
virtual void f();
virtual void g();
virtual void h();
}; class Derive{ //子类
public:
void f(); //重定义基类中的虚函数
virtual void g1();
virtual void h1();
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看出:

• 派生类中重定义的虚函数(如void f())会被放到虚表中原来父类虚函数的位置

• 没有被重定义的虚函数保持原样

[注]:正因如此,当程序执行语句:

 Base *b=new Derive();
b->f();

由于虚表中Base::f()的位置已经被Derive::f()函数地址所取代,因此指针b此时调用的函数f()是Derive::f(),而不是Base::f()

2.3.2.3 多重继承(无虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive没有重定义任何父类中的虚函数,而是在其继承所有父类的基础上添加了两个新的虚函数,其代码主要如下(摘要):

 class Base1{//基类1
public:
virtual void f();
virtual void g();
virtual void h();
}; class Base2{//基类2
public:
virtual void f();
virtual void g();
virtual void h();
}; class Base3{//基类3
public:
virtual void f();
virtual void g();
virtual void h();
}; class Derive{ //子类
public:
virtual void f1();
virtual void g1();
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看出:

• 每一个父类都有自己的虚表

• 子类中新增加的虚函数会被添加在第一个父类的虚表的后面(所谓的第一个父类时按照声明的顺序来判断的),这样做的目的是为了使不同父类类型的指针在指向同一个子类的实例时都能够调用到实际的函数

2.3.2.4 多重继承(由虚函数覆盖)

如上图所示,在这个继承关系中,子类Derive重定义了部分父类中的虚函数,其代码主要如下(摘要):

 class Base1{//基类1
public:
virtual void f();
virtual void g();
virtual void h();
}; class Base2{//基类2
public:
virtual void f();
virtual void g();
virtual void h();
}; class Base3{//基类3
public:
virtual void f();
virtual void g();
virtual void h();
}; class Derive{ //子类
public:
void f();
virtual void g1();
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:

从中我们可以看到:

• 派生类中重定义虚函数(如void f())时,其所有父类的虚表中的相应位置都会被替换

• 没有被重定义的虚函数保持原样

3.纯虚函数

3.1 什么时纯虚函数

纯虚函数(pure virtual function)是指被标明为不具体实现的虚拟成员函数。通常情况下,纯虚函数常用在这种情况:定义一个基类时,基类中虚函数的具体实现由于必须依赖派生类的具体情况从而无法在基类中确切定义,此时可以把这个虚函数定义为纯虚函数

语法:virtual 返回值类型 函数名(参数表)=0;

3.2 使用纯虚函数的注意事项

• 含有纯虚函数的基类是不能用来定义对象的。纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数的类时抽象类

• 定义纯虚函数时,不需要定义函数的实现部分(因为没有意义,即使定义了函数的实现部分,编译器也不会对这部分代码进行编译)

• “=0”表明程序员将不定义该函数,函数声明是为派生类保留一个位置。“=0”的本质是将指向函数体的指针定位NULL

• 派生类必须重定义基类中的所有纯虚函数,少一个都不行,否则派生类中由于仍包含纯虚函数(从基类中继承而来),系统会仍将该派生类当成一个抽象类而不允许其实例化对象

示例:

 #include<iostream>
using namespace std;
class Animal{ //基类,抽象类
public:
virtual void eat()=; //纯虚函数
virtual void sleep()=;
}; class Person:public Animal{ //子类1
public:
void eat(){
cout<<"Person eat"<<endl;
}
void sleep(){
cout<<"Person sleep"<<endl;
}
}; class Dog:public Animal{ //子类2
public:
void eat(){
cout<<"Dog eat"<<endl;
}
void sleep(){
cout<<"Dog eat"<<endl;
}
}; void func(Animal &a){
a.eat();
a.sleep();
}
int main(){
Person person;
func(person);
cout<<"------分界线-----------"<<endl;
Dog dog;
func(dog);
return ;
}

C++:多态浅析的更多相关文章

  1. C++中的重载,隐藏,覆盖,虚函数,多态浅析

    直到今日,才发现自己对重载的认识长时间以来都是错误的.幸亏现在得以纠正,真的是恐怖万分,雷人至极.一直以来,我认为重载可以发生在基类和派生类之间,例如: class A { public: void ...

  2. Java——多态浅析

    前言 在面向对象程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性.多态的含义是什么,有什么作用以及在Java中是怎么实现的?下面将做介绍. 什么是多态 简单点说就是"一个接口,多种 ...

  3. Java继承与多态浅析

    一.继承 1.通过extends继承的父类可以是不加abstract关键字的普通类,也可以是加了abstract关键字的抽象类.继承普通类时可以覆写父类的方法,或者创建自己独有的方法,或者这两     ...

  4. Java基础之多态和泛型浅析

    Java基础之多态和泛型浅析 一.前言: 楼主看了许多资料后,算是对多态和泛型有了一些浅显的理解,这里做一简单总结 二.什么是多态? 多态(Polymorphism)按字面的意思就是“多种状态”.在面 ...

  5. Java 浅析三大特性之一多态

    Java 浅析三大特性之一多态 之前我们的文章讲了Java的封装和继承,封装讲的时候,并没有体现出来封装的强大之处,反而还要慎用封装.因为这时的封装还没有和多态联系到一起,还无法看出向上转型的厉害之处 ...

  6. 浅析Java三大特性封装、继承、多态,及作业分析

    前言 本次博客衔接上次博客,作为这一阶段Java学习的分析.上一篇博客着重介绍了Java的OO编程思维,面向对象与面向过程的区别.本篇博客重心在Java的三大技术特性,附带作业分析. Java三大特性 ...

  7. 【深入浅出jQuery】源码浅析2--奇技淫巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  8. Java 浅析三大特性之一继承

    上文Java 浅析三大特性之一封装我们说到Java是一个注重编写类,注重于代码和功能复用的语言.Java实现代码复用的方式有很多,这里介绍一个重要的复用方式--继承. 在介绍继承之前,我们要明确一点, ...

  9. [转载]C++虚函数浅析

    原文:http://glgjing.github.io/blog/2015/01/03/c-plus-plus-xu-han-shu-qian-xi/ 感谢:单刀土豆 C++虚函数浅析 JAN 3RD ...

随机推荐

  1. csrf在web网站中有多重要

    小弟是学python的,今天在上网时看到一个商城网站,正好昨天学到了CSRF跨站请求,就对这个商城网站进行了一波测试 可以看到网页布局做的还是很不错的,然后进入了注册页面看看 之后就开始测试了 正常请 ...

  2. [python]如何理解uiautomator里面的 child, child_by_text, sibling,及使用场景

    如何理解uiautomator里面的 child, child_by_text, sibling,我们借助android原生的uiautomatorviewer抓取的控件来进行理解 以如下图进行详细讲 ...

  3. webstorm 搭建es6开发环境

    本文转自:http://www.jianshu.com/p/26601581e152 1:新建一个Empty Project项目es6 ,然后在src目录下新建了一个es.js: 2:打开设置pref ...

  4. JSONP方法解决跨域请求

    Ajax跨域请求的问题 跨域:跨域名, 一个域名下的文件去请求了和他不一样的域名下的资源文件(注意是请求文件,而不是数据接口),那么就会产生跨域请求,下面来写一个ajax来跨域请求的例子 <!D ...

  5. ACM模拟赛

    今天是毕业的学长给高二的同学测试.组队比赛,ACM赛制,于是就愉快的和学姐一队啦. 看到英文题面感到恐慌,不过好在不难读懂. A:并没有什么技术含量的模拟题: B:字符串题,给定一些比赛和每个队胜利的 ...

  6. browerify初步了解

    之前在写Signature Request Warnings & eth_sign学习的时候在里的signing examples时了解到browserify工具,可以通过这个例子学习如何使用 ...

  7. weblogic92一次成功修改密码的记录

    假设你忘记了weblogic92控制台的密码了: 假设你的hostname叫localhost.localdomain 假设你的bea在/opt下: ------------------------- ...

  8. canal数据同步目录

    我们公司对于数据同步有以下需求 1.多个mysql库中有一些基础表需要数据统一,mysql跨库同步 2.mysql热数据加载到redis 3.全文检索需要mysql同步到es 4.数据变更是附属的其它 ...

  9. C#中的委托(二)

    一.Action<T>和Func<T>委托 除了上篇描述的为每个参数和返回类型定义一个新委托类型之外,还可以使用Action<T>和Func<T>委托. ...

  10. TensorFlow入门

    Win10下pycharm安装tensorflow: 1.安装git,这样就会有windows powerShell 2.安装python3.x,配置环境变量 3.安装pip,下载地址是:https: ...