【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针

    火速猛戳订阅   《C++要笑着学》    趣味教学博客 

   [ 本篇博客热榜最高排名:13 ]

 

写在前面:

 我是柠檬叶子C,我又来了!本章将正式开始学习C++中的面向对象,本篇博客涵盖讲解 访问限定符、封装的基础知识、类的作用域和实例化、探究类对象的存储和对于this指针由浅入深地讲解。如果觉得不错,可以 "一键三连" 支持一下博主!

你们的关注就是我更新的最大动力!Thanks ♪ (・ω・)ノ


Ⅰ. 面向对象

0x00 初步认识

面向过程:关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

面向对象:关注的是对象,将一件事情拆分为不同的对象,靠对象之间的交互完成。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第1张图片 举个栗子,比如设计简单的外卖系统 ~

面向过程:关注实现下单、接单、送餐这些过程。体现到代码层面 -- 方法/函数

面向对象:关注实现类对象及类对象间的关系,用户、商家、骑手以及他们之间的关系。体现到代码层面 —— 类的设计及类之间的关系。

 【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第2张图片 再举一个比较下饭的例子!

面向对象:狗吃屎 —— 关注于谁去吃屎,狗去吃屎的,狗这个对象,对象有一个动作是吃狗屎。

面向过程:吃狗屎 —— 是一个动作,关注于怎么去吃狗屎。

 有人说:面向过程不太适合去实现复杂的系统。

  但是我个人认为这个说法是不准确的,比如 Linux 操作系统就是由纯C语言写的。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第3张图片C++ 是基于面向对象的,支持面向过程和面向对象 "混编" 。

原因是 C++ 兼容 C,这个我们在本章也会慢慢探讨。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第4张图片 但是像 Java 这样的语言是纯面向对象。

只有面向对象,就算你想实现一个排序也要写一个类出来……

0x01 类的引入

我们来由浅入深地引入一下,我们先做一个的了解:

在C语言中,结构体中只能定义变量……

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第5张图片  而在C++中,结构体内不仅可以定义变量,还可以定义函数!

我们先来试着写一个最简单的类:

struct Student {
	char name[10];
	int age;
	int id;
};

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第6张图片 诶!这不就是 C语言 的结构体吗?

✅ 确实!但是在 C++ 里,struct 也跟着升级成了类。 

 我们可以使用 structclass 来定义类,它们之间有什么区别我们后面再说。

因为 C++ 兼容 C 里面结构体的用法,同时 struct 也在 C++ 中升级成了类。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第7张图片

所以 C++ 就可以直接使用类名来定义了:

int main(void)
{
	struct Student s1;  // 兼容C
	Student s2;         // C++就可以直接使用类名,Student类名,也是类型。

	strcpy(s1.name, "小明");
	s1.id = 10001;
	s2.age = 20;

	strcpy(s2.name, "小红");
	s2.id = 10002;
	s2.age = 19;

	return 0;
}

解读:

  我们既能用 struct Student s1 来定义,还能直接使用 Student s2,通过使用类名直接定义。这体现了 C++ 兼容 C 的特点!

 但是如果这是在 C语言 里, stuct Student 才是它的类型,直接使用 Student 定义是不可以的(当然,用 typedef 就另当别论了)。

我们通过调试模式来看看:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第8张图片

它其实就是一个结构,你可以理解成和我们之前学的结构体是 "一样的" ,只是定义的方式既兼容了 C 还兼容了 C++ ,但是下面我们还会认识到一些它的不同之处,我们继续往下看。

 除了类名的定义,还有什么新的玩法呢?

如果是在C语言里,结构体里只能定义变量,就是一个多个变量的集合。

如果我们想要将 s1 中的变量进行初始化,我们还得一个个写,真的是很麻烦:

int main(void)
{
	struct Student s1;
	Student s2;

	strcpy(s1.name, "小明");
	s1.id = 10001;
	s2.age = 20;

	strcpy(s2.name, "小红");
	s2.id = 10002;
	s2.age = 19;

	return 0;
}

 但是在C++里,我们不仅可以定义变量,还可以定义函数(方法)。

我们可以定义一个 "初始化" 函数:

struct Student {
    /* 成员变量 */
	char name[10];
	int age;
	int id;

    /* 成员方法 */
	void Init(const char* name, int age, int id) {
		...
	}
};

我们在 C++ 中一般称这些变量为成员变量,称这些函数为成员方法

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第9张图片 这个时候似乎发现了一些新的问题!

