C++ | 你真的学懂了类和对象的封装思想吗?

C++ | 你真的学懂了类和对象的封装思想吗?_第1张图片

在这里插入图片描述

文章目录

  • 一、前言
  • 二、面向过程与面向对象
  • 三、结构体与类
    • 1、C++中结构体的变化
    • 2、C++中结构体的具体使用
    • 3、结构体 --> 类
      • 类的两种定义方式
      • 成员变量命名规则
  • 四、类的访问限定符及封装【⭐】
    • 1、C++中的三类访问限定符
    • 2、初探类的封装
  • 五、类的实例化
    • 1、变量的声明与定义 - - 铁瓷还会铁吗?
    • 2、类对象的声明与定义 - - 别墅设计图
  • 六、类对象模型
    • 1、成员函数是否存在重复定义?
    • 2、计算类的大小【结构体内存对齐】
    • 3、探究类对象的存储方式
      • 感性理解:私有场所与共有场所
    • 4、空类大小计算【面试考点✒】
  • 七、this指针【⭐重点掌握⭐】
    • 1、提问:如何区分当前初始化对象?
    • 2、深度探究this指针的各种特性【原理分析】
    • 3、this指针的感性理解
    • 4、两道夺命面试题
      • this指针存放在哪里?
      • this指针可以为空吗?
  • 八、C和C++实现栈的对比
    • 1、代码展示
    • 2、C语言特性分析
      • C语言语法的松散性 - - 过红绿灯还是慢一点吧
    • 3、C++的特性优势分析
      • 再谈类的封装思想 - - 兵马俑还是保护起来吧
  • 九、总结与提炼

一、前言

从本文开始,我们就要正式来学习C++中的类和对象了,本文我将带你一步步从C语言的结构体struct到C++的类class,真正搞懂有关C++的面向对象的三大特征之一 —— 封装

  • 作为读者,可能你正在学习C语言,亦或者已经开始学习C++了,也有可能你是一位C++的资深开发者或者其他领域的从业人员。不过这没关系,都不会影响你阅读本文
  • 可能你了解过面向对象的一些语言,像Java、C#、python这些,也知道C++里面也有面向对象的一些思想,但是呢为何又可以写一些C语言的代码,C语言大家一定都学过,是一门面向过程的语言,可是为何C++也可以跑C语言的代码呢?

现在,我提出以下这几个问题,看看你是否都了解

  • C++是一门面向对象的语言吗?它和面向过程有什么联系?
  • 面向对象的三大特征为:封装、继承、多态,你真的有去好好了解过什么是类的封装吗?它的好处在哪里?
  • 类和结构体之间似乎很像,它们存在什么关联吗?
  • this指针了解多少?存放在哪里?是用来干嘛的?

接下去,就让我们带着疑惑,再度出发,好好地探一探这些知识,可能内容会比较多,但我会尽量用生动的语言和图文并茂的方式,结合一些生活中的实际场景,让你更好地理解每个知识点

二、面向过程与面向对象

对于C语言而言,它完全是一门【面向过程】的语言。关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题

C++ | 你真的学懂了类和对象的封装思想吗?_第2张图片
对于C++是基于【面向对象】的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

C++ | 你真的学懂了类和对象的封装思想吗?_第3张图片

  • 可是呢,C++为了兼容C,并没有完全面向对象,所以你才可以在一些C++的编译器上去写一些C语言的代码

可是面向过程和面向对象它们之间的区别到底是怎样的呢?可以通过一个在我们生活中最普遍的例子来说明一下

  • 若是现在公司要你写一个外卖订餐系统,你呢只会C语言和C++,此时若你使用C语言去写的话关注的就是从用户下单到商家接单,最后到骑手送单这整个过程该如何去实现;
  • 但如果你使用的是C++这样具有面向对象的语言去编写的话,那此时你要关注的就是各个对象之间会发生什么关系,对象有【用户】、【商家】、【骑手】这三个,那此时你要考虑的就是用户下单到商家接单,最后到骑手送单,它们之间是谁和谁之间发生关系

C++ | 你真的学懂了类和对象的封装思想吗?_第4张图片

三、结构体与类

1、C++中结构体的变化

  • 之前在C语言中,我们去定义一个结构体都是这么去写的,例如说这里有一个链表结点的结构体,一个是数据域,一个是指针域
struct ListNode {
	int val;
	struct ListNode* next;		
};
  • 在C++中,我们也可以这么去写,上面说到过C++兼容C,可是呢有一处却可以发生变化。也就是在定义这个指针域的时候,可以不需要再写struct
struct ListNode {
	int val;
	ListNode* next;		
};
  • 通过下面两幅图的对比就可以很清楚地看在C++中确实在使用结构体的时候不需要再去写一遍struct这个关键字了,直接使用定义出来的结构体即可;但是在C语言中没有这样的规定,所以是一定要写的

C++ | 你真的学懂了类和对象的封装思想吗?_第5张图片
C++ | 你真的学懂了类和对象的封装思想吗?_第6张图片


  • 不过C++相较于C语言可不只是多了这一点,C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数【但是这在C语言中,是不被允许的】

C++ | 你真的学懂了类和对象的封装思想吗?_第7张图片
C++ | 你真的学懂了类和对象的封装思想吗?_第8张图片

知道了上面这些,其实就可以回忆我们之前在数据结构中写过的很多代码,在结构体中只是定义了一些成员变量,具体的函数都是写在结构体外,那现在知道了C++可以这么去做的话,是否可以将这些函数都写到结构体内来呢?我们来试试看

2、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语言实现【栈】的时候在每个算法接口前面都是有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++ | 你真的学懂了类和对象的封装思想吗?_第9张图片
通过上面所写,使用C++去代替实现之前使用C语言写的【栈】时,发现完全没问题,这下你应该可以进一步了解为何C++兼容C了,不过呢在C++中,这样变量和函数存放在一起的结构我们不叫做结构体,而叫做【】,可是对于类来说,在C++中也不常使用struct这个关键字来定义,而是使用[class]

3、结构体 --> 类

语法格式:

class className
{
// 类体:由成员函数和成员变量组成
};  // 一定要注意后面的分号

【注】:class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略

  • 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数

类的两种定义方式

知道了一个类长什么样,接下去我们来聊聊一个类该如何去进行规范的定义

  1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
  • 这也就是我们上面讲到过有关【栈】的这种定义,只需要将struct换成class即可,这种类的定义方式简单粗暴,也是我们平常用得最多的,自己练习代码可以直接这样使用,但其实在日常项目的开发中,不建议大家这样使用❌
  1. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
  • 重点来讲一讲这一种,这也叫做多文件形式的编写,之间在C语言的学习中我们写的【扫雷】和【三子棋】也是使用的这种分文件编写,如果不了解的读者一定要学会这种思想,在日常企业的开发中是经常使用到的

