一、将子类赋值给父类

在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. nginx重定向rewrite

    引入rewrite vim /etc/nginx/conf.d/mobile_pc.conf server{ listen 80; server_name www.zls.com zls.com; r ...

  2. 032.Python魔术方法__new__和单态模式

    一 __new__ 魔术方法 1.1 介绍 触发时机:实例化类生成对象的时候触发(触发时机在__init__之前) 功能:控制对象的创建过程 参数:至少一个cls接受当前的类,其他根据情况决定 返回值 ...

  3. 如何做好FAE工作及FAE职位发展————资深FAE总结推荐

    http://bbs.elecfans.com/jishu_932585_1_1.html 曾经认识一位做电源研发的工程师,转行在一家代理商做FAE,做了一年半以后,就提出了离职请求,他老板问他是什么 ...

  4. make clean 和 make distclean区别-(转自秋水Leo)

    make clean仅仅是清除之前编译的可执行文件及配置文件. 而make distclean要清除所有生成的文件. Makefile 在符合GNU Makefiel惯例的Makefile中,包含了一 ...

  5. 机器学习实战二:波士顿房价预测 Boston Housing

    波士顿房价预测 Boston housing 这是一个波士顿房价预测的一个实战,上一次的Titantic是生存预测,其实本质上是一个分类问题,就是根据数据分为1或为0,这次的波士顿房价预测更像是预测一 ...

  6. 基于 BDD 理论的 Nebula 集成测试框架重构(上篇)

    本文首发于 Nebula Graph 公众号 NebulaGraphCommunity,Follow 看大厂图数据库技术实践. 测试框架的演进 截止目前为止,在 Nebula Graph 的开发过程中 ...

  7. 一文读懂一条 SQL 查询语句是如何执行的

    2001 年 MySQL 发布 3.23 版本,自此便开始获得广泛应用,随着不断地升级迭代,至今 MySQL 已经走过了 20 个年头. 为了充分发挥 MySQL 的性能并顺利地使用,就必须正确理解其 ...

  8. kindeditor富文本框使用方法

    这周我一共使用了两个文本框编辑器!我的上一篇文档讲的是wangeditor这个编辑器,现在就来讲讲kindeditor这个编辑器! 首先还是去它的官网去下载脚本! http://kindeditor. ...

  9. java IO教程《四》

    properties使用 什么是Properties? Properties(Java.util.Properties),该类主要用于读取Java的配置文件,不同的编程语言有自己所支持的配置文件,配置 ...

  10. fiddler修改请求包和返回包

    设置好过滤后,找到需要修改的包,按如下步骤进行包的数据修改1.设置"禁止上传"(禁止XX为本人自己理解,专业术语不记得了,高手可留言笔者重新修订博文),打上断点,如下标志就是在请求 ...