我们的这个参数名取的好像和成员变量里一样了,比如我们访问 name 的时候到底是成员变量里的 name 还是成员方法里的 name 呢?

 这就让人区分不开了……

为了能够更好的区分哪个是成员变量,我们在定义成员变量名时可以给它们做一些标记:

下面是几种常见的表示为成员变量的 "风格" :

① 前面加斜杠 :

	char _name[10];

② 后面加斜杠:

    char name_[10]

③ 前面加个 m (表示成员 member):

    char mname[10]

……

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第10张图片 这个并没有明确的规定,不同的公司也有不同的风格。

我个人喜欢用前面加杠的方式来区分,看上去比较显眼。

这样就可以区分开来了:

struct Student {
	/* 成员变量 */
	char _name[10];
	int  _age;
	int  _id;

	/* 成员函数 */
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}
};

 为了方便测试,我们再来写一个简单的打印函数:

#include 
using namespace std;

struct Student {
	/* 成员变量 */
	char _name[10];
	int  _age;
	int  _id;

	/* 成员函数 */
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}

	void Print() {
		cout << _name << " " << _age << " " << _id << endl;
	}
};

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第11张图片

  我们来调用它们进行一个打印: 

#include 
using namespace std;

struct Student {
	/* 成员变量 */
	char _name[10];
	int  _age;
	int  _id;

	/* 成员函数 */
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}

	void Print() {
		cout << _name << " " << _age << " " << _id << endl;
	}
};


int main(void)
{
	struct Student s1;
	Student s2;

	/* 初始化 */
	s1.Init("小明", 20, 10001);
	s2.Init("小红", 19, 10002);

	/* 打印 */
	s1.Print();
	s2.Print();

	return 0;
}

运行结果如下:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第12张图片

总结:C++ 对我们的 struct 进行升级了,升级为类了。它兼容以前的用法,又有了新的用法。

0x02 class 关键字

 我们刚才引入部分讲了 struct ,我们知道了它在 C++ 里升级成了类。其实 C++ 也有自己的亲儿子,就是 class,我们现在就来好好地讲一讲这个 class

语法:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第13张图片

class 为定义类的关键字,className 为类的名字。

② 大括号中内容为类的主题,注意类定义结束时后面要加分号!

我们先试着把刚才写的代码改成 class

#include 
using namespace std;

class Student {
	/* 成员变量 */
	char _name[10];
	int  _age;
	int  _id;

	/* 成员函数 */
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}

	void Print() {
		cout << _name << " " << _age << " " << _id << endl;
	}
};

int main(void)
{
	struct Student s1;
	Student s2;

	/* 初始化 */
	s1.Init("小明", 20, 10001);
	s2.Init("小红", 19, 10002);

	/* 打印 */
	s1.Print();
	s2.Print();

	return 0;
}

运行结果如下:(报错)

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第14张图片

啊这,我们换成 C++ 亲儿子 class 居然报错了,这又是为什么呢?

 因为 C++ 讲究 "封装" ……

C++ 这里是它把数据和方法都放在了类里面。

这和C语言是不同的,C语言里数据是数据,方法是方法。

 这里我们就来提一下 面向对象的三大特性:封装、继承、多态。

我们先来重点看一下这个 封装

① 数据和方法都被放在了一起。

② 访问限定符

就是因为这个访问限定符,所以这里我们报错了,我们下面来学习一下访问限定符。

Ⅱ. 类的访问限定符及封装

0x00 访问限定符

C++ 实现封装的方式:用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性地将其接口提供给外部的用户使用。

一共有三种访问限定符,分别是 public(公有)、protected(保护)、private(私有)。

这一听名字就能知道,公有就是随便玩,保护和私有就是藏起来一点点不让你随便玩得到。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第15张图片

访问限定符说明

public 修饰的成员,可以在类外面随便访问(直接访问)。

protectedprivate 修饰的成员,不能在类外随便访问。

  (此处 protected 和 private 是类似的,现在你可以认为他们是一样的,后面我们讲继承的时候才能体现出它们两的区别)

这就分出了两个阵营,一个阵营是可以随便访问的,一个阵营是不能随便访问的。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第16张图片

③ class 的默认访问权限为 privatestruct public

这就是为什么我们刚才编译会报错,因为 class 默认访问权限是 private

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第17张图片

而之前我们用 struct 没有问题,是因为 struct 默认访问权限是 public 共有的。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第18张图片 好家伙,这下破案了!原来刚才报错的原因是 class 默认是私有的,