stack.h

#pragma once

#include 
#include 
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++ | 你真的学懂了类和对象的封装思想吗?_第10张图片

  • 这就是在上面说到的:成员函数名前需要加类名::,我们在命名空间的讲解中有说到过有关【作用域】的概念,在C++中,对于一个类体而言,其实就属于一个作用域,将成员变量和成员函数包含在里面。那么此时要在另一个cpp的文件中访问这个类中定义的成员变量的话也就是访问Stack作用域中的内容,就要加上【域作用限定符::】,就像下面这样

C++ | 你真的学懂了类和对象的封装思想吗?_第11张图片

成员变量命名规则

最后再来普及一点,你可以自己观察我上面在写【栈】的时候对成员变量的命名形式,前面都加上了_,可能你看起来会很别扭,但这却是比较规范的一种定义形式

  • 其实你可以去看一看库里面一些变量的命名方式,很多都是采用这种下划线的方式进行,原因其实就在于避免造成【成员变量】和【形参】的命名冲突从而引发歧义

C++ | 你真的学懂了类和对象的封装思想吗?_第12张图片

  • 可以看到,我在下面写了一个日期类,通过Init()这个函数对类中的成员变量去进行一个初始化,观察【成员变量】和【形参】可以发现我故意将它们写成了一样的,此时调用函数进行初始化操作的时候会发生什么呢?
class Date {
public:
	void Init(int year, int month, int day)
	{
		year = year;
		month = month;
		day = day;
	}
	int year;
	int month;
	int day;
};

C++ | 你真的学懂了类和对象的封装思想吗?_第13张图片
通过观察可以发现,若是【成员变量】和【形参】的名字一样的话,其实这个时候就会造成歧义,初始化的就不是当前这个对象的成员变量了,如果你自己观察就可以发现,命名一样的话,在VS中二者的字体都会变淡,这其实就是VS在提示你这样的做法其实是无效的❌

C++ | 你真的学懂了类和对象的封装思想吗?_第14张图片

那要如何命名才是最规范的呢?

  • 这个其实我说了不算,要看你实际的开发的地方是如何规定的,如果是你自己的做开发的话,那建议就是【成员变量】改成_变量名或者是m_变量名,但如果你在公司里面的话,内部是如何规定的你怎么做就行了,这个没有强制,只要别造成相同即可
  • 但是你一定在某些地方见过this->year = year这种写法,确实这也可以,这里面就用到了C++类和对象中很重要的一块叫做【this指针】,这里先不做详解,见最后一个模块哦
this->year = year;
this->month = month;
this->day = day;

C++ | 你真的学懂了类和对象的封装思想吗?_第15张图片

四、类的访问限定符及封装【⭐】

学习了上面的这些,你只是初步了解了什么是类,但是C++中的类远远不止将struct换成class这简单,如果你自己观察的话,可以发现我在上面的Date类中加了【public:】和【private:】这两个东西,它们就叫做类的访问限定符

1、C++中的三类访问限定符

  • 正式来说一说C++中的三类访问限定符【public】【protected】和【private】
  • 其中,对于public来说只的是共有,表示从当前public到收括号};为止的所有成员变量或者是成员函数均为公有的,什么是公有呢?就是类内类外都可以随意调用访问,不受限制
  • private指的就是私有,这很直观,对于共有来说就是所有东西都是公开的,无论是谁都可以访问;那对于私有来说便是无法访问,谁无法访问呢?这里指的是外界无法访问,但类内还是可以访问的,例如就是类内的成员函数访问这些成员变量是不受限制的
  • protected指的是保护,代表它会将类内的这些东西保护起来,外界无法访问到。但对于这个限定来说暂时可以把它当成和private是类同的,到了C++中的【多态】才会出现差异

C++ | 你真的学懂了类和对象的封装思想吗?_第16张图片

  • 光就上面这么说你一定会觉得有些抽象,其实读者可以将这个访问限定符看作是一把【锁】,设想你家里装了一把锁,那么此时锁住的就是外面的人,对家里的人是不会有影响的

接下去再来看看有关访问限定符的一些特性指南

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. 如果后面没有访问限定符,作用域就到 } 即类结束。
  5. class的默认访问权限为private,struct为public(因为struct要兼容C)
  • 主要还是来看一下这个第5点,在C语言中,我们在结构体中直接定义出一个成员变量的时候不需要去考虑是否可以访问到,而是直接就这访问了;但是在C++中,我们在访问类中的一个成员变量的时候,是会受到限制的,我们可以来看看

C++ | 你真的学懂了类和对象的封装思想吗?_第17张图片

  • 可以看出,即使我将类中的private去掉的话,还是会存在【不可访问】的现象,原因就是在于类内的定义的内容默认访问权限都是private,外界是无法访问到的

但一定会有同学有这么一个疑问,那在加上[private]关键字后,这个成员变量也是私有的呀,为什么可以对他们去进行一个初始化呢?那不是访问到了这些成员变量了

  • 这一点要注意,当我在初始化的时候,并没有直接去访问类内的【成员变量】,而是调用了【成员函数】,在成员函数的内部又调用了类内的【成员变量】,上面有说到过,对于私有的东西虽然类外是不可访问的,但类内还是可以访问的,这个只是锁住了外来入侵者,自己家里的人还是不受限制的

对于上面这一点来说,其实就又一些C++中类的封装思想了,接下去我们就正式来谈谈面向对象的三大特性之一 —— 【封装】

2、初探类的封装

【封装思想】:用类将对象的属性(数据)与操作数据的方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用

  • 封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件
  • 设想若是没有将电脑中的一些元件给封装起来,就是将内部的一些部件展现在用户的眼前,那么用户每次在将电脑开始的时候都需要那边点一下,这边开个开关,使用起来就会很麻烦,所以可以看出,对于电脑来说,也是存在这么一个封装的思想【很好地将内部的细节给屏蔽起来了,方便管理

C++ | 你真的学懂了类和对象的封装思想吗?_第18张图片

这里先初步地讲一下有关【类的封装】思想,文章的后半部分会不断地加强读者对这块的理解

五、类的实例化

当我们写好一个类之后,就要去把它给定义出来,就好比在C语言中,我们要使用一个变量的话,也是要把它定义出来才行,才可以使用,例如:结构体声明好了之后就要将其定义出来,否则是不用的

1、变量的声明与定义 - - 铁瓷还会铁吗?

  • 首先我要问你一个问题,下面的这三个成员变量是已经被定义出来了?还是只是声明呢?

