C++虚函数多态原理-最直白的讲解

2024-08-28 02:32

本文主要是介绍C++虚函数多态原理-最直白的讲解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C++的主要特点是抽象,继承,封装和多态。我们先理解抽象是什么?在这之前,首先我问你C++是什么,是用来干什么的?

一切的语言都是用来描述现实世界的 ,C++也是.C++的任何特性都是为了去描述这个世界,并为其解决提供方法。但是C++还是并不能完全去描述这个世界,因为现实世界是无法完全认知的,只能不断去认知的,扯远了,感到到了哲学这段。抽象?例如现实世界的人类,书,树,桌子,椅子等名词都是一种对某些具有共同特征实体的抽象描述。感觉有点可以用集合来理解。难道每次去描述人时都是直立行走,两只眼睛,一直嘴巴?所以需要抽象来解决这个问题。因此抽象就对应了C++中的类。

类的基本定义形式:

class human{public:friend void ShowN(Internet &obj);protected:private:
};
void ShowN(human &obj)        
{ 
cout<<obj.name<<endl;          //可访问human类中的成员
} 
如果不标注属性,默认为public。
那protected右包括什么呢?类的本质属性,无法被类外访问的特性,只可以被子类与本类访问。
public:类外,类内均可访问,真正面对使用者的接口。
private:只可被类内使用,子类不可用。
也有特殊的情况,比如友元函数和友元类。会破坏类的封装性。
友元函数 : 可以访问类成员的普通函数。友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:friend 类型 函数名(形式参数);友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。一个函数可以是多个类的友元函数,只需要在各个类中分别声明。友元函数的调用与一般函数的调用方式和原理一致。友元类 : 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。       当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:friend class 类名;其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。

再比如描述人时,你还是不知道它是亚洲人,欧洲人,还是非洲人,或许是外星人?所以又出现了继承这一概念,对应人集合中的子集。

1、公开继承:在公开继承下,父类型中的数据是公开的到子类型中权限是公开的;父类型中保护权限的数据到子类中是保护的;父类中私有的数据到子类中会隐藏掉(就是说看不见权限,但是实际上式在子类中的);

2、私有继承:在私有继承下,父类中的公开数据到子类中变成私有的,父类中的保护数据到子类中称为私有的,父类中的私有数据到子类中隐藏;

3、保护继承:保护继承下,父类中的公开数据和保护数据到了子类中都成为保护权限,父类中私有的数据到了子类中就变成了隐藏的;

继承的特殊点:构造函数和析构函数是不能被继承的,但是可以被调用。并且子类一定会调用父类的构造函数。
子类默认调用父类的无参构造,也可以制定调用构造函数;当程序中没有显式的给出构造函数时候,系统会提供默认的构造函数,并不会中任何事情;析构函数的调用和构造函数的调用顺序相反;拷贝构造函数和赋值运算符函数也不能被继承:在子类不提供拷贝构造和赋值运算符时,子类默认调用父类的赋值运算符和拷贝构造函数。但子类一旦提供拷贝构造和赋值运算符函数则不再调用父类拷贝构造和赋值运算符函数。

封装就是对类中的信息进行分级,主要是针对类的实例来说,有些东西你可以调用,有些东西你不能调用。也就是类的设计者并不想让类的实例为所欲为。你只需要知道类的方法成员接口,具体内部的实现细节给隐藏起来了,随便搜一个开源项目比如opencv,你就只能知道类的具体接口以及功能,却并不知道具体是如何实现的?这就是封装。

突然想到人到底有没有灵魂一说呢?假如上帝是人类的设计者,每个人就是一个实例,将灵魂属性进行隐藏,你又如何知道它的存在呢?或许只有对实例代码进行研究透,搞明白任何原理才知道吧。

先来讨论一下子类和父类的关系?先讨论函数。

子类会继承父类的成员变量和成员函数,同时子类也可以有自己的成员函数和成员变量。如果子类的成员变量的名称和父类的成员变量名称一样,调用子类实例时,是调用父类变量还是子类变量呢?如果不加作用域的情况下,调用子类变量。这是隐藏。子类将父类的变量隐藏了。同样:如果子类的成员函数的名称和父类的成员函数名称一样时,尽管返回值或者参数类型,参数个数也不一样,调用子类实例时,父类的同名成员函数均会被隐藏,无法调用。这就是函数隐藏。隐藏不代表就没有了,可以通过类名作用域::访问到被隐藏的成员。b.A::show();b为子类对象,A为父类。

至于如果子类有多个成员函数的名称一样,其他不同,则他们彼此之间的关系为函数重载。根据参数类型,返回值类型而调用不同的成员函数。同一作用域下。

函数重写:在父类中出现一个虚函数,如果在子类中提供和父类同名的函数(注意区分名字隐藏),这就加函数重写。

函数重写要求必须有相同函数名,相同的参数列表,相同的返回值类型。

虚函数在函数前面加上virtual关键字修饰过的就是虚函数.虚函数的主要表现为会占用四个字节的空间,只要成员中出现虚函数,不管有多少个虚函数,都只用四个字节来维护这个虚关系。虚函数会影响对象的大小。维护虚关系使用一个指针来维护的,所以是四个字节。

定义下多态的含义:

通过父类类型的指针或者引用指向子类对象。通过该指针或者引用就可以实现,若父类和子类中均有虚函数,只能调用子类的虚函数以及继承父类的函数,而无法对子类其它的函数进行调用的功能。

