c++中的多态机制
目录
背景介绍
虚函数重写:子类重新定义父类中有相同返回值、名称和参数的虚函数;
非虚函重写:子类重新定义父类中有相同名称和参数的非虚函数;
父子间的赋值兼容:子类对象可以当作父类对象使用(兼容性);具体表现为:
1. 子类对象可以直接赋值给父类对象;
2. 子类对象可以直接初始化父类对象;
3. 父类指针可以直接指向子类对象;
4. 父类引用可以直接引用子类对象;
当发生赋值兼容时,子类对象退化为父类对象,只能访问父类中定义的成员,可以直接访问被子类覆盖的同名成员;
// 在赋值兼容原则中,子类对象退化为父类对象,子类是特殊的父类;
#include <iostream>
#include <string> using namespace std; class Parent
{
public:
int mi; void add(int i)
{
mi += i;
} void add(int a, int b)
{
mi += (a + b);
}
}; class Child : public Parent
{
public:
int mi; void add(int x, int y, int z)
{
mi += (x + y + z);
}
}; int main()
{
Parent p;
Child c; c.mi = ;
p = c; // p.mi = 0; 子类对象退化为父类对象
Parent p1(c); // p1.mi = 0; 同上
Parent& rp = c;
Parent* pp = &c; rp.add();
pp->add(, ); cout << "p.mi: " << p.mi <<endl; // p.mi: 0;
cout << "p1.mi: " << p1.mi <<endl; // p1.mi: 0;
cout << "c.Parent::mi: " << c.Parent::mi <<endl; // c.Parent::mi: 35
cout << "rp.mi: " << rp.mi <<endl; // rp.mi: 35
cout << "pp->mi: " << pp->mi <<endl; // pp->mi: 35 return ;
}
赋值兼容测试
在面向对象的继承关系中,我们了解到子类可以拥有父类中的所有属性与行为;但是,有时父类中提供的方法并不能满足现有的需求,所以,我们必须在子类中重写父类中已有的方法,来满足当前的需求。此时尽管我们已经实现了函数重写(这里是非虚函数重写),但是在类型兼容性原则中也不能出现我们期待的结果(不能根据指针/引用所指向的实际对象类型去调到对应的重写函数)。接下来我们用代码来复现这个情景:
#include <iostream>
#include <string> using namespace std; class Parent
{
public:
void print()
{
cout << "I'm Parent." << endl;
}
}; class Child : public Parent
{
public:
void print()
{
cout << "I'm Child." << endl;
}
}; void how_to_print(Parent* p)
{
p->print();
} int main()
{
Parent p;
Child c; how_to_print(&p); // I'm Parent // Expected to print: I'm Parent.
how_to_print(&c); // I'm Parent // Expected to print: I'm Child. return ;
}
非虚函数重写与赋值兼容的问题
为什么会出现上述现象呢?(在赋值兼容中,父类指针/引用指向子类对象时为何不能调用子类重写函数?)
问题分析:在编译期间,编译器只能根据指针的类型判断所指向的对象;根据赋值兼容,编译器认为父类指针指向的是父类对象;因此,编译结果只可能是调用父类中定义的同名函数。
在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么。但是编译器没有理由报错,于是,编译器认为最安全的做法是调用父类的print函数。因为父类和子类肯定都有相同的print函数。
要想解决这个问题,就需要使用c++中的多态。那么如何实现c++中的多态呢?请看下面详解:
多态介绍
1、 什么是多态
在现实生活中,多态是同一个事物在不同场景下的多种形态。
在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。
2、 多态的分类
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错;
动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。
3、动态多态成立的条件
由之前出现的问题可知,编译器的做法并不符合我们的期望(因为编译器是根据父类指针的类型去父类中调用被重写的函数);但是,在面向对象的多态中,我们期望的行为是 根据实际的对象类型来判断如何调用重写函数(虚函数);
1. 即当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;
2. 即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数;
这种多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。
那么在c++中,如何实现这种表现效果呢?(实现多态的条件)
1. 要有继承
2. 要有虚函数重写(被 virtual 声明的函数叫虚函数)
3. 要有父类指针(父类引用)指向子类对象
4、静态联编和动态联编
静态联编:在程序的编译期间就能确定具体的函数调用;如函数重载,非虚函数重写;
动态联编:在程序实际运行后才能确定具体的函数调用;如虚函数重写,switch 语句和 if 语句;
#include <iostream>
#include <string> using namespace std; class Parent
{
public:
virtual void func()
{
cout << "Parent::void func()" << endl;
} virtual void func(int i)
{
cout << "Parent::void func(int i) : " << i << endl;
} virtual void func(int i, int j)
{
cout << "Parent::void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
}
}; class Child : public Parent
{
public:
void func(int i, int j)
{
cout << "Child::void func(int i, int j) : " << i + j << endl;
} void func(int i, int j, int k)
{
cout << "Child::void func(int i, int j, int k) : " << i + j + k << endl;
}
}; void run(Parent* p)
{
p->func(, ); // 展现多态的特性
// 动态联编
} int main()
{
Parent p; p.func(); // 静态联编
p.func(); // 静态联编
p.func(, ); // 静态联编 cout << endl; Child c; c.func(, ); // 静态联编 cout << endl; run(&p);
run(&c); return ;
}
/*
Parent::void func()
Parent::void func(int i) : 1
Parent::void func(int i, int j) : (1, 2) Child::void func(int i, int j) : 3 Parent::void func(int i, int j) : (1, 2)
Child::void func(int i, int j) : 3
*/
静态联编与动态联编的案列
5、动态多态的实现原理
虚函数表与vptr指针
1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表;
2. 虚函数表是一个存储类成员函数指针的数据结构;
3. 虚函数表是由编译器自动生成与维护的;
4. virtual成员函数会被编译器放入虚函数表中;
5. 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。
多态执行过程:
1. 在类中,用 virtual 声明一个函数时,就会在这个类中对应产生一张 虚函数表,将虚函数存放到该表中;
2. 用这个类创建对象时,就会产生一个 vptr指针,这个vptr指针会指向对应的虚函数表;
3. 在多态调用时, vptr指针 就会根据这个对象 在对应类的虚函数表中 查找被调用的函数,从而找到函数的入口地址;
》 如果这个对象是 子类的对象,那么vptr指针就会在 子类的 虚函数表中查找被调用的函数
》 如果这个对象是 父类的对象,那么vptr指针就会在 父类的 虚函数表中查找被调用的函数
注:出于效率考虑,没有必要将所有成员函数都声明为虚函数。
如何证明vptr指针的存在?
#include <iostream>
#include <string> using namespace std; class Demo1
{
private:
int mi; // 4 bytes
int mj; // 4 bytes
public:
virtual void print(){} // 由于虚函数的存在,在实例化类对象时,就会产生1个 vptr指针
}; class Demo2
{
private:
int mi; // 4 bytes
int mj; // 4 bytes
public:
void print(){}
}; int main()
{
cout << "sizeof(Demo1) = " << sizeof(Demo1) << " bytes" << endl; // sizeof(Demo1) = 16 bytes
cout << "sizeof(Demo2) = " << sizeof(Demo2) << " bytes" << endl; // sizeof(Demo2) = 8 bytes return ;
} // 64bit(OS) 指针占 8 bytes
// 32bit(OS) 指针占 4 bytes
vptr指针的证明
显然,在普通的类中,类的大小 == 成员变量的大小;在有虚函数的类中,类的大小 == 成员变量的大小 + vptr指针大小。
6、 虚析构函数
定义:用 virtual 关键字修饰析构函数,称为虚析构函数;
格式:virtual ~ClassName(){ ... }
意义:虚析构函数用于指引 delete 运算符正确析构动态对象;(当父类指针指向子类对象时,通过父类指针去释放所有子类的内存空间)
应用场景:在赋值兼容性原则中(父类指针指向子类对象),通过 delete 父类指针 去释放所有子类的内存空间。(动态多态调用:通过父类指针所指向的实际对象去判断如何调用 delete 运算符)
#include <iostream>
#include <cstring> using namespace std; class Base
{
protected:
char *name;
public:
Base()
{
name = new char[];
strcpy(name, "Base()");
cout <<this << " " << name << endl;
} ~Base()
{
cout << this << " ~Base()" << endl;
delete[] name;
}
}; class Derived : public Base
{
private:
int *value;
public:
Derived()
{
strcpy(name, "Derived()");
value = new int(strlen(name));
cout << this << " " << name << " " << *value << endl;
} ~Derived()
{
cout << this << " ~Derived()" << endl;
delete value;
}
}; int main()
{
cout << "在赋值兼容中,关于 子类对象存在内存泄漏的测试" << endl; Base* bp = new Derived();
cout << bp << endl;
// ...
delete bp; // 虽然是父类指针,但析构的是子类资源 return ;
} /**
* 在赋值兼容中,关于 子类对象存在内存泄漏的测试
* 0x7a1030 Base()
* 0x7a1030 Derived() 9
* 0x7a1030
* 0x7a1030 ~Base()
*/
赋值兼容中,子类内存泄漏案列
#include <iostream>
#include <cstring> using namespace std; class Base
{
protected:
char *name;
public:
Base()
{
name = new char[];
strcpy(name, "Base()");
cout <<this << " " << name << endl;
} virtual ~Base()
{
cout << this << " ~Base()" << endl;
delete[] name;
}
}; class Derived : public Base
{
private:
int *value;
public:
Derived()
{
strcpy(name, "Derived()");
value = new int(strlen(name));
cout << this << " " << name << " " << *value << endl;
} virtual ~Derived()
{
cout << this << " ~Derived()" << endl;
delete value;
}
}; int main()
{
//Derived *dp = new Derived();
//delete dp; // 直接通过子类对象释放资源不需要 virtual 关键字 cout << "在赋值兼容中,虚析构函数的测试" << endl; Base* bp = new Derived();
cout << bp << endl;
// ...
delete bp; // 动态多态发生 return ;
} /**
* 在赋值兼容中,虚析构函数的测试
* 0x19b1030 Base()
* 0x19b1030 Derived() 9
* 0x19b1030
* 0x19b1030 ~Derived()
* 0x19b1030 ~Base()
*/
虚析构函数解决子类内存泄漏案列
两个案列的区别:第1个案列只是普通的析构函数;第2个案列是虚析构函数。
7、 关于虚函数的思考题
1. 构造函数可以成为虚函数吗?--- 不可以
不可以。因为在构造函数执行结束后,虚函数表指针才会被正确的初始化。
在c++的多态中,虚函数表是由编译器自动生成与维护的,虚函数表指针是由构造函数初始化完成的,即构造函数相当于是虚函数的入口点,负责调用虚函数的前期工作;在构造函数执行的过程中,虚函数表指针有可能未被正确的初始化;由于在不同的c++编译器中,虚函数表 与 虚函数表指针的实现有所不同,所以禁止将构造函数声明为虚函数。
2. 析造函数可以成为虚函数吗?--- 虚函数,且发生多态
可以,并且产生动态多态。因为析构函数是在对象销毁之前被调用,即在对象销毁前 虚函数表指针是正确指向对应的虚函数表。
3. 构造函数中可以调用虚函数发生多态吗?--- 不能发生多态
构造函数中可以调用虚函数,但是不可能发生多态行为,因为在构造函数执行时,虚函数表指针未被正确初始化。
4. 析构函数中可以调用虚函数发生多态吗?--- 不能发生多态
析构函数中可以调用虚函数,但是不可能发生多态行为,因为在析构函数执行时,虚函数表指针已经被销毁。
#include <iostream>
#include <string> using namespace std; class Base
{
public:
Base()
{
cout << "Base()" << endl; func();
} virtual void func()
{
cout << "Base::func()" << endl;
} virtual ~Base()
{
func(); cout << "~Base()" << endl;
}
}; class Derived : public Base
{
public:
Derived()
{
cout << "Derived()" << endl; func();
} virtual void func()
{
cout << "Derived::func()" << endl;
} virtual ~Derived()
{
func(); cout << "~Derived()" << endl;
}
}; void test()
{
Derived d;
} int main()
{
//栈空间
test(); // 堆空间
//Base* p = new Derived();
//delete p; // 多态发生(指针p指向子类对象,并且又有虚函数重写) return ;
}
/*
Base()
Base::func()
Derived()
Derived::func()
Derived::func()
~Derived()
Base::func()
~Base()
*/
构造与析构中调用虚函数案列
结论:在构造函数与析构函数中调用虚函数不能发生多态行为,只调用当前类中定义的函数版本! !
8、纯虚函数、抽象类、接口
1. 定义 --- 以案例的方式说明
想必大家很熟悉,对于任何一个普通类来说都可以实例化出多个对象,也就是每个对象都可以用对应的类来描述,并且这些对象在现实生活中都能找到各自的原型;比如现在有一个“狗类
c++中的多态机制的更多相关文章
- 深度分析:理解Java中的多态机制,一篇直接帮你掌握!
Java中的多态 1 多态是什么 多态(Polymorphism)按字面的意思就是"多种状态".在面向对象语言中,接口的多种不同的实现方式即为多态.用白话来说,就是多个对象调用同一 ...
- 第二十二篇:C++中的多态机制
前言 封装性,继承性,多态性是面向对象语言的三大特性.其中封装,继承好理解,而多态的概念让许多初学者感到困惑.本文将讲述C++中多态的概念以及多态的实现机制. 什么是多态? 多态就是多种形态,就是许多 ...
- 简述C++中的多态机制
前言 封装性,继承性,多态性是面向对象语言的三大特性.其中封装,继承好理解,而多态的概念让许多初学者感到困惑.本文将讲述C++中多态的概念以及多态的实现机制. 什么是多态? 多态就是多种形态,就是许多 ...
- java中实现多态的机制是什么?
多态性是面向对象程序设计代码重用的一个重要机制,我们曾不只一次的提到Java多态性.在Java运行时多态性:继承和接口的实现一文中,我们曾详细介绍了Java实现运行时多态性的动态方法调度:今天我们再次 ...
- c++中虚多态的实现机制
c++中虚多态的实现机制 參考博客:http://blog.csdn.net/neiloid/article/details/6934135 序言 证明vptr指针存在 无继承 单继承无覆盖 单继承有 ...
- Java多态机制和继承中重写重载
关于Java中多态机制 http://www.cnblogs.com/chenssy/p/3372798.html 这篇博文讲的很透彻 大体意思是 多态定义: 多态就是指程序中定义的引用变量所指向的具 ...
- 深入理解Java多态机制
从字节码层面来看,Java中的所有方法调用,最终无外乎转换为如下几条调用指令. invokestatic: 调用静态方法. invokespecial: 调用实例构造器<init>方法,私 ...
- 转载:C#中的多态
原文地址 http://www.cnblogs.com/jhxk/articles/1644018.html 感谢博主分享! 之前看到过类似的问题:如果面试时主考官要求你用一句话来描述多态,尽可能的 ...
- 深入理解Java中的多态
一.什么是多态? 多态指同一个实体同时具有多种形式.它是面向对象程序设计(OOP)的一个重要特征.如果一个语言只支持类而不支持多态,只能说明它是基于对象的,而不是面向对象的. 二.多态是如何实现的? ...
随机推荐
- percona-toolkit 之 【pt-query-digest】使用
背景 关于pt-query-digest的使用场景和方法在percona-toolkit 之 [pt-query-digest]介绍文章里已经做了详细说明,现在开始介绍下如何使用,以及常用的命令. 使 ...
- Centos 7 中 部署 asp.net core 3.0 + nginx + mongodb 的一些新手简单入门,非docker
目录 零.准备工作 一.部署Mongodb 1.安装Mongodb 2.创建mongodb的数据目录 3.设置目录权限 4.设置mongodb启动 5.修改mongodb的配置文件 6.启动Mongo ...
- GPUImage学习总结
GPUImage是iOS上一个基于OpenGL进行图像处理的开源框架,内置大量滤镜,架构灵活,可以在其基础上很轻松地实现各种图像处理功能. GPUImgae特性 1,丰富的输入组件 摄像头.图片.视频 ...
- H5多列布局
多列布局 基本概念 1.多列布局类似报纸或杂志中的排版方式,上要用以控制大篇幅文本. 2.跨列属性可以控制横跨列的数量 /*列数*/ -webkit-column-count: 3; /*分割线*/ ...
- #AcWing系列课程Level-2笔记——5.高精度“+”算法
高精度"+"算法 编写高精度"+",记住下面的过程,代码也就游刃有余了! 1.首先我们要明白大整数是如何存储的? 2.其次存储完,如何运算? 高精度" ...
- DNA sequence HDU - 1560
DNA sequence Time Limit: 15000/5000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Tot ...
- 第十章、Vue项目的联调上线
抓包 Fiddler 一.解决跨域 proxyTable(查看博客总结) 二.解决用本机ip地址不能访问 在dev中加上 --host 0.0.0.0就可以用本机ip访问,这样的话可以用手机在内网(局 ...
- sonarqube配置全指南,集成阿里巴巴p3c规范
环境准备 内置数据库 Sonar安装成功后,默认内置H2数据库,用于记录单次的扫描结果,对同一个project重复扫码,会覆盖之前的扫描记录,所以H2 数据库只应用于测试,不可以用于生产环境,那如果你 ...
- Redis系列二 - 数据结构
前言 redis作为我们开发的一大神器,我们接触肯定不会少,但是很多同学也许只会存储String类型的值,这是非常不合理的.在这里,将带大家认识Redis的5中数据结构. 1.问:Redis有那些数据 ...
- Ajax上传数据和上传文件(三种方式)
Ajax向后端发送数据可以有三种方式:原生Ajax方式,jQuery Ajax方式,iframe+form 方式(伪造Ajax方式) <!DOCTYPE html> <html la ...