c++面向对象之封装、继承、和多态

news2024/12/22 22:52:48

一、封装

把客观事物封装成类,而且可以把自己的数据和方法设置为只能让可信的类或者对象操作,对不可信的信息进行隐藏(利用public,private,protected,friend)实现

二、继承

2.1类与类的关系

  • has-a :描述一个类由多个部件类构成,一个类的成员属性是另一个已经定义好的类。
  • use-a:一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现。
  • is-a:继承,关系具有传递性

2.2继承

让某个类型对象获得另一个类型对象的方法,可以使用现有类的所有功能。有三种继承方法:

2.2.1 实现继承

使用基类的属性,不需要额外功能

2.2.2 接口继承

仅使用属性和方法的名称,子类提供实现的能力

2.2.3 可视继承

子类使用基类外观和实现代码的能力(C++不常用)

2.3 继承机制中的对象之间如何转换?指针和引用之间如何转换?

  • 将派生类指针或引用转换为基类的指针或者引用被称为向上类型转换,向上类型转换会自动进行,而且是类型安全的。
  • 将基类指针或者引用转换为派生类指针或者引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,向下不知道会对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术,RTTI技术,用dynamic_cast向下类型转换。

2.4 基类

如果想要将某个类用作基类,为什么这个类必须定义而非声明?

派生类中会包含并且使用从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。

什么是虚拟继承?

代码来源阿秀的学习笔记

#include <iostream>
using namespace std;

class A{}
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};

int main()
{
    cout << "sizeof(A):" << sizeof A <<endl; // 1,空对象,只有一个占位
    cout << "sizeof(B):" << sizeof B <<endl; // 4,一个bptr指针,省去占位,不需要对齐
    cout << "sizeof(C):" << sizeof C <<endl; // 4,一个bptr指针,省去占位,不需要对齐
    cout << "sizeof(D):" << sizeof D <<endl; // 8,两个bptr,省去占位,不需要对齐
}

B和C虚拟继承A,D公有继承B和C,这种方式是一种菱形继承或者钻石继承。虚拟继承情况下,无论基类被继承多少次,只会存在一个实体。所以D类只会包含一个A类对象,避免重复成员出现,减少了内存使用。 虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr。如果既存在vptr又存在bptr,那么编译器会将其优化,合并为一个指针

当一个类使用虚拟继承,它的构造函数需要显示调用虚基类的构造函数,同时虚基类的构造函数会在最底层的派生类构造函数中进行调用,而不是在中间类的构造函数中调用。

2.5 抽象基类为什么不能创建对象?

因为抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。

2.6 多继承

什么是多继承

C++允许为一个派生类指定多个基类,这样的继承结构被称为多重继承。

优点

对象可以调用多个基类中的接口。

缺点

派生类所继承的多个基类有相同的基类,派生类对象需要调用这个祖先类的接口方法,就会出现二义性。

解决方法

  • 加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝
  • 虚拟继承

2.7 public,private,protected访问权限

图片来源阿秀的学习笔记

在这里插入图片描述
在这里插入图片描述

三、多态

同一事物表现不同事物的能力。允许将子类类型指针赋值给父类类型指针
多态有两种-重载和虚函数。

3.1 实现多态的两种方法

1、重载(编译时多态)

编译时多态,允许存在多个同名函数,而这些函数的参数表不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数(参数个数或者参数类型不同)

重载是在同一范围定义中的同名成员函数才存在重载关系重载和函数成员是否是虚函数没有关系

2、虚函数(覆盖)(重写)(编译时多态)

运行时多态。子类重新定义父类的虚函数。在基类的函数前加上virtual关键字,在派生类中重写该函数,运行的时候根据所指对象是基类还是基类还是派生类来调用里面的函数。

注意

  • 与基类的虚函数有相同的参数个数
  • 与基类的虚函数有相同的参数类型
  • 与基类的虚函数有相同的返回值类型

final和override关键字

  • override
    这个关键字的意思是,这个子类的这个函数是重写父类的,如果不小心打错了名字,编译是不会通过的
  • final
    不希望某个类被继承或者不希望某个虚函数被重写,可以在类名和虚函数后面添加final关键字。添加final关键字之后如果被继承或者重写,编译器会报错
    代码来源阿秀的学习笔记
