目录
一、前言
二、【面向过程】 与 【面向对象】
三、结构体 与 类
🍎C++中结构体的变化
🍉C++中结构体的具体使用
🍐结构体 --> 类
✨类-----语法格式:
✨类的两种定义方式:
四、类的访问限定符及封装【⭐】
🍓C++中的三类访问限定符
🍌 初探类的封装👈
五、类的实例化
🍊变量的声明与定义
💦类对象的声明与定义
六、类的成员函数--对象模型
🍍成员函数是否存在重复定义?
🍑计算类的大小【结构体内存对齐】
🍇探究类对象的存储方式🔍
🍒空类大小计算【面试考点✒】
七、this指针【⭐重点掌握⭐】
🥝提问:如何区分当前初始化对象?
🍋深度探究this指针的各种特性【原理分析】
🍈this指针的感性理解
🍩一道笔试题✍
八、总结与提炼
九、共勉
一、前言
在学习了一段时间的 C++ 后,感觉之前的基础不牢固,从而导致有点学不下去了😂。但是回头看了看,还是决定坚持一下,并且认真的补一下之前的漏洞。所以从本文开始,我们就要重新认识一下C++中的类和对象了,本文我将总结我的心得,一步步解析从C语言的结构体
struct
到C++的类class
,真正搞懂有关C++的面向对象的三大特征之一 —— 封装
- 作为读者,可能你正在学习C语言,亦或者已经开始学习C++了,也有可能你是一位C++的资深开发者或者其他领域的从业人员。不过这没关系,都不会影响你阅读本文📖
现在,我提出以下这几个问题,看看你是否都了解👇
1️⃣:C++是一门面向对象的语言吗?它和面向过程有什么联系?
2️⃣:面向对象的三大特征为:封装、继承、多态,你真的有去好好了解过什么是类的封装吗?它的好处在哪里?
3️⃣ :类和结构体之间似乎很像,它们存在什么关联吗?
4️⃣:this指针了解多少?存放在哪里?是用来干嘛的?
如果大家还有不清楚的,请一定要让你真的看下去哦,对于C++后面的学习真的很重要。接下来,就让我们带着疑惑,再度出发,好好地探一探这些知识,可能内容会比较多,但我会尽量用生动的语言和图文并茂的方式,结合一些生活中的实际场景,让你更好地理解每个知识点🤔
二、【面向过程】 与 【面向对象】
👉对于C语言而言,它完全是一门【面向过程】的语言。关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题
👉对于C++是基于【面向对象】的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
- 可是呢,C++为了兼容C,并没有完全面向对象,所以你才可以在一些C++的编译器上去写一些C语言的代码
可是 面向过程 和 面向对象 它们之间的区别到底是怎样的呢?可以通过一个在我们生活中最普遍的例子来说明一下
- 若是现在公司要你写一个外卖订餐系统,你呢只会C语言和C++,此时若你使用C语言去写的话关注的就是从用户下单到商家接单,最后到骑手送单这整个过程该如何去实现;
- 但如果你使用的是C++这样具有面向对象的语言去编写的话,那此时你要关注的就是各个对象之间会发生什么关系,对象有【用户】、【商家】、【骑手】这三个,那此时你要考虑的就是用户下单到商家接单,最后到骑手送单,它们之间是谁和谁之间发生关系
三、结构体 与 类
🍎C++中结构体的变化
- 之前在C语言中,我们去定义一个结构体都是这么去写的,例如说这里有一个链表结点的结构体,一个是数据域,一个是指针域
struct ListNode
{int val; // 数据域struct ListNode* next; // 指针域
};
- 在C++中,我们也可以这么去写,上面说到过C++兼容C,可是呢有一处却可以发生变化。也就是在定义这个指针域的时候,可以不需要再写
struct
了
- 通过下面两幅图的对比就可以很清楚地看在C++中确实在使用结构体的时候不需要再去写一遍
struct
这个关键字了,直接使用定义出来的结构体即可;但是在C语言中没有这样的规定,所以是一定要写的
- 不过C++相较于C语言可不只是多了这一点,C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数【但是这在C语言中,是不被允许的】
知道了上面这些,其实就可以回忆我们之前在--数据结构--中写过的很多代码,在C语言的结构体中只是定义了一些成员变量,具体的函数都是写在结构体外,那现在知道了C++可以这么去做的话,是否可以将这些函数都写到结构体内来呢?我们来试试看👇
🍉C++中结构体的具体使用
下面我要使用C++去实现一个栈,来详解C++中的结构体,如果有忘记的小伙伴可以再去回顾一下栈的一些知识
typedef int DataType;
struct Stack
{void Init(size_t capacity){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (nullptr == _array){perror("malloc申请空间失败");return;}_capacity = capacity;_size = 0;}void Push(const DataType& data){// 扩容..._array[_size] = data;++_size;}DataType Top(){return _array[_size - 1];}void Destroy(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}DataType* _array;size_t _capacity;size_t _size;
};
- 可以看到,虽然这个栈是使用C++去实现的,但其实和C语言没有什么大致的区别,只是将这些接口函数放到了结构体中而已。那此时便有同学问:这些变量为什么可以放在下面,不应该在函数的上面就定义出来吗?这点要注意了,这是在一个结构体中,而不是外界的普通程序,不会像我们之前那样需要先定义变量然后再去使用它,编译器需要一个向上查找的过程
- 在C++的结构体中,这个【成员变量】可以定义在结构体 / 类的任何地方,你在何处去进行引用都是可以的
定义出来这么一个栈的结构体之后,我们就可以去使用了👇
- 在C++中,调用一个数据结构的算法接口不是像C语言必须要传入当前定义出来变量的地址,因为这些算法接口直接定义在了结构体中,那一定可以知道这个是属于谁的。所以仔细观察其实可以看出,原本我以C语言实现【栈】的时候在每个算法接口前面都是有ST(stack--栈),但是在C++这一块,我却一个都没有加,这就是因为它们一定是属于【栈】的接口算法,而不是其他数据结构:队列、链表、二叉树
- 那要如何去调用这个接口算法呢,很简单,回忆我们在结构体章节所学习的,如何去访问结构体中的成员,就可以知道是使用
.
这个操作符,然后传入对应的参数即可
int main()
{Stack s;s.Init(10);s.Push(1);s.Push(2);s.Push(3);cout << s.Top() << endl;s.Destroy();return 0;
}
来看一下运行结果💻
⚡总结:
通过上面所写,使用C++去代替实现之前使用C语言写的【栈】时,发现完全没问题,这下你应该可以进一步了解为何C++兼容C了,不过呢在C++中,这样变量和函数存放在一起的结构我们不叫做结构体,而叫做【类】,可是对于类来说,在C++中也不常使用struct这个关键字来定义,而是使用[class]
🍐结构体 --> 类
✨类-----语法格式:
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
【注】:class
为定义类的关键字,ClassName
为类的名字,{}
中为类的主体,注意类定义结束时后面分号不能省略
- 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数
✨类的两种定义方式:
知道了一个类长什么样,接下去我们来聊聊一个类该如何去进行规范的定义
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名
::
- 这也就是我们上面讲到过有关【栈】的这种定义,只需要将
struct
换成class
即可,这种类的定义方式简单粗暴,也是我们平常用得最多的,自己练习代码可以直接这样使用,但其实在日常项目的开发中,不建议大家这样使用❌
重点来讲一讲这一种,这也叫做多文件形式的编写,之间在C语言的学习中我们写的【扫雷】和【三子棋】也是使用的这种分文件编写,如果不了解的读者一定要学会这种思想,在日常企业的开发中是经常使用到的
stack.h
#pragma once#include <iostream>
#include <stdlib.h>
using namespace std;typedef int DataType;
struct Stack
{void Init(size_t capacity);void Push(const DataType& data);DataType Top();void Destroy();DataType* _array;size_t _capacity;size_t _size;
};
stack.cpp
#include "stack.h"void Init(size_t capacity)
{_array = (DataType*)malloc(sizeof(DataType) * capacity);if (nullptr == _array){perror("malloc申请空间失败");return;}_capacity = capacity;_size = 0;
}void Push(const DataType& data)
{// 扩容..._array[_size] = data;++_size;
}DataType Top()
{return _array[_size - 1];
}void Destroy()
{if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}
}
test.cpp
#include "stack.h"int main()
{Stack s;s.Init(10);s.Push(1);s.Push(2);s.Push(3);cout << s.Top() << endl;s.Destroy();return 0;
}
- 以上就是有关C++中类的分文件编写格式,其实和C语言的函数也相差不太多,不过从下图可以看出,似乎是出了点什么问题🤨
- 这就是在上面说到的:成员函数名前需要加类名 ::,我们命名空间的讲解中有说到过有关【作用域】的概念,在C++中,对于一个类体而言,其实就属于一个作用域,将成员变量和成员函数包含在里面。那么此时要在另一个cpp的文件中访问这个类中定义的成员变量的话也就是访问Stack作用域中的内容,就要加上【域作用限定符: :】,就像下面这样
四、类的访问限定符及封装【⭐】
学习了上面的这些,你只是初步了解了什么是类,但是C++中的类远远不止将
struct
换成class
这简单,如果你自己观察的话,可以发现我在上面的Date类中加了【public:】和【private:】这两个东西,它们就叫做类的访问限定符
🍓C++中的三类访问限定符
- 正式来说一说C++中的三类访问限定符【public】【protected】和【private】
- 其中,对于 public 来说指的是公有,表示从当前public到收括号 }; 为止的所有成员变量或者是成员函数均为公有的,什么是公有呢?就是类内类外都可以随意调用访问,不受限制
- private 指的就是私有,这很直观,对于共有来说就是所有东西都是公开的,无论是谁都可以访问;那对于私有来说便是无法访问,谁无法访问呢?这里指的是外界无法访问,但类内还是可以访问的,例如就是类内的成员函数访问这些成员变量是不受限制的
- protected指的是保护,代表它会将类内的这些东西保护起来,外界无法访问到。但对于这个限定来说暂时可以把它当成和 private 是类同的,到了C++中的【多态】才会出现差异
- 光就上面这么说你一定会觉得有些抽象,其实读者可以将这个访问限定符看作是一把【锁】🔒,设想你家里装了一把锁,那么此时锁住的就是外面的人,对家里的人是不会有影响的
接下去再来看看有关访问限定符的一些特性指南
- public 修饰的成员在类外可以直接被访问
- protected 和 private 修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class 的默认访问权限为 private,struct 为 public (因为struct要兼容C)
- 主要还是来看一下这个 第5点,在C语言中,我们在结构体中直接定义出一个成员变量的时候不需要去考虑是否可以访问到,而是直接就去访问了;但是在C++中,我们在访问类中的一个成员变量的时候,是会受到限制的,我们可以来看看
- 可以看出,即使我将类中的
private
去掉的话,还是会存在【不可访问】的现象,原因就是在于类内的定义的内容默认访问权限都是private
,外界是无法访问到的
但一定会有同学有这么一个疑问,那在加上
[private]
关键字后,这个成员变量也是私有的呀,为什么可以对他们去进行一个初始化呢?那不是访问到了这些成员变量了
- 这一点要注意,当我在初始化的时候,并没有直接去访问类内的【成员变量】,而是调用了【成员函数】,在成员函数的内部又调用了类内的【成员变量】,上面有说到过,对于私有的东西虽然类外是不可访问的,但类内还是可以访问的,这个🔒只是锁住了外来入侵者🗡,自己家里的人还是不受限制的
对于上面这一点来说,其实就又一些C++中类的封装思想了,接下去我们就正式来谈谈面向对象的三大特性之一 —— 【封装】
🍌 初探类的封装👈
【封装思想】:用类将对象的属性(数据)与操作数据的方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
- 封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件
- 设想若是没有将电脑中的一些元件给封装起来,就是将内部的一些部件展现在用户的眼前,那么用户每次在将电脑开始的时候都需要那边点一下,这边开个开关,使用起来就会很麻烦,所以可以看出,对于电脑来说,也是存在这么一个封装的思想【很好地将内部的细节给屏蔽起来了,方便管理】
这里先初步地讲一下有关【类的封装】思想,文章的后半部分会不断地加强读者对这块的理解
五、类的实例化
当我们写好一个类之后,就要去把它给定义出来,就好比在C语言中,我们要使用一个变量的话,也是要把它定义出来才行,才可以使用,例如:结构体声明好了之后就要将其定义出来,否则是没用的
🍊变量的声明与定义
- 首先我要问你一个问题,下面的这三个成员变量是已经被定义出来了?还是只是声明呢?
private:int year;int month; // 声明 or 定义 ?int day;
};
- 读者一定要理解【声明】和【定义】的区别,对于声明来说,只是告诉编译器存在这么一个东西,但它不是实际存在于内存中的;而对于定义来说就是实实在在地把空间给开出来,那么此时在内存中就有它的一席之地了
可能就这么说不太好理解,我们通过一个形象一点的案例来说明💬
- 你呢,背井离乡在二线城市当一个程序员💻,工作了几年也赚了不少钱,此时你就想把一直以来的出租屋换成一个崭新的房子,想要在你所处的城市买个房,虽然交不起所有的钱,但首付还是可以的,不过呢还差那么几万块钱,于是呢就想到了你大学时候的室友,也是个铁瓷很要好的朋友,想找他结点钱💴
- 于是就打电话过去说:“兄弟呀,我最近想买个房,交个首付,不过手头上还差个几万块钱,你看你有没有一些不急着用的先借我点,之后赚来了再还给你。”那听到昔日的好友这么一番话,便说:“可以可以,好兄弟开口,那必须帮忙!”于是呢他就这么答应你了,不过也只是口头答应,也就是一种承诺。这个口头答应其实指得就是【声明】,你只能知道会有这么一笔钱给到你,但是这笔钱还没真正到你的手里
- 对于上面的第二种情况,就很像平常在写程序的时候出现链接错误的情况,那就是【声明了但是未定义】的这种行为。之前承诺了、声明了,但是找你要的时候实际却没有
- 对于函数而言就是有声明找不到定义
- 对于变量而言就是这个变量没开空间
- 所以对于这三个成员变量来说只是一个声明,不是定义,并没有开出实际的空间
private:int year;int month; // 仅仅是声明,并没有开出实际的空间int day;
};
那怎样才算定义呢?又是何时开出空间,让我们来瞧瞧【类对象的声明与定义】👇
💦类对象的声明与定义
- 要实际地开出空间来,其实指得就是要将这个类定义出来,因为你的成员变量是声明在类里面的,那你把写好的这个类定义出来后,【成员变量】也就对应的在内存中开出了一块空间,它们是作为一个整体来进行定义的
int main(void)
{Date d; //类对象的实例化return 0;
}
用类类型创建对象的过程,称为类的实例化
1️⃣: 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
- 比如:入学时填写的【学生信息表】📊,表格就可以看成是一个类,来描述具体学生信息
2️⃣:一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
- 这个又怎么去理解呢?这里给读者举一个形象的例子:不知道你是否了解一个建筑物是如何从设计到建成的,重要要经过很多的步骤,但是在一开始建筑师一定是要设计出一张【设计图】来,好对这个房子的整体样式和架构先有一个大致的轮廓,在后面才可以一步一步地慢慢实施建设计划。
- 那其实对于这个类来说就和【设计图】是一样的,比方说现在我们要造一栋别墅🏠,那么一张图纸📑,即一个类中描述的就是这个别墅有几层,多少个房间,门朝哪儿开,是一个大致的框架,不过呢这也就仅仅是一个设计图罢了,还不是一个真实的别墅,不存在具体的空间,因此是不能住人的🛏
- 那要怎样才能住人呢?也就是建筑师通过这张设计图,找几个施工队真实地将别墅设计出来,那才可以真正地住人
- 但平常我们看到的那种高档小区中,可不止一栋这样的别墅,而是有几十栋,难道设计师也要画出几十张设计图才能建完这些别墅吗?当然不是,对于一张设计图来说是可以建造出无数的别墅,只要根据这个设计图来建就行。那上面说到对于设计图来说就是一个类,也就意味着一个类也是可以实例化出多个对象的🐘🐘🐘
- 实例化出这个对象后也就实实在在地将空间给开出来了,那我们上面说到过的【成员变量】,此时也开出了空间,就可以存放对应的数据了
Date d;
d.year = 2023;
d.month = 3;
d.day = 18;
- 但对于下面这种形式去初始化成员变量是不行的,若是还没有定义出一个对象的,成员变量不存在实际空间的,直接用类名去进行访问就会出错,不过后面的文章中我会讲到一种叫做静态成员变量,用
static
进行修饰,是可以的直接使用类名来进行访问的
六、类的成员函数--对象模型
🍍成员函数是否存在重复定义?
- 上面,我们说到了对于一个成员变量来说,若是类没有被定义出来的话它是不存在具体空间的,那在一个类中除了成员变量外,还有【成员函数】,仔细观察可以发现,这似乎就已经把成员函数定义出来了呀,那空间不是已经存在了。 此时是外面再去实例化这个类的话,岂不是会造成重复定义了?
- 可是刚才我们有看过,在实例化出这个Date类的对象时,并没有报出重复定义这个错误,而且在调用这个
Init()
和Print()
函数的时候也不会有什么问题,这是为何呢?难道这个【成员函数】和类没什么关系吗?它存在于类中吗?
让我们带着这个疑问开始本模块的学习
🍑计算类的大小【结构体内存对齐】
要想知道这个类中存在多少东西,其实我们去计算一个它的大小即可
- 还记得结构体内存对齐吗?忘记了就再去看看,下面是对应的规则👇
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 在C语言中,我们有去计算过一个结构体的大小,那上面我们在对于结构体和类做对比的时候说到对于
struct
和class
都可以去定义一个类,那么结构体内存对齐的规则也一样适用。不过我们只会计算成员变量的大小,那就来先计算一下这个【year】、【month】、【day】的大小
- 通过画图以及运行结果可以观察,可以得出类的大小和我们计算的【成员变量】大小竟然是一致的,那【成员函数】呢?没有算上去吗?还是根本不计算在内?
🍇探究类对象的存储方式🔍
在看了上面惊人的一幕后,我们就来思考一下,对于这个类对象究竟是如何进行存储的。在下面,我给出了类对象存储方式的三种设计方式,你认为哪一种设计方式是正确的呢?
- 首先是第一种,也就是将类的【成员变量】和【成员函数】都存放在一起,其实对于每个类的成员变量来说都是不一样的,都有它们不同的空间,可调用的是同一份函数。如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
- 下面是第二种设计方式。代码只保存一份,在对象中保存存放代码的地址,这种方式似乎看起来不错,你认为可行吗?
- 再来看看第三个设计方案。可以看到对于【成员变量】和【成员函数】完全就是分离了,存在了一个叫做公共代码区的地方,类的所有函数都放在一个类函数成员表中
- 对于每一个对象来说都是互相独立的,里面只存放了各自的成员变量,而要找成员函数的话就要通过当前这个类的对象去公共代码区进行调用
1️⃣:答案揭晓,那就是最后这一种,实现了成员与函数的分离,为什么要这么设计呢?上面其实有提到过,虽然每个对象的成员变量是不同的,各自各自的空间,但是对于成员函数来说,大家都是一样的,例如这个Init()函数,外界被定义出来的对象只需要调用一下这个函数去初始它自己的成员变量即可,不需要将其放在自己的类内。
2️⃣:设想若是每个类中都写一个这样相同函数的话,此时每个对象就会变得非常庞大,也就是我不去调用这个函数,只是将对象定义出来函数的空间就已经会存在了,这样的设计其实是不好的,所以我们应该采取第三种做法
感性理解:私有场所与共有场所
但是这么说一定是比较抽象了,我们再通过一个生活小案例来理解一下
- 就用刚才说到的这个别墅小区好了,那在每栋别墅里面都是房间的,像客厅、卧室、厨房、洗手间,每家每户基本都有,但是呢每一家都有它们自己家庭的设计,既然是个人别墅,那么一定不可能每栋房子的客厅、卧室、厨房、洗手间都在同一个位置吧,那就太单调了╮(╯▽╰)╭,这些房间呢值得就是【成员变量】
- 那在一个小区中,除了挨家挨户的的私人领域外,一定会存在公共区域,在这些公共区域中,会有一些公共场所,例如像篮球场、咖啡馆、游泳馆、小卖部或是健身器材等等,对于这个公共区域你可以理解为【公共代码区】,而对于这些公共场所、设施你可以理解为【成员函数】
那其实这就很形象了,【成员变量】是每个对象各自的,由于类的封装特性别人无法轻易访问,可是呢对于这个【成员函数】来说,是大家共有的,可以一起使用,所以不需要放在自己家里,除非你真的很有钱,在一个别墅小区再自己建一些私人的游泳池、篮球场和小卖部👈
🍒空类大小计算【面试考点✒】
- 学习了如何去计算一个类之后,接下去请你来判别一下下面三个类的大小分别为多少
// 类中既有成员变量,又有成员函数
class A1 {void f1() {}
private:int a;
};// 类中仅有成员函数
class A2 {void f1(){}
};// 类中什么都没有---空类
class A3 {};
- 首先是类
A1
,有一个成员变量,那经过上面的学习可以得知成员函数是不存在于类中,又因为整型占4个字节,所以很容易可以得知A1的大小为4
- 接下去对于类
A2
,只有一个成员函数f1()
,没有成员变量,那【sizeof(A2)】的结果会是多少呢?一会看运行结果后再来分析
- 接下去是类
A3
,对于这个类来说既没有成员函数也没有成员变量,那它的大小会是多少呢?0吗?
我们来看一下运行结果
- 通过观察可以得知,似乎只算对了第一个类A1的大小,但是前两个类的大小为什么都是1呢?这相信读者也是非常疑惑吧?立马为你来解答👇
一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。但是对于空类的大小却不太一样,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象【这1B不存储有效数据,为一个占位符,标识对象被实例化定义出来了】
⚡上面的这个概念在笔试面试中都有可能会涉及,准备校招的同学要重视 ⚡
七、this指针【⭐重点掌握⭐】
🥝提问:如何区分当前初始化对象?
- 继续来回顾一下上面所写的Date日期类,有三个成员变量和两个成员函数
class Date {
public://定义void Init(int year, int month, int day){_year = year;_year = month;_year = day;}void Print(){cout << "year:" << _year << endl;cout << "month:" << _year << endl;cout << "day:" << _year << endl;}
private:int _year; //仅仅是声明,并没有开出实际的空间int _month;int _day;
};
- 那现在我定义出一个变量后开始传递数据,然后初始化 d1 里面的【year】【month】【day】,然后在内部Init()函数中使用 _year = year 这样的方式来进行初始化,此时右边的[year]是外部传递进来的2024,[_year]是内部的成员变量,但是仔细回忆一下,刚才我们有说到这个[_year]只是类内部声明的,并没有被定义出来呀,那要怎么赋值初始化呢?
- 有同学说:外面不是定义出这个对象d1了,那么三个成员变量的空间自然也就开出来了,是的,这没错
Date d1;
d1.Init(2024, 4, 13);
- 可是现在呢,我又定义了一个对象,此时就会存在两个对象 d1 和 d2 ,然后分别去调用这个Init()函数来初始化自己的成员变量,那外部传入实参的时候是可以分清的,但是传入到内部时_year = year中的[_year]要怎么区分这是d1还是d2的成员变量呢?若有又定义了一个 d3 呢?如何做到精准赋值无误?
- 在外部定义出来的对象调用的时候可以很明确是哪个对象调的,但是到了函数内部又是辨别的呢?对于成员函数来说存放在公共代码区,大家都可以调用,那即使调用了也没有传入当前对象的地址呀,函数内部怎么知道要给哪个对象初始化成员变量呢?
好,就让我们带着这些问题,进入下一小节的学习📖
Date d1;
Date d2;d1.Init(2024, 4, 13);
d2.Init(2024, 4, 14);
🍋深度探究this指针的各种特性【原理分析】
面对上面情况,其实就可以使用到 C++ 中的
this 指针
了,这个我在上面有提过一嘴,还有印象吗
- 上面讲了这么多不知读者是否关注到我说的一点:外界无法传入当前对象的地址给到被调用的成员函数
- 那我现在要说的是,其实这件事情是做了的,当这个成员函数被调用的时候,编译器就会自动给在这个函数的最前面加上一个形参,他就是专属于当前类的一个指针,就是
this 指针
//void Init(int year, int month, int day)
void Init(Date* this, int year, int month, int day)
- 那么形参部分改变了,实参也需要修改,那要传递什么呢?没错,就是当前对象的地址
//d1.Init(2023, 3, 18);
d1.Init(&d1, 2023, 3, 18);
- 那么当this接受了当前对象的地址之后,编译器就将代码转换成了下面这种形式,【this】在英文单词中指的就是当前,那么意思就很明确了,为当前对象的year、month 和 day 进行初始化。随着每次的传入的对象地址不同,this指针就会通过不同的地址去找到内存中对应的那块地址中的成员变量,进行精准赋值
- 不过通过观察可以发现,似乎我们自己去加上这一个参数好像是行不通,编译器报出了很多错误,
看看下面这段话就知道为什么了👇
- C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数(this),让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成
- 所以这个this指针我们是不可以加的,编译器会自动帮我们加上,并且传递当前对象的地址
了解了this指针的基本原理后,我们来聊聊它的相关特性
1️⃣:this指针 的类型:类类型* const(Date* const),即成员函数中,不能给this指针赋值
- 对于this指针来说,是被 const 常所修饰的,为【指针常量】,对于指针本身的指向是不可修改的,但是指针所指向的内容可以通过解引用的方式来修改。如果不是很清楚这一块可以看看【指针常量】与【常量指针】
2️⃣:只能在“成员函数”的内部使用
- 这一点要牢记,对于 this 指针 而言,只可以在类的成员函数内部进行使用,是不可以在外部进行使用的,因为它是作为一个成员函数的形参,若是没有传递给当前对象地址的话,那么它的指向是不确定的,但当进入成员函数内部时,编译器底层一定调用了这个this指针,为其传递了对象的地址,此时在内部再去使用的话是不会有问题的
3️⃣: this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
- 这一点上面也有强调过,this指针是作为形参来使用,那对于函数形参,我们在函数栈帧中有深入研究过它是需要进行压栈的,那就要建立函数栈帧,可以很明确它(this)就是存放在栈区的,而不是存放在对象中。
4️⃣:this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
🍈this指针的感性理解
说到了这么多有关this指针的特性,有些特性对大家来说可能还是比较难以理解,接下去我会通过两个生活中的小场景带你好好理解一下😄
- 夏天到了,呆在家里一定会很热,一天到晚打空调对身体又不好,此时就会想到去游泳馆游泳,那去游泳的话肯定要换上专门的衣物,去哪里换呢?当然是更衣室了,有过经历的同学一定知道当你去更衣室换衣服的时候,前台就会给你一个手环,可以识别感应里面的柜子,一个人一个柜子可以放置自己的私人物品。然后就把这个手环套在你的手上,最后当你游完泳后要怎么知道那个是你的柜子呢?那是通过这个手环来进行感应打开柜门取出自己的衣物【这个手环就是用来识别的,别人的手环打不开你的柜子】
- 住过小区的一定都知道,现在的小区管理制度是越来越严了,出入呢都需要这个门禁卡,才可以证明你的身份,是属于这个小区的,防止外来人员入室盗窃,所以这个门禁卡就是你身份的象征【有没带门禁进不去单元门的吗🐕】
通过上面的两个生活小案例,相信你对this指针一定有有了自己的理解
🍩一道笔试题✍
💬下面程序段包含4个函数,其中具有隐含this指针的是( )
int f1();
class T
{public:static int f2();private:friend int f3();protect:int f4();
};
【答案】:f4
【解析】:
- f1为全局函数,不是类的成员函数,因此没有this指针
- f2为static函数,放在公共代码段,不属于类,因此没有this指针
- f3为友元函数,不属于类,因此没有this指针
- f4为成员函数,在类T中具有保护权限,因此有this指针
好,接下去就对上面的笔试和面试题所引申出来的知识点做一个总结与回顾,我们就进入下一模块
【总结一下】:
- 对于this指针而言,是存放在【栈区】的,虽然其是属于成员函数的一个隐式形参,但是却不和成员函数一样存放在【公共代码区】,对于成员函数而言只是编译器将其解析后的汇编指令存放在公共区罢了,而对于函数内部的形参和临时变量,都还是存放在栈区的,是需要开辟栈帧、压栈的↓
- this指针可以为空,但能不能正常运行取决于你如何去调用,仅仅是传递了空对象地址但没有进行解引用的话不会出现问题。但若是在成员函数内部访问成员变量的话,无论你有无给出this->,都会造成解引用从而导致空指针异常⚠的问题。
八、总结与提炼
到这里,就讲完了类的封装思想,我们来总结回顾一下📖
- 首先我们聊到了【面向过程】与【面向对象】之间的区别,初步感受了C语言和C++两门语言特性的不同。为了引出C++中的类,便对比了
struct
和class
这两个关键字,知道了原来在C++中结构体可以这么玩,并且二者都可以用在来定义一个类
- 接下去呢我们就正式开始聊C++中的类,说到了三种访问限定符,
puiblc
、protected
和private
,若是加上了访问限定符后,类内的成员对外部来说就存在不同的限制。初次讲到了类的封装思想,将对象的属性(数据)与操作数据的方法结合在一块,起到了更好地管理
- 写好一个类后,就可以用它定义出一个对象了,在这一模块,通过形象地案例向读者展示了【声明】和【定义】之间的区别,如果要将一个东西定义出来,就要实实在在为其开出一块空间
- 将类实例化后,就可以通过这个对象去调用类中的向外提供的公有成员函数来说来操控私有成员变量,但是在计算类的大小时却发现【成员函数】似乎并不计算在内,通过探究发现原来其存在于一个公共代码区,为什么减少类内部的负担,将大家都要使用的东西像小区中的公共设施一般放在一个【公共代码区】,这样谁要用的时候通过对应的地址找到然后去调用一下即可。可是成员函数内部要如何知道是哪个对象来调用的我呢?
- 此时我们谈到了一个重要知识点 ——
this指针
,随着每次的传入的对象地址不同,隐式形参this指针就会通过不同的地址去找到内存中对应的那块地址中的成员变量,进行精准赋值
九、共勉
以下就是我对C++ 封装思想的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++ 类和对象的理解,请持续关注我哦!!!