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. JS中 map, filter, some, every, forEach, for in, for of 用法总结

    本文转载自:http://blog.csdn.net/gis_swb/article/details/52297343 1.map 有返回值,返回一个新的数组,每个元素为调用func的结果. let ...

  2. Win10环境Tensorflow-GPU13.1/JupyterNotebook的安装

    参考 : Anaconda Tensorflow GPU 版本的安装问题 https://blog.csdn.net/u010977034/article/details/62038698 Windo ...

  3. TIOBE 11月编程语言榜:Go逆袭,Python势头很猛!

    导读 离 TIOBE 宣布 2018 年的编程语言只有2个月了.目前来看,有 5 个候选对象,它们都是来自前五名的:Java.C.C++.Python.Visual Basic.NET.每年我们都希望 ...

  4. dom4j加载xml文件

    ## dom4j加载xml文件 ``` // 1. 加载xml文件 InputStream is = MyTest.class.getResourceAsStream("user.xml&q ...

  5. eclipse 格式化快捷键(Ctrl+shift+f)不起作用的解决办法

    eclipse格式化快界面Ctrl+Shift+f不起作用一般是键位冲突所导致的,一般是搜狗输入法的“繁体与简体”中文切换快界面冲突. 把它禁用掉就可以了. 下面是禁用步骤: 点击sougou输入法右 ...

  6. ansible 变量定义和引用

    cat /etc/ansible/hosts [nodes]10.2.1.232 key=23210.2.1.43 key=43 cat debug.yaml ---- name: test how ...

  7. STM32 中 BIT_BAND(位段/位带)和别名区使用入门(转载)

    一. 什么是位段和别名区 是这样的,记得MCS51吗? MCS51就是有位操作,以一位(BIT)为数据对象的操作,MCS51可以简单的将P1口的第2位独立操作: P1.2=0;P1.2=1 :这样就把 ...

  8. 20155216 Exp7 网络欺诈技术防范

    Exp7 网络欺诈技术防范 基础问题回答 1.通常在什么场景下容易受到DNS spoof攻击? 1.在同一局域网下比较容易受到DNS spoof攻击,攻击者可以冒充域名服务器,来发送伪造的数据包,从而 ...

  9. 让Visual Studio载入Symbol(pdb)文件

    让Visual Studio载入Symbol(pdb)文件 让Visual Studio载入Symbol(pdb)文件 在VC编译工程的编译连接阶段,会产生Symbol文件,也就是常说的 pdb 文件 ...

  10. [CF986F]Oppa Funcan Style Remastered[exgcd+同余最短路]

    题意 给你 \(n\) 和 \(k\) ,问能否用 \(k\) 的所有 \(>1\) 的因子凑出 \(n\) .多组数据,但保证不同的 \(k\) 不超过 50 个. \(n\leq 10^{1 ...