❓ 那好,既然知道问题所在了,我们该如何解决让它成功访问呢?

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第19张图片  

使用访问限定符就可以了!

使用我们的访问限定符,加一个 public

#include 
using namespace std;

class Student {
	char _name[10];
	int  _age;
	int  _id;

public:
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}

	void Print() {
		cout << _name << " " << _age << " " << _id << endl;
	}
};

int main(void)
{
	struct Student s1;
	Student s2;

	/* 初始化 */
	s1.Init("小明", 20, 10001);
	s2.Init("小红", 19, 10002);

	/* 打印 */
	s1.Print();
	s2.Print();

	return 0;
}

运行结果如下:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第20张图片

 搞定!我们再来细说一下刚才加进去的访问限定符。

在这之前我们再说两个知识点:

③ 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。

④ 如果后面没有访问限定符,作用域就到 { 类结束。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第21张图片 也就是说,我们刚才加进去的 public 

从它开始到下一个访问限定符出现为止的这块范围,都是共有的了,

但是因为后面没有再次出现访问限定符,所以作用域就到类结束为止,我们看范围图:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第22张图片

我们再加一个访问限定符 private 进去看看:

class Student {
	char _name[10];
	int  _age;
	int  _id;

public:
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}

private:
	void Print() {
		cout << _name << " " << _age << " " << _id << endl;
	}
};

现在, public 能影响到的范围就到 private 出现前为止了,我们再来看看它们的范围图:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第23张图片

 这,就是访问限定符在这里起到的一个作用。

注意事项:

① 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。

② 我们一般在定义类的时候,建议明确定义访问限定符,不要用 struct / class 的默认的访问权限,就像这样:

class Student {
private:
	char _name[10];
	int  _age;
	int  _id;

public:
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}
	void Print() {
		cout << _name << " " << _age << " " << _id << endl;
	}
};

虽然你不指定他会有默认限定,但是还是建议你明确写出来,

因为这样能让人一眼就看出它是共有的还是私有的。

0x01 什么是封装

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第24张图片

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来实现对象进行交互。

① 把数据都封装到类里面。

② 可以给你访问定义成公有,不想给你访问的定义成私有或者保护。

0x02 封装的意义

❓ 封装的意义是什么?

 封装是一种更好的严格管理,不封装是一种自由管理。

❓ 那么是严格管理好,还是自由管理好呢?

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第25张图片 举一个最简单的例子:

比如最近的疫情,漂亮国单日新增一百万,你说是自由的管理好呢?还是严格的管理好呢?

我们和漂亮国其实都是在控制疫情的,但是我们是动态清零的政策,是非常严格的管理。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第26张图片 而漂亮国是自由管理,虽然人人高呼 "Freedom" ,但是疫情一直都难以得到控制。

0x03 封装的本质

封装的本质是一种管理。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第27张图片

我们是如何管理疫情的呢?

比如进商场,如果疫情期间没有人管理,

让大家都随意进,那疫情就不好控制了。

所以我们要对商场进行很好的防疫管理措施!

那么我们首先要把商场 "封装" 起来,你想进入商场就必须要扫健康码。

并不是说不让你进商场,而是你必须要走正门扫码才可以进入。

通过扫码,是绿码你才能进商场,在疫情防疫合理的监管机制下进商场。

类也是一样,我们使用类数据和方法都封装到了一起,不想让人随意来访的,

就是用 protected / private 把成员封装起来,开放一些共有的成员函数对成员合理的访问。

 所以,封装是一种更好、更严格的管理!

Ⅲ.  类的作用域和实例化

0x00 类定义的两种方式

① 申明和定义全部放在类中

class Student {
public:
	void Init(const char* name, int age, int id) {
		strcpy(_name, name);
		_age = age;
		_id = id;
	}
	void Print() {
		cout << _name << " " << _age << " " << _id << endl;
	}

private:
	char _name[10];
	int  _age;
	int  _id;
};

注意事项:

成员函数如果在类中定义,编译器可能会将它当作内联函数来处理。

注意,是可能。并不是说一定会成为内联函数,之前讲内联函数的时候我们也说了。

内联函数对编译器来说也只是一个建议。至于到底会不会成为内联是由编译器来决定的。

这也不取决于编译器心情,心情好就让你成为内联,心情差就不让……

而是!取决于编译器看这个函数符不符合条件,一般一个函数太长(大概是十几行左右),

或者函数是一个递归,编译器就不会让它成为内联了。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第28张图片