在这里插入图片描述

  • 读者一定要理解【声明】和【定义】的区别,对于声明来说,只是告诉编译器存在这么一个东西,但它不是实际存在于内存中的;而对于定义来说就是实实在在地把空间给开出来,那么此时在内存中就有它的一席之地了

可能就这么说太好理解,我们通过一个形象一点的案例来说明

  • 你呢,背井离乡在二线城市当一个程序员,工作了几年也赚了不少钱,此时你就想把一直以来的出租屋换成一个崭新的房子,想要在你所处的城市买个房,虽然交不起所有的钱,但首付还是可以的,不过呢还差那么几万块钱,于是呢就想到了你大学时候的室友,也是个铁瓷很要好的朋友,想找他结点钱
  • 于是就打电话过去说:“兄弟呀,我最近想买个房,交个首付,不过手头上还差个几万块钱,你看你有没有一些不急着用的先借我点,之后赚来了再还给你。”那听到昔日的好友这么一番话,便说:“可以可以,好兄弟开口,那必须帮忙!”于是呢他就这么答应你了,不过也只是口头答应,也就是一种承诺这个口头答应其实指得就是【声明】,你只能知道会有这么一笔钱给到你,但是这笔钱还没真正到你的手里

C++ | 你真的学懂了类和对象的封装思想吗?_第19张图片

  • 不过呢,过了好几天了,还是不见兄弟把钱打过来,眼看就要交首付了,只能再给他打一个电话过去说:“兄弟,上次和你说的那个钱怎么样了,后天就要交首付了,你看能不能先打过来。”当你说完这句话之后,其实就会出现两种情况Ⅱ
    1. 你的兄弟回道:“哦哦,不好意思,最后手头太忙可了,都给忘了,马上给你转过来。”此时就听到【支付宝到账5万元】的声音,那么这笔钱就真正地到你手里的,这是实实在在的,已经存在了的事,指的就是【定义】
    2. 你的兄弟回道:“啊呀,这个,真是不好意思啊,家里的钱都给媳妇管着呢,它不同意我也办法,对不住了兄弟,要不你再找找别人。”于是他便小心翼翼地挂掉了电话,你俩就没有再联系过,铁瓷也不铁了~

  • 对于上面的第二种情况,就很像平常在写程序的时候出现链接错误的情况,那就是【声明了但是未定义】的这种行为。之前承诺了、声明了,但是找你要的时候实际却没有
    • 对于函数而言就是有声明找不到定义
    • 对于变量而言就是这个变量没开空间
  • 所以对于这三个成员变量来说只是一个声明,不是定义,并没有开出实际的空间

在这里插入图片描述
那怎样才算定义呢?又是何时开出空间,让我们来瞧瞧【类对象的声明与定义】

2、类对象的声明与定义 - - 别墅设计图

  • 要实际地开出空间来,其实值得就是要将这个类定义出来,因为你的成员变量是声明在类里面的,那你把写好的这个类定义出来后,【成员变量】也就对应的在内存中开出了一块空间,它们是作为一个整体来进行定义的
int main(void)
{
	Date d;		//类对象的实例化
	return 0;
}

用类类型创建对象的过程,称为类的实例化

  1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;

    • 比如:入学时填写的【学生信息表】,表格就可以看成是一个类,来描述具体学生信息
      C++ | 你真的学懂了类和对象的封装思想吗?_第20张图片
    • 对于类来说就像是谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。例如谜语:"年纪不大,胡子一把,主人来了,就喊妈妈“。这只是一个【描述】,但是实际要知道这个,谜语在描述写什么,这个类里面有什么东西,想要传达出什么,就要将它实例化出来,定义出来,那么谜底也就揭晓了 谜底:山羊
  2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量

    • 这个又怎么去理解呢?这里给读者举一个形象的例子:不知道你是否了解一个建筑物是如何从设计到建成的,重要要经过很多的步骤,但是在一开始建筑师一定是要设计出一张【设计图】来,好对这个房子的整体样式和架构先有一个大致的轮廓,在后面才可以一步一步地慢慢实施建设计划。
    • 那其实对于这个类来说就和【设计图】是一样的,比方说现在我们要造一栋别墅,那么一张图纸,即一个类中描述的就是这个别墅有几层,多少个房间,门朝哪儿开,是一个大致的框架,不过呢这也就仅仅是一个设计图罢了,还不是一个真实的别墅,不存在具体的空间,因此是不能住人的
      C++ | 你真的学懂了类和对象的封装思想吗?_第21张图片
    • 那要怎样才能住人呢?也就是建筑师通过这张设计图,找几个施工队真实地将别墅设计出来,那才可以真正地住人
    • 但平常我们看到的那种高档小区中,可不止一栋这样的别墅,而是有几十栋,难道设计师也要画出几十张设计图才能建完这些别墅吗?当然不是,对于一张设计图来说是可以建造出无数的别墅,只要根据这个设计图来建就行。那上面说到对于设计图来说就是一个类,也就意味着一个类也是可以实例化出多个对象的
      C++ | 你真的学懂了类和对象的封装思想吗?_第22张图片
    • 实例化出这个对象后也就实实在在地将空间给开出来了,那我们上面说到过的【成员变量】,此时也开出了空间,就可以存放对应的数据了
Date d;
d.year = 2023;
d.month = 3;
d.day = 18;
  • 但对于下面这种形式去初始化成员变量是不行的,若是还没有定义出一个对象的,成员变量不存在实际空间的,直接用类名去进行访问就会出错,不过后面的文章中我会讲到一种叫做静态成员变量,用static进行修饰,是可以的直接使用类名来进行访问的

C++ | 你真的学懂了类和对象的封装思想吗?_第23张图片

六、类对象模型

1、成员函数是否存在重复定义?

  • 上面,我们说到了对于一个成员变量来说,若是类没有被定义出来的话它是不存在具体空间的,那在一个类中除了成员变量外,还有【成员函数】,仔细观察可以发现,这似乎就已经把成员函数定义出来了呀,那空间不是已经存在了。 此时是外面再去实例化这个类的话,岂不是会造成重复定义了?

C++ | 你真的学懂了类和对象的封装思想吗?_第24张图片

  • 可是刚才我们有看过,在实例化出这个Date类的对象时,并没有报出重复定义这个错误,而且在调用这个Init()Print()函数的时候也不会有什么问题,这是为何呢?难道这个【成员函数】和类没什么关系吗?它存在于类中吗?

让我们带着这个疑问开始本模块的学习

2、计算类的大小【结构体内存对齐】

