详解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表示的是一种未知状态,未来状态,比如小明兜里 ...
随机推荐
- 查看linux系统是多少位,使用 getconf LONG_BIT
查看linux系统是多少位,使用 getconf LONG_BIT echo $HOSTTYPE
- cent 7 识别exfat
cent 7 识别exfat install fuse-exfat exfat-utils
- 攻防世界(九)PHP2
攻防世界系列:PHP2 1.打开什么信息也没有. 尝试各种首页index.php index.html 加 [F12]没有结果,最后发现是index.phps .phps文件是什么? phps文件就 ...
- Redis I/O 多路复用技术原理
引言 Redis 是一个单线程却性能非常好的内存数据库, 主要用来作为缓存系统. Redis 采用网络 I/O 多路复用技术来保证在多个连接时,系统的高吞吐量(TPS). 系统吞吐量(TPS)指的是系 ...
- 8.4 parted:磁盘分区工具
parted 对于小于2TB的磁盘可以用fdisk和parted命令进行分区,这种情况一般采用flisk命令,但对于大于2TB的磁盘则只能用parted分区,且需要将磁盘转换为GPT格式. p ...
- 10.9 ping:测试主机之间网络的连通性
ping命令 可用于测试主机之间网络的连通性.执行ping命令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而可得知该主机运作正常. ping命令 ...
- unity 2017 卡在 loading界面
看了网上很多方法,都不奏效. 提供一个简单的方法,不妨一试! 打开其他版本unity,登陆你的账户,再打开unity2017即可!
- 80个Python练手项目列表
80个Python练手项目列表 我若将死,给孩子留遗言,只留一句话:Repetition is the mother of all learning重复是学习之母.他们将来长大,学知识,技巧.爱情 ...
- MinkowskiNonlinearities非线性
MinkowskiNonlinearities非线性 MinkowskiReLU class MinkowskiEngine.MinkowskiReLU(*args, **kwargs) __init ...
- Jmeter(五十一) - 从入门到精通高级篇 - jmeter之运动战(详解教程)
1.简介 运动战是一种军事作战方式,依托较大的作战空间来换取时间移动兵力包围敌方,以优势兵力速战速决,运动战的运用归为这样一段话"避敌主力,诱敌深入,集中优势兵力逐个击破".今天宏 ...