② 声明和定义分离:

test.h:

class Student {
public:
	void Init(const char* name, int age, int id);
	void Print();

private:
	char _name[10];
	int  _age;
	int  _id;
};

test.cpp:

#include test.h


void Student::Init(const char* name, int age, int id) {
    strcpy(_name, name);
	_age = age;
	_id = id;
}

void Student::Print() {
	cout << _name << " " << _age << " " << _id << endl;
}


【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第29张图片 诶,这函数名前的 : : 是什么?我们继续往下看~

0x01 类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。

在类外定义成员,需要使用作用域解析符 : : 来指明成员属于哪个类域。 

 比如说我们写一个比较正式一点的项目(申明和定义分离)

Stack.h:

class Stack {
	public:
		void Init();
		void Push(int x);
        // ...

	private:
		int* _array;
		int  _top;
		int  _capacity;
};

Stack.cpp:

#include "Stack.h"
// 这里需要指定 Init 是属于 Stack 这个类域
// 我们使用 :: 来指定
void Stack::Init() {
	_array = nullptr;
	_top = _capacity = 0;
}

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

	return 0;
}

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第30张图片

0x02 类的实例化

首先要说清楚的是:类本身是没有存储空间的。

通过类建立出对象,即实例化,才会有实际的存储空间。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第31张图片

我们把用类类型创建对象的过程称为 —— 类的实例化。

① 类只是一个像 "图纸" 一样的东西,限定了类有哪些成员。定义出一个类,并没有分配实际的内存空间来存储它。

② 一个类可以实例化出多个对象,占用实际的物理空间,存储类成员的变量。

举个例子:

类实例化对象就像是在现实中使用设计图建造房子,类就像是设计图。

你可以根据这个设计图造出很多栋楼出来。

只有楼建好了里面才能住人,你才能放家具、放东西进去。

设计图能住人吗?当然是不能!因为并没有分配实际的内存空间。

只有你照着设计图去建造,即实例化出对象,占用实际的物理空间,才能住人。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第32张图片

Ⅳ.  类对象模型

0x00 计算类的存储大小

 类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?我们该如何计算一个类的大小呢?

❓ 比如这个栈和它定义出来的对象是多大呢?

Stack.h

#include 

class Stack {
	public:
		void Init();
		void Push(int x);

	private:
		int* _array;
		int  _top;
		int  _capacity;
};

Stack.cpp

#include "Stack.h"
using namespace std;

void Stack::Init() {
	_array = nullptr;
	_top = _capacity = 0;
}

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

	cout << sizeof(Stack) << endl;
	cout << sizeof(s) << endl;

	return 0;
}

运行结果如下:(64位环境)

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第33张图片

 对象中存了成员变量,是否存了成员函数呢?没存成员函数!

计算类或类对象的大小只看成员变量!

并且要考虑内存对齐,C++内存对齐规则和C结构体一致。

0x01 类对象的存储方式猜测

① 对象中包含类的各个成员:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第34张图片

 缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次会浪费空间。

② 只保存成员变量,成员函数存放在公共的代码段:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第35张图片

❓ 对于上述两种存储方式,那计算机到底是按照哪种方式来存储的呢?

// 类中既有成员变量,又有成员函数
class A1 {
public:
	void f1() {}
private:
	int _a;
};

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

// 类中什么都没有 - 空类
class A3
{};

sizeof(A1): 4

sizeof(A2): 1

sizeof(A3): 1

❓ A2 没有成员变量,A3 更是什么都没有,为什么大小是 1 呢?为什么不是 0 呢?

int main(void)
{
    A2 aa;
    A2 bb;
    cout << &aa << endl;
    cout << &bb << endl;
}

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第36张图片

我们尝试给 aa 对象 和 bb 对象取地址,它们是有地址的,

取地址就是要拿出它存储空间的那块,所以这里总不能给一个空指针吧?

如果大小给 0 的话就没办法区分空间了。

所以,空类会给 1 字节,这 1 字节不存储有效数据,只是为了占个坑,表示对象存在。

结论:一个类的大小,实际就是该类中成员变量之和。当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

0x02 内存对齐规则

 C++ 内存对齐规则和 C语言 中的结构体内存对齐规则是一样的。

(以下内容是C语言教学部分的内容,如果没印象了可以复习一下)

我们先来观察下面的代码:

#include 

struct S
{
    char c1; // 1
    int i; // 4
    char c2; // 1
};

int main()
{
    struct S s = { 0 };
    printf("%d\n", sizeof(s));

    return 0;
}

  12