要想知道这个类中存在多少东西,其实我们去计算一个它的大小即可

  • 还记得结构体内存对齐吗?忘记了就再去看看,下面是对应的规则
  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  • 在C语言中,我们有去计算过一个结构体的大小,那上面我们在对于结构体和类做对比的时候说到对于structclass都可以去定义一个类,那么结构体内存对齐的规则也一样适用。不过我们只会计算成员变量的大小,那就来先计算一下这个【year】、【month】、【day】的大小

C++ | 你真的学懂了类和对象的封装思想吗?_第25张图片

  • 通过画图以及运行结果可以观察,可以得出类的大小和我们计算的【成员变量】大小竟然是一致的,那【成员函数】呢?没有算上去吗?还是根本不计算在内?

在这里插入图片描述

3、探究类对象的存储方式

在看了上面惊人的一幕后,我们就来思考一下,对于这个类对象究竟是如何进行存储的。在下面,我给出了类对象存储方式的三种设计方式,你认为哪一种设计方式是正确的呢?

  • 首先是第一种,也就是将类的【成员变量】和【成员函数】都存放在一起,其实对于每个类的成员变量来说都是不一样的,都有它们不同的空间,可调用的是同一份函数。,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?

C++ | 你真的学懂了类和对象的封装思想吗?_第26张图片

  • 下面是第二种设计方式。代码只保存一份,在对象中保存存放代码的地址,这种方式似乎看起来不错,你认为可行吗?

C++ | 你真的学懂了类和对象的封装思想吗?_第27张图片

  • 再来看看第三射击方案。可以看到对于【成员变量】和【成员函数】完全就是分离了,存在了一个叫做公共代码区的地方,类的所有函数都放在一个类函数成员表中
  • 对于每一个对象来说都是互相独立的,里面只存放了各自的成员变量,而要找成员函数的话就要通过当前这个类的对象去公共代码区进行调用

C++ | 你真的学懂了类和对象的封装思想吗?_第28张图片

  • 答案揭晓,那就是最后这一种,实现了成员与函数的分离,为什么要这么设计呢?上面其实有提到过,虽然每个对象的成员变量是不同的,各自各自的空间,但是对于成员函数来说,大家都是一样的,例如这个Init()函数,外界被定义出来的对象只需要调用一下这个函数去初始它自己的成员变量即可,不需要将其放在自己的类内。
  • 设想若是每个类中都写一个这样相同函数的话,此时每个对象就会变得非常庞大,也就是我不去调用这个函数,只是将对象定义出来函数的空间就已经会存在了,这样的设计其实是不好的,所以我们应该采取第三种做法

感性理解:私有场所与共有场所

但是这么说一定是比较抽象了,我们再通过一个生活小案例来理解一下

  • 就用刚才说到的这个别墅小区好了,那在每栋别墅里面都是房间的,像客厅、卧室、厨房、洗手间,每家每户基本都有,但是呢每一家都有它们自己家庭的设计,既然是个人别墅,那么一定不可能每栋房子的客厅、卧室、厨房、洗手间都在同一个位置吧,那就太单调了╮(╯▽╰)╭,这些房间呢值得就是【成员变量】
  • 那在一个小区中,除了挨家挨户的的私人领域外,一定会存在公共区域,在这些公共区域中,会有一些公共场所,例如像篮球场、咖啡馆、游泳馆、小卖部或是健身器材等等,对于这个公共区域你可以理解为【公共代码区】,而对于这些公共场所、设施你可以理解为【成员函数】

C++ | 你真的学懂了类和对象的封装思想吗?_第29张图片

  • 那其实这就很形象了,【成员变量】是每个对象各自的,由于类的封装特性别人无法轻易访问,可是呢对于这个【成员函数】来说,是大家共有的,可以一起使用,所以不需要放在自己家里,除非你真的很有钱,在一个别墅小区再自己建一些私人的游泳池、篮球场和小卖部

4、空类大小计算【面试考点✒】

  • 学习了如何去计算一个类之后,接下去请你来判别一下下面三个类的大小分别为多少
// 类中既有成员变量,又有成员函数
class A1 {
	void f1() {}
private:
	int a;
};

// 类中仅有成员函数
class A2 {
	void f1(){}
};

// 类中什么都没有---空类
class A3 {};
  • 首先是类A1,有一个成员变量,那经过上面的学习可以得知成员函数是不存在于类中,又因为整型占4个字节,所以很容易可以得知A3的大小为4
  • 接下去对于类A2,只有一个成员函数f1(),没有成员变量,那【sizeof(A2)】的结果会是多少呢?一会看运行结果后再来分析
  • 接下去是类A3,对于这个类来说既没有成员函数也没有成员变量,那它的大小会是多少呢?0吗?

我们来看一下运行结果

C++ | 你真的学懂了类和对象的封装思想吗?_第30张图片

  • 通过观察可以得知,似乎只算对了第一个类A1的大小,但是前两个类的大小为什么都是1呢?这相信读者也是非常疑惑吧?立马为你来解答
  • 一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。但是对于空类的大小却不太一样,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象【这1B不存储有效数据,为一个占位符,标识对象被实例化定义出来了

上面的这个概念在笔试面试中都有可能会涉及,准备校招的同学要重视

七、this指针【⭐重点掌握⭐】

1、提问:如何区分当前初始化对象?

  • 继续来回顾一下上面所写的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]是外部传递进来的2023,[_year]是内部的成员变量,但是仔细回忆一下,刚才我们有说到这个[_year]只是类内部声明的,并没有被定义出来呀,那要怎么赋值初始化呢?
  • 有同学说:外面不是定义出这个对象d1了,那么三个成员变量的空间自然也就开出来了,是的,这没错
Date d1;
d1.Init(2023, 3, 18);
  • 可是现在呢,我又定义了一个对象,此时就会存在两个对象d1和d2,然后分别去调用这个Init()函数来初始化自己的成员变量,那外部传入实参的时候是可以分清的,但是传入到内部时_year = year中的[_year]要怎么区分这是d1还是d2的成员变量呢?若有又定义了一个d3呢?如何做到精准赋值无误?
  • 在外部定义出来的对象调用的时候可以很明确是哪个对象调的,但是到了函数内部又是辨别的呢?对于成员函数来说存放在公共代码区,大家都可以调用,那即使调用了也没有传入当前对象的地址呀,韩素红内部怎么知道要给哪个对象初始化成员变量呢?

好,就让我们带着这些问题,进入下一小节的学习

Date d1;
Date d2;

d1.Init(2023, 3, 18);
d2.Init(2024, 3, 18);

2、深度探究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】在英文单词中指的就是当前,那么意思就很明确了,为当前对象的yearmonthday进行初始化。随着每次的传入的对象地址不同,this指针就会通过不同的地址去找到内存中对应的那块地址中的成员变量,进行精准赋值