class Base
{
    virtual void foo();
};
 
class A : public Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
};

虚函数底层实现原理

虚表:类中含有virtual关键词时,系统自动生成虚表
虚表指针:含有虚函数的类实例化对象时,对象地址前四个字节存储指向虚表的指针

所以构造函数不能定义为虚函数,因为虚函数对应一个虚函数表,类中存储一个虚表指针,而此时还没有初始化,所以没有虚表指针,无法找到虚表。

虚表的特征

  • 全局共享,也就是全局只有一个,在编译的时候就构造完成。
  • 虚表类似于一个数组,类对象中存储vptr指针,指向虚函数表。
  • 虚表存储的是虚函数的地址即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译的时候就可以确定,虚函数表的大小是在编译的时候确定的,不用动态分配内存空间。

虚函数表存放位置

虚函数表类似于类中的静态成员的变量,静态成员变量也是全局共享。
测试结果显示:虚函数表vtable在Linux中存放在可执行文件的只读数据段(也就是常量区)中;微软编译器将虚函数表存放在常量段。虚函数位于代码区

虚函数实现多态的过程

  1. 编译器发现基类中有虚函数,自动生成一个虚函数表,这个是一个一维数组,虚表保存虚函数入口地址
  2. 编译器会在每个对象前四个字节中保存一个虚表指针,构造时,根据对象类型初始化虚表指针。
  3. 派生类定义对象的时候,程序运行会自动调用构造函数,构造子类对象的时候,先调用父类构造函数,然后才调用子类的构造函数,为子类对象初始化虚表指针,令他指向子类虚表
  4. 派生类对基类没有重写的时候,派生类虚表指针指向基类的虚表,重写的时候,指向自身的虚表

构造函数或者析构函数能否调用虚函数?

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类型,同样进入基类析构函数时,对象也是基类类型。所以,虚函数始终仅仅调用基类的虚函数,不能达到多态的效果。

3、重载和重写的区别

  • 重写是父类和子类的垂直关系,重载是不同函数之间的水平关系。
  • 重写要求参数列表相同,重载则要求参数列表不同,返回值不要求。
  • 重写关系中,调用方法根据对象类型决定,重载根绝调用的时候实参表与形参表对应关系来选择函数体

4、隐藏

隐藏指的是子类隐藏父类的函数(还存在),具有以下特征

  • 子类函数与父类名称相同,但是参数不相同,父类函数被隐藏
  • 子类函数与父类函数名称相同,参数也相同,但是父类函数没有virtual,父类函数被隐藏
  • 两个函数参数相同,但是基类函数不是虚函数,和重写的区别在于基类函数是否是虚函数
  • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中
举例一

代码来源C++中函数重载、隐藏、覆盖和重写的区别

#include <iostream>
using namespace std;

void func(char* s){
    cout<<"global function with name:"<<s<<endl;
}

class A{
    void func(){
        cout<<"member function of A"<<endl;
    }
public:
    void useFunc(){
        //func("lvlv");//A::func()将外部函数func(char*)隐藏
        func();
        ::func("lvlv");
    }
    virtual void print(){
        cout<<"A's print"<<endl;
    }
};

class B:public A{
public:
    void useFunc(){          //隐藏A::vodi useFunc()
        cout<<"B's useFunc"<<endl;
    }
    int useFunc(int i){      //隐藏A::vodi useFunc()
        cout<<"In B's useFunc(),i="<<i<<endl;
        return 0;
    }
    virtual int print(char* a){
        cout<<"B's print:"<<a<<endl;
        return 1;
    }

    //下面编译不通过,因为对父类虚函数重写时,需要函数返回值类型,函数名称和参数类型全部相同才行
    // virtual int print(){
        // cout<<"B's print:"<<a<<endl;
    // }
};

int main(){
    A a;
    a.useFunc();
    B b;
    b.useFunc();//A::useFunc()被B::useFunc()隐藏
    b.A::useFunc();
    b.useFunc(2);
    //b.print();//编译出错,A::print()被B::print(char* a)隐藏
    b.A::print();
    b.print("jf");
}

