详解C++中的多态和虚函数
一、将子类赋值给父类
在C++中经常会出现数据类型的转换,比如 int-float等,这种转换的前提是编译器知道如何对数据进行取舍。类其实也是一种数据类型,也可以发生数据转换,但是这种转换只有在 子类-父类 之间才有意义。并且只能将子类赋值给父类,子类的对象赋值给父类的对象,子类的指针赋值给父类的指针,子类的引用赋值给父类的引用。这在C++中称为向上转型。相反的称为向下转型,但是向下转型有风险,本文只介绍向上转型。
1.1 将子类对象赋值给父类对象
下面我们通过一个具体地实例来看一下:
class people{
protected:
string name;
int age;
public:
people(string name, int age); // 有参构造函数声明
void display();
};
people::people(string name, int age){ // 有参构造函数定义
this->name = name;
this->age = age;
}
void people::display(){
cout << "name: " << name << "\tage: " << age << "\t是个无业游民。" << endl;
}
class teacher : public people{
private:
int salary; // 子类中不仅包含从父类中继承的 name 和 age,还有自己本身的 salary
public:
teacher(string name, int age, int salary); // 子类中的有参构造函数声明
void display();
};
// 显示调用父类中的有参构造函数来初始化从父类中继承的 name 和 age 成员
teacher::teacher(string name, int age, int salary):people(name, age){
this->salary = salary;
}
void teacher::display(){
cout << "name: " << name << "\tage: " << age << "\t是个教师,收入为:" << salary << endl;
}
people p("张三", 24); // 创建一个父类对象
p.display();
teacher t("李四", 25, 5000); // 创建一个子类对象
t.display();
p=t; // 将子类对象赋值给父类对象
p.display(); // 显示复制后的父类对象
在上述例子中,首先定义了一个父类 people 和其子类 teacher,并将子类对象 t 赋值给父类对象 p,输出结果如下:
name: 张三 age: 24 是个无业游民。 // 没有赋值之前的父类输出
name: 李四 age: 25 是个教师,收入为:5000 // 没有赋值之前的子类输出
name: 李四 age: 25 是个无业游民。 // 将子类赋值给父类之后的父类输出
通过结果我们可以发现,将子类赋值给父类对象之后,父类对象相应的成员变量改变了,但是成员函数依旧没有改变,还是父类中的成员函数。因为赋值的本质就是将现有的数据写入已经分配好的内存中,我们在 详解C++中继承的基本内容 - ZhiboZhao - 博客园 (cnblogs.com) 中分析过类对象的内存模型,对象的内存只包含了成员变量,因此对象之间的赋值时成员变量的赋值,而成员函数不存在赋值的问题。 但是子类中的成员变量不仅包含父类中继承的变量,也包含自己定义的变量,那么在将子类赋值给父类的过程中,父类中没有对应的内存空间,所以子类自己定义的变量会被舍弃。即在子类赋值给父类的过程中,父类只拿回属于自己的那一部分,如下图所示:

