一、将子类赋值给父类

在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. CentOS7安装vncserver(启动失败及连接黑屏解决办法)

    CentOS7安装vncserver(启动失败及连接黑屏解决办法) 转载weixin_34167043 最后发布于2017-11-09 15:11:00 阅读数 42  收藏 展开 AutoSAR入门 ...

  2. jmeter中beanshell postprocessor结合fastjson库提取不确定个数的json参数

    在项目实践中,遇到了这样一个问题.用jmeter作http接口测试,需要的接口参数个数是不确定的.也就是说,在每次测试中,根据情况不同,可能页面中的列表中所含的参数个数是不确定的,那么要提取的参数个数 ...

  3. C/C++ 复习

    本文总结一下C++面试时常遇到的问题.C++面试中,主要涉及的考点有 关键字极其用法,常考的关键字有const, sizeof, typedef, inline, static, extern, ne ...

  4. Mac 使用 Parallels Desktop 虚拟机安装 win10 教程

    Parallels Desktop 介绍 Parallels Desktop 是一款运行在 Mac 电脑上的极为优秀的虚拟机软件,用户可以在 Mac OS X下非常方便运行 Windows.Linux ...

  5. python3 访问windows共享目录

    python3 访问windows共享目录 1.安装pysmb包 pip install pysmb 2.连接共享目录 #!/usr/bin/env python3 # -*- coding:utf- ...

  6. Go语言协程并发---原子操作

    package main import ( "fmt" "sync/atomic" ) /* 用原子来替换锁,其主要原因是: 原子操作由底层硬件支持,而锁则由操 ...

  7. 【注意力机制】Attention Augmented Convolutional Networks

    注意力机制之Attention Augmented Convolutional Networks 原始链接:https://www.yuque.com/lart/papers/aaconv 核心内容 ...

  8. HTML <a> 标签的 href 属性

    w3school页面的描述: HTML <a> 标签的 href 属性 HTML <a> 标签 实例 href 属性规定链接的目标: <a href="http ...

  9. 3 Python相对路径地址的的一个问题

    构建程序xiaojie_test.py import os from xxx.yyy import test test() 同目录下构建一个目录xxx,并且目录中有/tmp/results/graph ...

  10. JVM-gcRoots 和 强引用,软引用, 弱引用, 虚引用, 代码演示和应用场景

    什么是垃圾? 什么是gcRoots, 谈谈你对 强, 软, 弱 , 虚引用的理解, 他们的应用场景 jvm采用可达性分析法: 从gcRoots集合开始,自上向下遍历,凡是在引用链上的对象,都不是垃圾, ...