C++ | 你真的学懂了类和对象的封装思想吗?_第31张图片

  • 不过通过观察可以发现,似乎我们自己去加上这一个参数好像是行不通,编译器报出了很多错误,

C++ | 你真的学懂了类和对象的封装思想吗?_第32张图片
看看下面这段话就知道为什么了

  • C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成
  • 所以这个this指针我们是不可以加的,编译器会自动帮我们加上,并且传递当前对象的地址

不过,虽然我们不能传递观察,但可以通过这个隐藏的this指针来看看是否真的传递了当前对象的地址进去

C++ | 你真的学懂了类和对象的封装思想吗?_第33张图片

了解了this指针的基本原理后,我们来聊聊它的相关特性

  1. this指针的类型:类类型* const(Date* const),即成员函数中,不能给this指针赋值

    • 对于this指针来说,是被const常所修饰的,为【指针常量】,对于指针本身的指向是不可修改的,但是指针所指向的内容可以通过解引用的方式来修改。如果不是很清楚这一块可以看看常量指针与指针常量的感性理解

C++ | 你真的学懂了类和对象的封装思想吗?_第34张图片

  1. 只能在“成员函数”的内部使用
    • 这一点要牢记,对于this指针而言,只可以在类的成员函数内部进行使用,是不可以在外部进行使用的,因为它是作为一个成员函数的形参,若是没有传递给当前对象地址的话,那么它的指向是不确定的,但当进入成员函数内部时,编译器底层一定调用了这个this指针,为其传递了对象的地址,此时在内部再去使用的话是不会有问题的

C++ | 你真的学懂了类和对象的封装思想吗?_第35张图片

  1. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针

    • 这一点上面也有强调过,this指针是作为形参来使用,那对于函数形参,我们在函数栈帧一文有深入研究过它是需要进行压栈的,那就要建立函数栈帧,可以很明确它就是存放在栈区的,而不是存放在对象中,这一点下面有一道面试题也是涉及到,再做细讲
    • 而且刚才在求解类的大小时,通过结构体内存对齐可以很明确地看出除了【成员变量】之外的其他的东西都是不计算在内的
  2. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

    • 这一点我们可以通过汇编指令来看
    • 可以观察到,传递进函数Init()的参数都会被压入栈中,不过可以观察到,由于栈【先进后出】的性质,是从第4个参数开始压栈的,若是按照原本的三个参数来说应该会压三次,但是看到2023被Push进去之后还有一个[d1]需要被lea(load effective address)进去,不过并不是直接加载,而是放到一个寄存器ecx中再加载,这个d1指的其实就是对象d1的地址
    • 通过汇编指令可以把底层的逻辑看得很清楚,观察到确实是存在this指针接受当前调用对象的地址

C++ | 你真的学懂了类和对象的封装思想吗?_第36张图片
C++ | 你真的学懂了类和对象的封装思想吗?_第37张图片

3、this指针的感性理解

说到了这么多有关this指针的特性,有些特性对大家来说可能还是比较难以理解,接下去我会通过三个生活中的小场景带你好好理解一下

  1. 夏天到了,呆在家里一定会很热,一天到晚打空调对身体又不好,此时就会想到去游泳馆游泳,那去游泳的话肯定要换上专门的衣物,去哪里换呢?当然是更衣室了,有过经历的同学一定知道当你去更衣室换衣服的时候,前台就会给你一个手环,可以识别感应里面的柜子,一个人一个柜子可以放置自己的私人物品。然后就把这个手环套在你的手上,最后当你游完泳后要怎么知道那个是你的柜子呢?那是通过这个手环来进行感应打开柜门取出自己的衣物【这个手环就是用来识别的,别人的手环打不开你的柜子】

C++ | 你真的学懂了类和对象的封装思想吗?_第38张图片

  1. 在大学生活中,每个人一定都有自己的校园卡,这张校园卡呢可以用来吃饭、洗澡、接水,甚至可以代替人脸识别,所以在这个校园中,校园卡就是你的身份象征,每个人都是唯一的

C++ | 你真的学懂了类和对象的封装思想吗?_第39张图片

  1. 住过小区的一定都知道,现在的小区管理制度是越来越严了,出入呢都需要这个门禁卡,才可以证明你的身份,是属于这个小区的,防止外来人员入室盗窃,所以这个门禁卡就是你身份的象征【有没带门禁进不去单元门的吗】

C++ | 你真的学懂了类和对象的封装思想吗?_第40张图片
通过上面的三个生活小案例,相信你对this指针一定有有了自己的理解

4、两道夺命面试题

本小节的内容可能会让你感到非常枯燥,如果没有校招需求的读者可以选择跳过,当然也可以浏览一下喽

this指针存放在哪里?

先来看看第一位同学的回答:

this指针是存放对象地址的,和对象存在关系。应该是存放在对象中的把

  • 听完了他这一番话,我差点没拿起我的四十米大刀抡过去(╯▔皿▔)╯,刚才我们讲到了有关this指针的特性,现在再重复一遍。它是作为成员函数的隐含形参,既然是函数形参的话,那就需要压栈、建立栈帧,所以这个this指针是存放在栈上的。
  • 不过在VS中,编译器会使用寄存器ecx进行一个临时保存,刚才我们也有通过汇编指令进行一个查看

C++ | 你真的学懂了类和对象的封装思想吗?_第41张图片

再来听听第二位同学的回答:

刚才不是说这个成员函数是存放在公共代码区的吗,那隐藏形参this是属于这个函数的,为何没有存放在公共代码区呢?

  • 这个问题其实问得不错,不过这属于一个知识混淆了,不要把【栈区】和【公共代码区】混为一谈
  • 我们现在对一段程序进行编译,实例化出一个对象后这个对象就是存在【栈区】中的,但是成员函数不存放在其中,因为成员函数是属于公共区域的一部分,所以在编译完成之后,call指令的地址不在对象中找,而去【公共代码区】中找,为什么要去这个公共区找呢?因为成员函数被编译出来的这些指令(刚才看的指令)存放在这里面, 而成员函数内部的一些形参、临时变量则不在这里面,它们都是存放在【栈区】中的。所以this指针不在【公共代码区】,而在【栈区】
  • 听完我的这番话后,这个同学似乎就明白了一些东西

this指针可以为空吗?

好,接下去我们再来看第二个面试题

请问下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

class Date {
public:
	//定义
	//void Init(Date* this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		cout << "this:" << this << endl;
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

	void Print()
	{

		cout << "Print()" << endl;
	}
private:
	int _year;	//仅仅是声明,并没有开出实际的空间
	int _month;
	int _day;
};