程序运行结果

程序执行结果: 
member function of A
global function with name:lvlv
B's useFunc
member function of A
global function with name:lvlv
In B's useFunc(),i=2
A's print
B's print:jf
举例二

代码来源C++中的覆盖与隐藏(详细讲解)

class father
{
public:
    void show1()
    {
        cout << "father::show1" << endl<< endl;
    }

    virtual void show2()
    {
        cout << "father::show2" << endl << endl;
    }
};

class son:public father
{
public:
    void show1()
    {
        cout << "son::show1" << endl<< endl;
    }

    virtual void show2()
    {
        cout << "son::show2" << endl << endl;
    }
};
int main(){  
//基类指针指向派生类对象的时候,基类指针可以直接调用派生类的覆盖函数,也可以通过::调用基类被覆盖的虚函数
//而基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数
	father f;
	son s;  
	father *pf=&s;  
	son *ps=&s;  
	pf->show1();  //father::show1
	pf->show2();   //son::show2
	return 0;
}
  • 因为show1是非virtual函数,调用它的对象类型为静态类型即父类(静态联编),所以调用的是父类的对象
  • 但是show2为virtual函数,调用它的对象类型为动态类型即指针指向的类型(动态联编),所以调用的是子类的类型

3.2 虚函数和纯虚函数

虚函数和纯虚函数都是用于实现多态的机制,它们都支持动态绑定的特性,但二者有着一些区别:

  • 实现方式不同。虚函数需要在基类中有一个默认的实现,派生类可以选择重载该函数或者使用默认的实现,而纯虚函数没有默认的实现,必须在派生类中进行实现才能使用。
  • 使用场景不同。虚函数应被定义为默认的实现和在派生类中重新实现的继承函数,它们让继承树中不同的实现起到相同的作用,提高代码重用性;而纯虚函数则常被用作通用接口或抽象类,提供一种规范的实现方式,以确保子类中的实现得到规范化。
  • 不同的初始化方式。派生类中的虚函数在默认没有被重载时和基类中的虚函数都指向同一个实现,这个实现允许在对象构造时调用;而纯虚函数不能在基类中被调用,因为其没有实现,只作为规范的存在。如果执行的代码需要实现纯虚函数,这时只能从派生类构造函数中调用。
  • 对于纯虚函数,它们在基类中充当抽象角色。在实际中,纯虚函数常常适用于基类,因为基类其实是一个不能被实例化的抽象类,它只包含一些接口,让继承它的派生类去实现这些接口;而虚函数则适用于一个可以被实例化的类,它通过继承实现了多态性的特性。
  • 纯虚函数没有具体的函数体,在虚表中的值为0,而具有函数体的虚函数则是函数的具体地址

3.3 虚函数的代价

  • 每一个带有虚函数的类会产生一个虚函数表,用来存储指向虚成员函数的指针
  • 带有虚函数的类的每一个对象会有一个指向虚表的指针,增加对象的空间大小
  • 虚函数不能在世内联函数。因为内联函数在编译阶段进行替代,而虚函数在运行阶段才能确定用哪种函数,虚函数不能是内联函数。

3.4 哪些函数不能是虚函数?

  • 构造函数:派生类必须知道基类干了什么,才能进行构造;当有虚函数的时候,每一个类有一个虚表,对象有虚表指针,虚表指针在构造函数中初始化
  • 内联函数
  • 静态函数:静态函数不属于对象属于类,没有this指针,设置为虚函数没有意义
  • 友元函数、普通函数:不属于成员函数,不能被继承

3.5 静态绑定和动态绑定

  • 静态绑定:绑定的是静态类型(对象在声明时采用的类型,在编译器确定),发生在编译器
  • 动态绑定:绑定动态类型(通常是指一个指针或引用目前所指对象的类型,是在运行期决定的),所对应的函数或属性依赖于对象的动态类型,发生在运行期。

建议:绝对不要重新定义继承而来的非虚函数,因为这样重写的时候是没有多态的,这样会给程序留下不可预知的隐患和莫名其妙的bug,而且动态绑定时,要注意默认参数的使用,当缺省参数和virtual函数一起使用的时候要谨慎。