❓ 为什么是12呢?这就涉及到结构体内存对齐的问题了。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第37张图片

结构体的对齐规则:

① 结构体的第一个成员放在结构体变量在内存中存储位置的0偏移处开始。

② 从第2个成员往后的所有成员,都要放在一个对齐数(成员的大小和默认对齐数的较小值)的整数的整数倍的地址处。VS 中默认对齐数为8!

③ 结构体的总大小是结构体的所有成员的对齐数中最大的那个对齐数的整数倍。

④ 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

注意事项:VS 中默认对其数为8,Linux中没有默认对齐数概念!

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第38张图片

Ⅴ.  this指针

0x00 this指针的引出

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第39张图片

为了能够更好地讲解,我们首先来定义一个日期类 Date:

#include 
using namespace std;

class Date {
public:
	void Init(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main(void)
{
	Date d1;
	d1.Init(2022, 3, 7);
	d1.Print();

	Date d2;
	d2.Init(2022, 5, 20);
	d2.Print();

	return 0;
}

运行结果:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第40张图片

❓ 这里我们思考一个问题:

Date 类中有 Init Print 两个成员函数,函数体中没有关于不同对象的区分,那当 d1 调用 Print 函数时,这个 Print 函数是如何知道要打印 d1 对象的?而不是去打印 d2 对象呢? 

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第41张图片

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第42张图片

 因为 C++ 在这有一个隐藏的东西 —— this 指针!

C++ 通过引入 this 指针解决该问题。

 C++ 编译器给每个 "非静态的成员函数" 增加了一个隐藏的指针参数,

让该指针指向当前对象(函数运行时调用该函数的对象),它是系统自动生成的,

在函数体中所有成员变量的操作,都是通过该指针去访问。

只不过所有的操作对程序员来说是透明的,

就是不需要程序员自己来传递,编译器自动帮你去完成。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第43张图片

当然,Init 也会被处理(注释的是隐藏前的内容):

#include 
using namespace std;

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

	// void Print(Date* this)
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
		// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main(void)
{
	Date d1;
	d1.Init(2022, 3, 7);    // d1.Init(&d1, 2022, 3, 7);
	d1.Print();             // d1.Print(&d1);

	Date d2;
	d2.Init(2022, 5, 20);   // d2.Init(&d2, 2022, 5, 20);
	d2.Print();             // d2.Print(&d2);

	return 0;
}

0x01 this 使用细则

注意事项:this 是作为一个关键字存在的

① 调用成员函数时,不能 "显示地" 传实参给 this :

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第44张图片

② 定义成员函数时,也不能 "显示地" 声明形参 this :

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第45张图片

③ 但是,在成员函数内部,我们可以 "显示地" 使用 this :

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第46张图片

也就是说,你不写 this 他会自动加,你写了他也是允许你写的。

❓ 这是为什么呢? 因为有些地方我们用得到这个 this,这个我们下一章会讲。(返回本体)

虽然可以 "显示地" 用,但是一般情况下我们都不会自己 "显示地写" 。

因为没有必要,你不写他也会自动加上去的:

void Print() {
    cout << _year << "-" << _month << "-" << _day << endl;
	// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}

 但是你想写也没人会拦你,简单说就是 ——  

 【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第47张图片

0x03 this指针中的const

注意事项: this 指针是不能被改变的。

 【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第48张图片因为考虑到这是第一次讲解 this 指针,为了方便由浅入深地讲解,

我们选择了 —— 战术装瞎 ,忽略了 this 指针中的 const 来讲解的。

 其实 this 指针还被 const 修饰。

this 指针的本质是一个常量指针,是通过 const 修饰 this 指针指向的内存空间。

不信?你改改看:

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第49张图片

  

写得更准确些(注释的是隐藏前的内容):

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第50张图片

class Date {
public:
	// void Init(Date* const this, int year, int month, int day)
	void Init(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}

	// void Print(Date* const this)
	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
		// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

0x04 this指针的特性

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第51张图片

① this 指针的类型:  类类型* const

② this 指针只能在 "成员函数" 的内部使用。

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

④ this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。

【C++要笑着学】类和对象 | 初识封装 | 访问限定符 | 类的作用域和实例化 | 类对象模型 | this指针_第52张图片


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. .

笔者:王亦优

更新: 2022.3.7

❌ 勘误:暂无

声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

本章完。

你可能感兴趣的:(《C++要笑着学》,c++,面向对象)