int main()
{
	Date* p = nullptr;
	p->Print();
}

运行结果:

C++ | 你真的学懂了类和对象的封装思想吗?_第42张图片

  • 看了上面的运行结果,你是否感觉很吃惊,为何没有【运行崩溃】呢?为什么可以对一个空指针去解引用呢?
  • 可以知道这个Print()是Date类的一个成员函数,那既然是成员函数的话就可以调用this指针,我们来试试

在这里插入图片描述

  • 可以看到this指针所接收到的地址为空,这很明确,我们在因为在外界调用这个函数的对象指针就是空的。那可以看出其实从调用到函数内部的执行完全没有进行一次解引用的操作,所以才不会引发空指针异常的问题

  • 不过呢,就上面的这一个还考察不出一个人对this指针的理解,一般考了上面这个还会接连着考下面这个
p->Init(2023, 3, 19);

运行结果:

C++ | 你真的学懂了类和对象的封装思想吗?_第43张图片

  • 很明显,若是去调用Init()初始化函数的话就会发生空指针异常的问题,这是为什么呢?

有同学说:一看就是这个this->的问题,很明显的解引用嘛,去掉不就好了

  • 然后继续给他看了这个,他便陷入了沉思

C++ | 你真的学懂了类和对象的封装思想吗?_第44张图片

  • 这里要说明一点,本模块一开始我们初谈this指针的时候说到, 在成员函数内部调用成员变量时,可以在前面加上this->去指向,当然也可以不加,因为是默认带有的,所以在这里无论是显式地加上还是隐式让他保留,都会产生一个【指针指向】,那我们在C语言的指针章节有谈到,对于指针指向而言其实就是解引用,所以这才造成了出错

通过上面这两个函数的调用,相信你对this指针传参机制有了一些基本认识。但还是没有清楚这其中的原理,接下来我便带你分析一下里面的原理

  • 上面有说到过,对于成员函数而言是不存在于类内部的,而是存放于【公共代码区】,所以对于上面的这两个函数而言都不去类里找,即【栈区】里找,而是通过函数名修饰后的名称去【公共代码区】里找
  • 对于Print()函数而言,并没有涉及到访问成员变量,那你可以把它理解为在别人家的小区里的公共蓝球场打了个篮球,那这其实是属于公共的区域,而没有闯入别人的私人领地
  • 使用空指针去调用了一下这个函数,此时传递这个空对象的地址给到成员函数内部的【this】指针,然后我去打印了一下这个this指针,那也就是将这个对象的地址给打印出来,因为它是一个空对象,所以结果是00000000。但是并没有再做任何的事了,所以不会有问题
  • 就比方说你手里有一把刚从刀柄里拔出来的刀,但也只是从刀柄里拔出来看看,并没有那它去做任何坏事,伤害到别人,那也没什么问题嘛╮(╯▽╰)╭

  • 当在调用这个Init()函数时,也不是去【栈区】里找,而是去【公共代码区】里找,也是一样首先打印了一下传入的空对象地址,不过接下来的操作就不对了!只要是要访问当前类的成员变量时,都需要使用到this指针来进行指向,只是因为它是一个隐式形参罢了
  • 打个比方,你又进到别人家小区了,但是呢此时却没有经过别人的同意随意地躺在别人家沙发上上,这个沙发就属于别人的私有物品,也可以看做是成员变量,刚才我们并没有访问成员变量,所以是没问题的,但是现在却访问了一个私有的东西,那报出错误也是很正常的

那有同学又说:那我在调用Init()的时候不传空对象不就好了,直接调用

Init(2023, 3, 19);
  • 你觉得这样可以吗?我让他罚站了半个小时~
  • 上面我们说到,若是一个定义出来的对象去调用成员函数,会有一个隐藏的形参this接受传递过来的对象地址,以此去调用不同对象的成员变量。但是就上面这个调用,连执行的对象都没有,你觉得this指针会接收到什么东西呢?
  • 通过运行结果可以看出,连编译都过不了,谈何运行呢?

C++ | 你真的学懂了类和对象的封装思想吗?_第45张图片

好,看完了上面这一些,相信你对this指针的了解一定更加深刻了,我们再来看最后一个

还是调用刚才的Print()函数 A、编译报错 B、运行崩溃 C、正常运行

(*ptr).Print();
  • 在学习了上面的两道题后,相信你一定会觉得这是个【编译报错】或者是【运行奔溃】,不过结果却大相径庭。竟然完全没有问题,可以正常运行!

C++ | 你真的学懂了类和对象的封装思想吗?_第46张图片
这是什么呢???
C++ | 你真的学懂了类和对象的封装思想吗?_第47张图片

  • 若是你完全不懂这一点的话,应该再回过去看看指针的解引用那块,难道一个指针去进行指向->或者是解引用*就一定是在对指针进行解引用嘛
  • 不,会不会解引用取决于要不要到指向的对象中去找成员变量,而不是看有没有”->“。因为对于编译器来说,是将代码的这些语法转换为指令,我们要去看的是这些汇编指令

C++ | 你真的学懂了类和对象的封装思想吗?_第48张图片
在这里插入图片描述

  • 通过上面的观察可以看出,从【汇编角度】而言,其实编译器把->*解析成了同一种方式,至于内部的逻辑是怎么样的,上面已经讲过了,此处不再赘述

最后,有位同学又提出了这样的写法,蛮不错的,给读者分享一下

  • 你认为下面这种直接用类名然后【域作用限定符::】的方式去访问可行吗?
Date::Print();

运行结果如下:

C++ | 你真的学懂了类和对象的封装思想吗?_第49张图片

  • 对于这种调用形式,我们在上面其实也提到过,只有静态的成员函数或者是静态的成员变量才可以用类名直接访问,所以这样是不可以的,也是一种没有传入当前对象地址给this指针的形式

为什么没有让她罚站呢,怎么能让女生罚站呢,是吧

好,接下去就对上面的两道面试题所引申出来的知识点做一个总结与回顾,我们就进入下一模块

【总结一下】:

  • 对于this指针而言,是存放在【栈区】的,虽然其是属于成员函数的一个隐式形参,但是却不和成员函数一样存放在【公共代码区】,对于成员函数而言只是编译器将其解析后的汇编指令存放在公共区罢了,而对于函数内部的形参和临时变量,都还是存放在栈区的,是需要开辟栈帧、压栈的↓
  • this指针可以为空,但能不能正常运行取决于你如何去调用,仅仅是传递了空对象地址但没有进行解引用的话不会出现问题。但若是在成员函数内部访问成员变量的话,无论你有无给出this->,都会造成解引用从而导致空指针异常⚠的问题。
  • 看一个指针是否产生了介意用不是光光看->或者*,而是要去观察底层的汇编指令如何执行,这才是计算机内部的真正执行逻辑

