一、将子类赋值给父类

在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++中的多态和虚函数的更多相关文章

  1. C++中的多态与虚函数的内部实现

    1.什么是多态         多态性可以简单概括为“一个接口,多种行为”.         也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可 ...

  2. C++中的多态及虚函数大总结

    多态是C++中很关键的一部分,在面向对象程序设计中的作用尤为突出,其含义是具有多种形式或形态的情形,简单来说,多态:向不同对象发送同一个消息,不同的对象在接收时会产生不同的行为.即用一个函数名可以调用 ...

  3. 详解Objective-C中委托和协议

    Objective-C委托和协议本没有任何关系,协议如前所述,就是起到C++中纯虚类的作用,对于“委托”则和协议没有关系,只是我们经常利用协议还实现委托的机制,其实不用协议也完全可以实现委托. AD: ...

  4. jQuery:详解jQuery中的事件(二)

    上一篇讲到jQuery中的事件,深入学习了加载DOM和事件绑定的相关知识,这篇主要深入讨论jQuery事件中的合成事件.事件冒泡和事件移除等内容. 接上篇jQuery:详解jQuery中的事件(一) ...

  5. 图文详解Unity3D中Material的Tiling和Offset是怎么回事

    图文详解Unity3D中Material的Tiling和Offset是怎么回事 Tiling和Offset概述 Tiling表示UV坐标的缩放倍数,Offset表示UV坐标的起始位置. 这样说当然是隔 ...

  6. 【转】详解C#中的反射

    原帖链接点这里:详解C#中的反射   反射(Reflection) 2008年01月02日 星期三 11:21 两个现实中的例子: 1.B超:大家体检的时候大概都做过B超吧,B超可以透过肚皮探测到你内 ...

  7. 详解Webwork中Action 调用的方法

    详解Webwork中Action 调用的方法 从三方面介绍webwork action调用相关知识: 1.Webwork 获取和包装 web 参数 2.这部分框架类关系 3.DefaultAction ...

  8. 【转】详解JavaScript中的this

    ref:http://blog.jobbole.com/39305/ 来源:foocoder 详解JavaScript中的this JavaScript中的this总是让人迷惑,应该是js众所周知的坑 ...

  9. 深入详解SQL中的Null

    深入详解SQL中的Null NULL 在计算机和编程世界中表示的是未知,不确定.虽然中文翻译为 “空”, 但此空(null)非彼空(empty). Null表示的是一种未知状态,未来状态,比如小明兜里 ...

随机推荐

  1. 网络安全服务(Network Security Services, NSS

    网络安全服务(Network Security Services, NSS)是一套为网络安全服务而设计的库 支持支持安全的客户端和 服务器应用程序.使用NSS构建的应用程序可以支持SSL v2 和v3 ...

  2. 小米华为vivooppo手机记录隐私证据查询

    1.在拨号界面输入:*#*#4636#*#* 2.在输入代码之后 手机会自动跳转到下面这个页面 就可以查看她到底拿着手机在干嘛 2 输入下面代码可以检测小米手机的各种信息 *#*#64663#*#* 

  3. MySQL配置HeartBeat实现心跳监控和浮动IP

    1. 初始化环境配置 /sbin/chkconfig --add mysqld /sbin/chkconfig mysqld on ln -s /usr/local/mysql/bin/mysql / ...

  4. Linux创建RAID1_实战

    Linux创建RAID1实战 Linux创建RAID1 RAID1俗称镜像,它最少由两个硬盘组成,且两个硬盘上存储的数据均相同,以实现数据冗余 RAID1读操作速度有所提高,写操作理论上与单硬盘速度一 ...

  5. nginx 配置 conf stream

    nginx从1.9.0版本开始,新增了ngx_stream_core_module模块,使nginx支持四层负载均衡.默认编译的时候该模块并未编译进去,需要编译的时候添加--with-stream参数 ...

  6. 【JVM进阶之路】十四:类加载器和类加载机制

    在上一章里,我们已经学习了类加载的过程,我们知道在加载阶段需要"通过一个类的全限定名来获取描述该类的二进制字节流",而来完成这个工作的就是类加载器(Class Loader). 1 ...

  7. 论鸿蒙OS在某些人眼中的样子

    对于鸿蒙OS,博客园有一篇文章<为鸿蒙OS说两句公道话(我对鸿蒙OS的一些看法)>.有兴趣的可以看看. 在这篇文章中,个人觉得最精彩的不是文章本身,而是评论内容. 下面我挑一些出来,和大家 ...

  8. Python小白的数学建模课-05.0-1规划

    0-1 规划不仅是数模竞赛中的常见题型,也具有重要的现实意义. 双十一促销中网购平台要求二选一,就是互斥的决策问题,可以用 0-1规划建模. 小白学习 0-1 规划,首先要学会识别 0-1规划,学习将 ...

  9. 重新整理 .net core 实践篇—————日志系统之战地记者[十五]

    前言 本节开始整理日志相关的东西.先整理一下日志的基本原理. 正文 首先介绍一下包: Microsoft.Extengsion.Logging.Abstrations 这个是接口包. Microsof ...

  10. C#中关于Cookie的理解

    本文链接出自:https://www.cnblogs.com/xiangzhe-C/p/4230042.html 1.Cookie简介 Cookie 提供了一种在 Web 应用程序中存储用户特定信息的 ...