引用是否可以实现动态绑定

引用在创建的时候必须初始化,在访问虚函数的时候,编译器会根据绑定的对象类型决定要调用哪个函数(只能调用虚函数)。

3.6 如何防止一个类被实例化?

  • 将类定义为抽象基类或者将构造函数声明为private
  • 不允许外部创建类对象,只能在类内部创建对象(static)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/493398.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringCloud全面学习笔记之初尝美妙篇

目录 前言初识微服务单体架构分布式架构微服务架构初见SpringCloud微服务治理分布式服务架构案例 微服务组件及使用Eureka注册中心提供者和消费者Eureka的结构和作用搭建Eureka服务注册服务服务发现Eureka注册服务总结 Ribbon负载均衡原理负载均衡原理负载均衡策略懒加载 Nacos…

Qt quick基础2(包含平移旋转放缩以及qml控件大写开头啊)

Qt quick基础2&#xff08;包含平移旋转放缩以及qml控件大写开头啊&#xff09; 目录 Qt quick基础2&#xff08;包含平移旋转放缩以及qml控件大写开头啊&#xff09;前言简单的平移、旋转和放缩其他元素的一些基本使用qml文件作为控件时&#xff0c;务必以大写字母开头命名小结…

力扣题库刷题笔记682-棒球比赛

1、题目如下&#xff1a; 2、个人Python代码实现如下&#xff1a; 代码如下&#xff1a; class Solution: def calPoints(self, operations: List[str]) -> int: i 0 #用于遍历元素的下标 while i < len(operations): …

【Python入门篇】——Python基础语法(数据类型与数据类型转换)

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; Python入门&#xff0c;本专栏主要内容为Python的基础语法&#xff0c;Python中的选择循环语句…

数据结构与算法导学

文章目录 数据结构和算法导学认识数据结构认识算法总结 数据结构和算法导学 程序 数据结构 算法 认识数据结构 什么是数据结构&#xff1f; 数据结构是一门研究计算机中数据存储和数据操作的学科。 为什么要学习数据结构&#xff1f; 学习数据结构能让我们写出更加优秀的代码…

关于在线帮助中心你需要思考以下几个问题

搭建帮助中心是大多数企业都在尝试做的事情&#xff0c;它的重要性对于企业来说不言而喻。现在对于企业来说&#xff0c;搭建帮助中心或许不是什么难事&#xff0c;但是关于帮助中心&#xff0c;有几个问题需要思考清楚&#xff0c;才能让其发挥最大的价值。 一、如何让用户养成…

CAS 原子操作类

CAS 原子类 java.util.concurrent.atomic 是什么 CAS compare and swap的缩写&#xff0c;中文翻译比较并交换&#xff0c;实现并发算法时常用的一种技术 它包含三个操作数–内存位置、预期原值及更新值 执行CAS操作时&#xff0c;将内存位置的值与预期原值比较 如果相匹…

网络协议与攻击模拟-05-ICMP协议

ICMP 协议 1、理解 ICMP 协议 2、理解 ICMP 重定向 3、会使用 wireshark 分析 ICMP 重定向流量实验 一、 ICMP 基本概念 1、 ICMP 协议 Internet 控制报文协议&#xff0c;用于在 IP 主机、路由器之间传递控制消息&#xff0c;控制消息指网络通不通、主机是否可达、路由是否…

荔枝派Zero(全志V3S)驱动开发之hello驱动程序

文章目录 前言一、设备驱动分类二、字符设备驱动简介三、字符设备驱动开发1、APP打开的文件在内核中如何表示2、编写驱动程序的步骤3、hello 驱动程序编写<1>、试验程序编写<2>、测试程序编写<3>、编写 Makefile<4>、编译 3、运行测试<1>、上传程…

PyTorch实战4:猴痘病识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;365天深度学习训练营-第P4周&#xff1a;猴痘病识别&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 目录 一、搭建CNN网络结构1、原文网络结构1.1、网络…

4。计算机组成原理(2)存储系统