八、C和C++实现栈的对比

好,讲了这么多,相信读者对C++的类这一块一定有了自己的理解,本模块,我将通过C语言和C++分别去实现一个【栈】,通过观察来让读者看出C++到底是如何实现封装的

1、代码展示

C语言的相关代码可以看这篇文章 链接,这里就不贴代码了

主要来展示一下C++的代码,下面是比较规范的声明与定义分离的形式,可以先看看

stack.h

typedef int DataType;
class Stack
{
public:
	void Init(size_t capacity = 4);

	void Check_Capacity();

	void Push(const DataType& data);

	void Pop();

	bool Empty();

	DataType Top();

	void Destroy();

private:
	DataType* _array;
	size_t _capacity;
	size_t _top;
};

stack.cpp

#include "stack.h"

void Stack::Init(size_t capacity)
{
	_array = (DataType*)malloc(sizeof(DataType) * capacity);
	if (nullptr == _array)
	{
		perror("malloc申请空间失败");
		return;
	}
	_capacity = capacity;
	_top = 0;
}

void Stack::Check_Capacity()
{
	if (_top == _capacity)
	{	
		DataType* tmp = (DataType*)realloc(_array, sizeof(DataType) * _capacity * 2);
		if (nullptr == tmp)
		{
			perror("fail realloc");
			exit(-1);
		}
		_array = tmp;
		_capacity = _capacity * 2;
	}
}

void Stack::Push(const DataType& data)
{
	// 扩容
	Check_Capacity();
	_array[_top] = data;
	++_top;
}

bool Stack::Empty()
{
	return _top == 0;
}

void Stack::Pop()
{
	assert(_top > 0);
	assert(!Empty());
	_top--;
}

DataType Stack::Top()
{
	return _array[_top - 1];
}

void Stack::Destroy()
{
	if (_array)
	{
		free(_array);
		_array = nullptr;
		_capacity = 0;
		_top = 0;
	}
}

test.cpp

int main(void)
{
	Stack st;
	st.Init();

	st.Push(1);
	st.Push(2);
	st.Push(3);

	size_t top = st.Top();
	cout << top << endl;

	st.Pop();
	top = st.Top();
	cout << top << endl;
	
	st.Destroy();
	return 0;
}

运行结果:

C++ | 你真的学懂了类和对象的封装思想吗?_第50张图片


  • 其实除了C/C++之外,像其他语言例如Java都是可以实现一个栈的【链接】,只不过语言的立场、语法的细节不一样而已
  • 如果平常自己懒得去写,直接使用库里面给我们写好的就行,C++和Java都有现成的库给我们封装好了,只需要调用一下需要的API即可,像我们后续会学习的STL中stack,里面就有上面所学的全部内容

2、C语言特性分析

  1. 数据和方法是分离的
    • 这一点其实很好观察,当我们使用C语言去写一个栈的时候,是将存放数据的数组、栈顶指针、容量大小都放在结构体中,其他算法接口再另外分离写,二者的关联性并不是很大

C++ | 你真的学懂了类和对象的封装思想吗?_第51张图片

  1. 数据访问控制是自由的,不受限制

    • 还有第二点,就是在C语言中,我们去访问一个数据的时候,其实是比较自由的,不会受到过多的限制。
    • 举个例子,当初我们在数据结构中写栈的StackTop()时,产生了分歧,有的同学说直接像下面这样取就可以了
    int top1 = st->a[st->top - 1];	 
    
    • 但是呢有的同学却觉得即使是再小的功能也应该封装成为一个函数的形式,之后再去进行调用
    int top2 = StackTop(&st);
    
    • 我赞同了上面这种写法,还记得为什么吗?因为对于外界来说是无法知晓你底层的逻辑实现是怎样的,若是写成第一种形式的话,调用者就得知道当前这个栈在初始化的时候top指针初始化的值是多少,是-1呢?还是0呢?
    • 但若是采用第二种写法,调用者完全不需要关心这个函数内部的实现细节,只需要进行调用即可,就会显得很方便
    asssert(top > 0);
    
    • 不仅如此,若是采取第一种形式的话,访问者不仅要知道底层top指针初始化为多少,而且还要知道当前栈中还有多少数据了,因为我们在去栈顶元素前都会去使用一个assert()进行检查,此时若是这个栈顶指针 <= 0的话也就表明栈里面没有元素了,再去通过数组访问的话就会造成有越界的风险

【总结一下】;

  • 使用C语言去进行访问的时候过于自由,因此需要考虑到很多因素
    1. 需要知道底层的top的初始化
    2. 有越界的风险,不安全

C语言语法的松散性 - - 过红绿灯还是慢一点吧

虽说使用C语言这样去进行访问确实存在一些缺陷,那为什么标准委员会没有改进这一点呢?还是允许这样的访问。

  • 这其实就是因为C语言本身【语法的松散性】导致的,因为C语言的语法限制不太严格,对变量的类型约束不严格,影响程序的安全性,对数组下标越界不作检查等。所以C语言其实较其他高级语言来说其实更难掌握,要求使用者对代码具备一定的的控制能力
  • 那面对C语言中的一些缺陷,官方文档中的也只是建议说使用的时候不要这样去做,像上面那样直接访问栈顶元素的方式【不做推荐

但是推荐这个东西管用吗?

  • 举一个很形象的例子,日常我们在开车经过十字路口的时候,都会有很多红绿灯来阻拦我们,此时就无法做到一路畅通,但是所谓的”红灯停,绿灯行,黄灯等一等”真的起到了什么作用吗?
  • 还是会存在大批行人闯红灯的现象,总有人不遵纪守法,导致出事故

C++ | 你真的学懂了类和对象的封装思想吗?_第52张图片
C++ | 你真的学懂了类和对象的封装思想吗?_第53张图片

  • 要知道,这个世界永远存在不确定的事,不可能所有的事情都愿你想得那么美好,就好比我们日常在做开发的时候,总是需要考虑到各种各样的问题,为什么?因为用户的行为是不确定的,可能哪一天就会做出你在开发是会后根本想不到的事,因为我们写代码时需要考虑到各方面的因素

所以可以看出来C语言存在一定的不严谨性,而且C语言还比较乱,尤其体现在学校的教科书和一些相关书籍中

  • 在C++引用一文的,我有提到了很多学校的数据结构教材中的一些代码,其实是C和C++混编的,可以却告诉读者使用的是C语言是实现,就是因为C和C++之间有着一些联系,所以很多读者就会分不清哪个是C,那么是C++的代码
  • 不仅如此,在一些经典书籍中,也会出现类似的情况,这里点名说一本书叫做《大话数据结构》。如果你有看过这本书的话会觉得它里面的一些讲解和图其实都蛮好的,也挺适合初学者,不过呢里面的一些代码让人看起来确实有点难受
  • 但凡你有去工作过的话,真的是看不上这本书的代码,可以说这个作者应该是缺乏一些工程经验,代码没有规范性