多态的实现主要依赖于下面的三个东西:
虚函数:成员函数加了virtual修饰,virtual关键字只能修饰成员函数或者析构函数,其他的函数都不行。

虚函数表指针:一个类型有虚函数,则对这个类型提供一个指针,这个指针放在生成对象的前四个字节。同类型的对象共享一张虚函数表。并且不同类型的虚函数表地址不同。

虚函数表:虚函数表中的每个元素都是虚函数的地址。

一个类型一旦出现虚函数,则会生成一张虚函数表,虚函数表中存放的就是虚函数的函数地址,通过这个函数地址可以到代码区中去执行对应的函数。虚函数表中只存放类型中的虚函数,不是虚函数的一概不管。在每个生成的类型对象的前四个字节中存放的是虚函数表指针,通过这个指针可以访问到虚函数表,从而访问其中的函数。同种类型共享虚函数表,不同类型有自己独立的虚函数表,继承关系中的子类和父类属于不同类型,所以有自己独立的函数表。

假如现在有一个函数:输入为人类型比如亚洲或者欧洲或者非洲啊,输出为该人类型的肤色,那如何传参数呢?形参如何写呢?第一种是对该函数进行重载,这样会很浪费代码。

第二种就是用多态了,利用子类和父类之间的多态性来解决,来对应形参和实参之间的关系。形参为父类实例引用,实参为子类实例,这样调用的虚函数就对应了相应实参类型的虚函数。

多态的产生条件:

继承是构成多态的基础;

虚函数是构成多态的关键;

函数覆盖是构成多态的必备条件;

多态的应用:函数参数,函数返回值。

代码如下:

class CBse
{
public:virtual void f1(){}
};
class CDerive1 : public CBse
{
public:void f1(){cout << "Derive1" << endl;}
};
class CDerive2 : public CBse
{
public:void f1(){cout << "Derive2" << endl;}
};
void test(CBase &cbase)
{cbase.f1();
}
CBase * test1(int x){if(1==x) return new CDerive1();else {return new CDerive2();}
}
int main()
{CDerive1 test1;CDerive2 test2;test(test1);test(test2);}

关于虚析构函数:

当我们用new创建一个指向子类对象的父类指针时,例如Animal * animal = new Dog()时,其中Animal时父类,Dog是子类,并且delete animal时,其子类对象的析构函数不会被调用,只会调用父类之上的析构函数。所以就会遇到一个问题,如果子类对象有自己独立的堆内存时,这部分内存就无法释放。这时,我们只需要在父类的析构函数上用virtual修饰即可,子类析构函数就会被调用,同时子类的析构函数需要显示定义来释放动态内存。至于原因为什么?可查看该链接C++内存泄露与类继承.

对于delete object;这种形式,如果delete操作的对象的静态类型不同于动态类型,那么该静态类型必须是动态类型的基类,而且该静态类型必须有一个虚析构函数,否则行为未定义。而对于delete[]这种形式,如果静态类型和动态类型不一致,那么行为未定义(即使你静态类型包含虚析构函数,因为对数组用多态就是一个错误)

若不是用new创键的,则均会调用构造函数。

这样定义的话就会涉及到类类型转换问题,通过父类类型的指针或者引用指向子类对象,即这样,指针或者引用到底指向什么呢?指针或者引用指向子类实例。

理解基类和子类之间的类型转换:

通常情况下:引用或指针绑定到一个对象上,引用或指针的类型与对象的类型一样。或者对象的类型含有一个可接受的const类型转换规则。

将基类的指针或引用绑定到子类对象上含义:并不清楚所绑定对象的类型。

静态类型和动态类型:编译时变量的类型,运行时对象的类型。

动态绑定指调用虚函数时才发生。

当用一个子类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动、赋值,子类部分将被忽略。

子类经常对基类的所有虚函数进行声明与定义。继承基类时会重写(覆盖)虚函数放在表中,继承公有,保护成员,隐藏私有成员。如果子类没有覆盖基类的虚函数,则该虚函数也被子类以虚函数继承下来放在子类虚函数表中。

关于虚函数表继承详细情况 。每一个类仅有一个虚函数表。关于子类继承基类时对象组成部分:基类对象,子类对象,更新虚函数表。

将基类的指针或引用绑定到子类对象上含义:可调用基类部分和子类重写基类的虚函数,子类虚函数表。派生类的作用域嵌套在基类的作用域内。

问题是只能调用基类对象部分,但是为何调用基类虚函数(被子类重写)则调用子类的重写虚函数,是如何找到其函数地址的?1.首先调用一个函数,若其不为为虚函数则可直接找到输出,该函数是从基类对象中。若为虚函数,又分两种情况,其是否被重写。是该从子类的虚表中寻找,还是从基类的虚表中寻找呢?若未被重写,则该虚函数会被移到子类虚函数表中,该指针始终指向的是子类对象,所以是从子虚函数表中找,参数名,返回值,函数名均相同。即可。若被重写,还是从子对象的虚函数表中寻找,即可找到对应函数。

也就是说是根据基类的成员,在子类的虚函数表中寻找。从而实现多态。建立虚函数表时就已经确定了。

 

 

这篇关于C++虚函数多态原理-最直白的讲解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1113478

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的