嵌入式软件开发&#xff0c;非科班专业必须掌握的基本计算机知识 核心知识点&#xff1a;数据表示和运算、存储系统、指令系统、总线系统、中央处理器、输入输出系统 这一部分主要讲解了CPU的组成和扩容、CPU与存储器&#xff08;主存、辅存、缓存&#xff09;的连接 一 存储…

C++笔记——第十六篇 异常

目录 1.C语言传统的处理错误的方式 2. C异常概念 3. 异常的使用 3.1 异常的抛出和捕获 在函数调用链中异常栈展开匹配原则 3.2异常安全 4.异常的优缺点 1.C语言传统的处理错误的方式 传统的错误处理机制&#xff1a; 1. 终止程序&#xff0c;如assert&#xff0c;缺陷&a…

飞腾ft2000-麒麟V10-SP1安装Docker、运行gitlab容器

目录 一、安装及配置docker 1、卸载docker相关包及删除相关配置文件 2、安装二进制docker 1.下载软件包 2.解压 3.修改镜像加速地址 4.修改profile文件 5.启动docker 6.docker常用命令 二、安装并启动gitlab镜像 1.安装gitlab镜像 1.查询满足使用需求的gitlab版本 2…

很佩服的一个Google大佬,离职了。。

这两天&#xff0c;科技圈又有一个突发的爆款新闻相信不少同学都已经看到了。 那就是75岁的计算机科学家Geoffrey Hinton从谷歌离职了&#xff0c;从而引起了科技界的广泛关注和讨论。 而Hinton自己也证实了这一消息。 提到Geoffrey Hinton这个名字&#xff0c;对于一些了解过…

使用 Mercury 直接从 Jupyter 构建 Web 程序

动动发财的小手&#xff0c;点个赞吧&#xff01; 有效的沟通在所有数据驱动的项目中都至关重要。数据专业人员通常需要将他们的发现和见解传达给利益相关者&#xff0c;包括业务领导、技术团队和其他数据科学家。 虽然传达数据见解的传统方法&#xff08;如 PowerPoint 演示文…

Oracle SQL优化相关数据项

要掌握SQL调优技术,就需要能读懂SQL语句的执行计划,要想读懂SQL语句的执行计划,不仅需要准确理解SQL语句执行计划中各操作及其含义,还需要准确理解SQL语句执行计划中各数据项的含义。本书第7章中,已经对SQL语句执行计划中各个操作的含义做了详尽的阐述,本章中,我们将对S…

爱普特APT32F110x系列时钟介绍

最近要用APT32F110x做一些开发&#xff0c;顺便学习一下。 APT32F110x 是由爱普特推出的基于平头哥&#xff08;T-Head Microsystems&#xff09;CPU 内核开发的 32 位高性能低成本单片机。 APT32F1104x基于嵌入式 Flash 工艺制造&#xff0c;内部丰富的模拟资源&#xff0c;包…

ShardingJDBC核心概念与快速实战

目录 ShardingSphere介绍 ShardingSphere特点 ShardingSphere简述 ShardingSphere产品区分 ShardingJDBC实战 核心概念 实战 ShardingJDBC的分片算法 ShardingSphere目前提供了一共五种分片策略&#xff1a; 分库分表带来的问题 ShardingSphere介绍 ShardingSphere特…

结合SSE实现实时位置展示与轨迹展示

概述 实时位置与实时轨迹的展示是webgis中非常常见的一个功能&#xff0c;本文结合SSE来实现实现此功能。 SSE简介 SSE是Sever-Sent Event的首字母缩写&#xff0c;它是基于HTTP协议的&#xff0c;在服务器和客户端之间打开一个单向通道&#xff0c;服务端响应的不再是一次性…

车牌输入框 封装 (小程序 vue)

车牌输入框 封装 小程序licenseNumber.jslicenseNumber.jsonlicenseNumber.wxmllicenseNumber.wxss样例 vuevnp-input-box.vuevnp-input.vuevnp-keyboard.vue样例 小程序 licenseNumber.js const INPUT_NUM 8;//车牌号输入框个数 const EmptyArray new Array(INPUT_NUM).fi…