C++ | 你真的学懂了类和对象的封装思想吗?_第54张图片

上面的一些种种案例其实都可以说明C语言在语法设计这一块确实是有些松散了,导致缺乏经验的初学者会遇到很多难题

3、C++的特性优势分析

再谈类的封装思想 - - 兵马俑还是保护起来吧

  • 上面谈到了由于C语言在语法设计这一块存在松散性,因而导致了在使用的时候会有一些随机性和不确定性,存在一定的风险。但是C++在这一块却做得很好,一起来看看C++在封装这一块的思想
  1. 数据和方法都封装到类里面

    • C++做得很好是因为它并不是像C语言那样变量和函数都分离开来,而是将它们都封装到一个类里,全部保护起来了,外界是无法随意访问的
    • 初步谈到类的封装思想的时,说到【封装】其实就是一种更好地控制,更好地进行管理,现在我通过一个形象一点的案例再来描述一下这个封装的思想
      C++ | 你真的学懂了类和对象的封装思想吗?_第55张图片
    • 有去过西安的读者应该了解,这个六朝古都拥有世界八大奇迹之一的【秦始皇陵兵马俑】,是中华文化的瑰宝。从上图中我们可以看出,馆内将兵马俑都封在了中间的坑里,而外层则是一群群的游客,它们只能在站台上观看,而不可以下到坑洞里去触碰兵马俑
    • 这其实指得就是一种【封装】的思想,将兵马俑全部保护起来,外人无法轻易接触到。若是不将这些兵马俑保护起来,谁都可以接触到,那么要不了一个月,兵马俑上到处都会刻着“xxx到此一游”“xxx爱xxx”,或者缺胳膊少腿
      C++ | 你真的学懂了类和对象的封装思想吗?_第56张图片
  2. 控制访问方式。【愿意给你访问的共有,不愿意给你访问的私有】

    • 第二点呢就是在类的封装基础上,限定了外界的【控制访问方式】,若是想要给外界访问的就设置为共有public,不想给外界访问的就设置为私有private
    • 就比如说你去定义一个栈然后对其进行初始化,此时不能直接访问到这个类内部的成员变量,类会向外部提供一个公共的接口对私有的成员变量去进行一个访问
    Stack st;
    st.Init();
    
    • 这个共有的接口是类里面写好给你的,类写得没问题,那你用起来就不会有问题
    • 所以在C++中,我们要去获取栈顶元素的方式只有一种,那就是调用Top()成员函数,不需要去管内部的实现细节是怎样的,只需要调用就可以了
    • 而内部的数组和栈顶指针都设置为了私有,你是无法访问到的
    int top = StackTop(&st);		//✔
    int top = st->a[st->top - 1];	//❌
    

    讲得通俗一点,还是用我们上面讲到过的红绿灯例子

    • 行人老是闯红灯怎么办! 那就不让他过去了,把红绿灯给撤了,两遍围墙围起来不能通过马路,那怎么办呢?就弄一个高架桥,你想要过去只能走上面的高架桥,这也就一劳永逸杜绝了闯红灯的问题的,杜绝了安全隐患
  3. 调用函数比C语言要轻松一点,不用传入当前对象的地址

    • 如果你仔细观察C++和C语言实行去实现一个栈,不仅是类的封装这一块发生了大的改动,而且代码也简洁了不少,传入的参数均少了一个
    //C语言实现
    void PushStack(ST* st, STDataType x)
    PushStack(&st, 1);
    
    //C++实现
    void Stack::Push(const DataType& data)
    st.Push(1);
    
    • 那这么一对比确实有同学发现C++为何不用传当前对象的地址过去呢?
    • 那我想这位同学一定是忘了一个很重要的东西--> this指针。还记得this指针的原理吗 ?它是成员函数的一个隐藏形参,在当前对象调用成员函数时,当前对象的地址会连同其他参数一起压入栈中,VS中则是使用寄存器ecx来进行临时存放,最后再由成员函数中的this指针接受当前正在调用函数对象的地址,以此来访问不同对象的不同成员变量
    • 所以可以看出:对于C语言来说,是需要我们显式地传入当前对象的地址,是浮于水面的;对于C++来说,不需要我们去做这一件事,编译器自动会帮我们完成,是藏于水面下的

九、总结与提炼

到这里,就讲完了类的封装思想,我们来总结回顾一下

  • 首先我们聊到了【面向过程】与【面向对象】之间的区别,初步感受了C语言和C++两门语言特性的不同。为了引出C++中的类,便对比了structclass这两个关键字,知道了原来在C++中结构体可以这么玩,并且二者都可以用在来定义一个
  • 接下去呢我们就正式开始聊C++中的类,说到了三种访问限定符,puiblcprotectedprivate,若是加上了访问限定符后,类内的成员对外部来说就存在不同的限制。初次讲到了类的封装思想,将对象的属性(数据)与操作数据的方法结合在一块,起到了更好地管理
  • 写好一个类后,就可以用它定义出一个对象了,在这一模块,通过形象地案例向读者展示了【声明】和【定义】之间的区别,如果要将一个东西定义出来,就要实实在在为其开出一块空间
  • 将类实例化后,就可以通过这个对象去调用类中的向外提供的共有成员函数来说来操控私有成员变量,但是在计算类的大小时却发现【成员函数】似乎并不计算在内,通过探究发现原来其存在于一个公共代码区,为什么减少类内部的负担,将大家都要使用的东西像小区中的公共设施一般放在一个【公共代码区】,这样谁要用的时候通过对应的地址找到然后去调用一下即可。可是成员函数内部要如何知道是哪个对象来调用的我呢?
  • 此时我们谈到了一个重要知识点 —— this指针随着每次的传入的对象地址不同,隐式形参this指针就会通过不同的地址去找到内存中对应的那块地址中的成员变量,进行精准赋值
  • 最后,在学习了C++中的类后,便去实现了我们之前在数据结构中写过的【栈】,对二者进行对比可以发现,原来C++在封装这一块的思想确实考虑得很周到,很好地解决了C语言因为语法松散而导致的各种不安全性,进一步加深了对【封装思想】的理解

以上就是本文要介绍的所有内容,Thanks for reading, see you next article

在这里插入图片描述

你可能感兴趣的:(C++,c++,类和对象,封装)