由于成员函数不存在对象的内存模型中,所以 p.display() 调用的永远都是父类的 display()函数,即:对象之间的赋值不会影响成员函数,也不会影响 this 指针。
1.2 将子类指针赋值给父类指针
下面我们还根据上面的例子来解析一下指针之间的赋值:
people* p = new people("张三", 24); // 创建指向父类对象的指针 p
p->display(); // 显示父类对象成员
cout << "p的地址为:" << p << endl; // 输出指针 p,即父类对象的地址
teacher* t = new teacher("李四", 25, 5000); // 创建指向子类对象的指针 t
t->display(); // 显示子类对象成员
cout << "t的地址为:" << t << endl; // 输出指针 t,即子类对象的地址
p=t; // 将子类对象指针赋值给父类对象指针
p->display(); // 显示赋值后的父类对象成员
cout << "p的地址为:" << p << endl; // 输出赋值后的指针 p,即赋值后的父类对象地址
输出结果为:
name: 张三 age: 24 是个无业游民。 // 没有赋值之前的父类输出
p的地址为:014663B0 // 没有赋值之前的父类对象地址
name: 李四 age: 25 是个教师,收入为:5000 // 没有赋值之前的子类输出
t的地址为:0146BA50 // 没有赋值之前的子类对象地址
name: 李四 age: 25 是个无业游民。 // 将子类指针赋值给父类指针之后的父类输出
p的地址为:0146BA50 // 赋值之后的父类地址
通过输出结果我们发现,通过指针赋值的方式与对象赋值的方式得到的输出结果一致,即 p.display() 始终调用的都是父类中的成员函数。然而与对象变量之间的赋值不同的是,指针赋值其实只是改变了指针的指向,并没有拷贝对象的成员,也没有改变对象的数据。
1.3 将子类引用赋值给父类引用
在文章 C++中指针与引用详解 - ZhiboZhao - 博客园 (cnblogs.com) 中我们详细解释了C++中指针与引用的关系,因此我们可以大致得出结论:对象之间引用赋值的结果与对象之间指针赋值的结果时一致的,为了验证猜想,我们定义如下实例:
people p("张三", 24);
teacher t("李四", 25, 5000); // 创建了两个对象
people &rp = p; // 分别创建指向对象的引用
teacher &rt = t;
rp.display(); // 显示赋值前引用的成员变量
rt.display();
rp = rt; // 引用赋值
rp.display(); // 显示赋值后的成员变量
输出结果如下:
name: 张三 age: 24 是个无业游民。
name: 李四 age: 25 是个教师,收入为:5000
name: 李四 age: 25 是个无业游民。
二、多态的产生原因与实现
通过上一小节,我们可以发现:编译器通过 指针(引用或者对象)来访问成员变量,指针(引用或者对象)指向哪个对象就使用哪个对象的数据;编译器通过指针(引用或者对象)的类型来访问成员函数,指针(引用或者对象)属于哪个类的类型就使用哪个类的函数。但是从直观上来讲,如果指针指向了派生类对象,那么就应该使用派生类的成员变量和成员函数,这符合人们的思维习惯。但是上节的运行结果却告诉我们,当基类指针 p 指向派生类 teacher 的对象时,虽然使用了 teacher 的成员变量,但是却没有使用它的成员函数,换句话说,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。
为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。
我们将父类 person中的 display 函数改写为虚函数,然后测试一下输出结果:
class people{
protected:
string name;
int age;
public:
people(string name, int age);
virtual void display(); // 将父类中的display函数改写为虚函数
};
people::people(string name, int age){
this->name = name;
this->age = age;
}
void people::display(){
cout << "name: " << name << "\tage: " << age
<< "\t是个无业游民。" << endl;
}
people* p = new people("张三", 24); // 创建指向父类对象的指针 p
p->display(); // 显示父类对象成员
teacher* t = new teacher("李四", 25, 5000); // 创建指向子类对象的指针 t
t->display(); // 显示子类对象成员
p=t; // 将子类对象指针赋值给父类对象指针
p->display(); // 显示赋值后的父类对象成员
输出结果为:
name: 张三 age: 24 是个无业游民。
name: 李四 age: 25 是个教师,收入为:5000
name: 李四 age: 25 是个教师,收入为:5000
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事, 它有多种形态,或者说有多种表现方式,这种现象称为多态(Polymorphism)。
C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。上面我们提到过,通过指针调用成员函数时会根据指针的类型来判断调用哪个类的成员函数,但是对于虚函数而言,其调用时根据指针的指向来确定的,即指针指向哪个类的对象,就调用哪个类的虚函数。
总的来说,多态的使用条件是:父类的指针或者引用指向子类对象。
三、多态的内存模型
我们都知道,类内的普通成员函数并不占用对象的内存空间,当对象调用普通成员函数的时候,编译器默认的将对象的地址作为函数参数的一部分(this 指针),从而找到对应的成员函数。所以个人观点认为,当父类对象的指针指向子类对象时,再调用父类的成员函数,编译器还是会把父类的 this 指针作为参数传递给成员函数,因此调用的还是父类的函数。那么多态的实现基于虚函数,而虚函数的调用根据指针的指向来确定,那么虚函数的内存模型是怎样的呢?
我们再来看一个之前例子:
class A{
public:
void show(); // A中只定义了普通成员函数
};
class B{
public:
virtual void show(); // B中定义了虚函数
};
A a;
B b;
cout << "a 占的内存空间为:" << sizeof(a) << endl;
cout << "b 占的内存空间为:" << sizeof(b) << endl;
输出结果为:
a 占的内存空间为:1
b 占的内存空间为:4
通过对比发现:A 中的普通成员函数只是占了预先分配的一个字节,而 B中的虚函数却占了4个字节的地址,存的是每个虚函数的入口地址,这个就是虚指针。下图中简单地描述了一下带有虚函数的类的内部结构:

从上图中可以看到,子类继承了父类并重写了父类中的虚函数后,虚函数表的内部会更新为子类中的成员函数地址。所以在发生多态时(父类的引用指向了子类对象),会从虚函数表中找到对应子类对象的函数入口地址。
详解C++中的多态和虚函数的更多相关文章
- C++中的多态与虚函数的内部实现
1.什么是多态 多态性可以简单概括为“一个接口,多种行为”. 也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可 ...
- C++中的多态及虚函数大总结
多态是C++中很关键的一部分,在面向对象程序设计中的作用尤为突出,其含义是具有多种形式或形态的情形,简单来说,多态:向不同对象发送同一个消息,不同的对象在接收时会产生不同的行为.即用一个函数名可以调用 ...
- 详解Objective-C中委托和协议
Objective-C委托和协议本没有任何关系,协议如前所述,就是起到C++中纯虚类的作用,对于“委托”则和协议没有关系,只是我们经常利用协议还实现委托的机制,其实不用协议也完全可以实现委托. AD: ...
- jQuery:详解jQuery中的事件(二)
上一篇讲到jQuery中的事件,深入学习了加载DOM和事件绑定的相关知识,这篇主要深入讨论jQuery事件中的合成事件.事件冒泡和事件移除等内容. 接上篇jQuery:详解jQuery中的事件(一) ...
- 图文详解Unity3D中Material的Tiling和Offset是怎么回事
图文详解Unity3D中Material的Tiling和Offset是怎么回事 Tiling和Offset概述 Tiling表示UV坐标的缩放倍数,Offset表示UV坐标的起始位置. 这样说当然是隔 ...
- 【转】详解C#中的反射
原帖链接点这里:详解C#中的反射 反射(Reflection) 2008年01月02日 星期三 11:21 两个现实中的例子: 1.B超:大家体检的时候大概都做过B超吧,B超可以透过肚皮探测到你内 ...
- 详解Webwork中Action 调用的方法
详解Webwork中Action 调用的方法 从三方面介绍webwork action调用相关知识: 1.Webwork 获取和包装 web 参数 2.这部分框架类关系 3.DefaultAction ...
- 【转】详解JavaScript中的this
ref:http://blog.jobbole.com/39305/ 来源:foocoder 详解JavaScript中的this JavaScript中的this总是让人迷惑,应该是js众所周知的坑 ...
- 深入详解SQL中的Null
深入详解SQL中的Null NULL 在计算机和编程世界中表示的是未知,不确定.虽然中文翻译为 “空”, 但此空(null)非彼空(empty). Null表示的是一种未知状态,未来状态,比如小明兜里 ...
随机推荐
- zabbix screen 图片以邮件形式发送
zabbix screen 图片以邮件形式发送 #! /usr/bin/env python #coding=utf-8 # Andy_f import time,os import urllib i ...
- Bootstrap Bootstrap3 与 Bootstrap4 的区别
Bootstrap3 与 Bootstrap4 官网地址 Bootstrap3 官网:https://v3.bootcss.com Bootstrap4 官网:https://v4.bootcss.c ...
- 浅谈:@RequestBody,@ResponseBody 和 @RequestParam
1. @RequestBody @RequestBody:主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据) GET方式无请求体,所以使用@RequestBody接收数据时,前端不 ...
- 揭秘有状态服务上 Kubernetes 的核心技术
背景 随着 Kubernetes 成为云原生的最热门的解决方案,越来越多的传统服务从虚拟机.物理机迁移到 Kubernetes,各云厂商如腾讯自研上云也主推业务通过Kubernetes来部署服务,享受 ...
- Auto ML自动特征工程
Auto ML自动特征工程 特征工程是在做机器学习训练的过程中必不可少的环节,特征工程就是找出对模型结果有益的特征交叉关系,通常特征工程需要耗费算法工程师大量的精力去尝试.针对这样的场景,PAI推出智 ...
- VB 老旧版本维护系列---有点懵逼的webserver访问
有点懵逼的webserver访问 '定义webserver地址 Dim postUrl As String = "" '定义webserver所需xml字符串参数 Dim xmlR ...
- Contos8 安装 MariaDb 时报错:Could not open mysql.plugin table: table mysql.plugin
导语: 因个人服务器误删了一些文件导致MariaDB崩溃,一直在报错,所以想着重装一下,没想到在重装后启动时再次报错(与之前报错不同),这次的报错原因大致是因为某些插件表找不到. 因此又开启了漫长的寻 ...
- 编译原理-文法(G)和语言(L)
1.设文法G2(S): S->AB A->aA|a B->bB|b G2(S)产生的语言是什么? 解:L(G2)={ambn|m,n≥1} 2.请给出产生语言为{anbn|n≥1}的 ...
- 狂神说redis笔记(二)
四.三种特殊数据类型 Geospatial(地理位置) 使用经纬度定位地理坐标并用一个有序集合zset保存,所以zset命令也可以使用 geoadd key longitud(经度) latitude ...
- 已经安装好了tensorboardX,任然报错 No module named ‘tensorboardX‘ ??
问题: 1.在jupyter notebook网页版中已经使用命令pip install tensorboardX来安装tensorboardX包,但是运行程序时仍旧出现错误:No module na ...