写在前面
本章将正式开始学习C++中的面向对象,本篇博客涵盖讲解 访问限定符、封装的基础知识、类的作用域和实例化、探究类对象的存储和对于this指针由浅入深地讲解。
Ⅰ. 面向对象
0x00 初步认识
面向过程:关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。 面向对象:关注的是对象,将一件事情拆分为不同的对象,靠对象之间的交互完成。
举个栗子,比如设计简单的外卖系统 ~
面向过程:关注实现下单、接单、送餐这些过程。体现到代码层面 -- 方法/函数
面向对象:关注实现类对象及类对象间的关系,用户、商家、骑手以及他们之间的关系。体现到代码层面 —— 类的设计及类之间的关系。
再举一个比较下饭的例子!
面向对象:狗吃💩 —— 关注于谁去吃💩,狗去吃💩的,狗这个对象,对象有一个动作是吃狗💩。
面向过程:吃狗💩 —— 是一个动作,关注于怎么去吃狗💩。
有人说:面向过程不太适合去实现复杂的系统。
但是我个人认为这个说法是不准确的,比如 Linux 操作系统就是由纯C语言写的。
C++ 是基于面向对象的,支持面向过程和面向对象 "混编" 。
原因是 C++ 兼容 C,这个我们在本章也会慢慢探讨。
但是像 Java 这样的语言是纯面向对象。
只有面向对象,就算你想实现一个排序也要写一个类出来……
0x01 类的引入
📚 我们来由浅入深地引入一下,我们先做一个的了解:
在C语言中,结构体中只能定义变量……
而在C++中,结构体内不仅可以定义变量,还可以定义函数!
💬 我们先来试着写一个最简单的类:
struct Student { char name[10]; int age; int id; };
诶!这不就是 C语言 的结构体吗?
✅ 确实!但是在 C++ 里,struct 也跟着升级成了类。
我们可以使用 struct 或 class 来定义类,它们之间有什么区别我们后面再说。
💡 因为 C++ 兼容 C 里面结构体的用法,同时 struct 也在 C++ 中升级成了类。
💬 所以 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 还兼容了 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++ 中一般称这些变量为成员变量,称这些函数为成员方法。
这个时候似乎发现了一些新的问题!
我们的这个参数名取的好像和成员变量里一样了,比如我们访问 name 的时候到底是成员变量里的 name 还是成员方法里的 name 呢?
这就让人区分不开了……
为了能够更好的区分哪个是成员变量,我们在定义成员变量名时可以给它们做一些标记:
下面是几种常见的表示为成员变量的 "风格" :
① 前面加斜杠 :
char _name[10];
② 后面加斜杠:
char name_[10]
③ 前面加个 m (表示成员 member):
char mname[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 <iostream> 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; } };
💬 我们来调用它们进行一个打印:
#include <iostream> 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++ 对我们的 struct 进行升级了,升级为类了。它兼容以前的用法,又有了新的用法。
0x02 class 关键字
我们刚才引入部分讲了 struct ,我们知道了它在 C++ 里升级成了类。其实 C++ 也有自己的亲儿子,就是 class,我们现在就来好好地讲一讲这个 class 。
📚 语法:
① class 为定义类的关键字,className 为类的名字。
② 大括号中内容为类的主题,注意类定义结束时后面要加分号!
💬 我们先试着把刚才写的代码改成 class:
#include <iostream> 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++ 亲儿子 class 居然报错了,这又是为什么呢?
因为 C++ 讲究 "封装" ……
C++ 这里是它把数据和方法都放在了类里面。
这和C语言是不同的,C语言里数据是数据,方法是方法。
这里我们就来提一下 面向对象的三大特性:封装、继承、多态。
📚 我们先来重点看一下这个 封装 :
① 数据和方法都被放在了一起。
② 访问限定符
就是因为这个访问限定符,所以这里我们报错了,我们下面来学习一下访问限定符。
Ⅱ. 类的访问限定符及封装
0x00 访问限定符
📚 C++ 实现封装的方式:用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性地将其接口提供给外部的用户使用。
一共有三种访问限定符,分别是 public(公有)、protected(保护)、private(私有)。
这一听名字就能知道,公有就是随便玩,保护和私有就是藏起来一点点不让你随便玩得到。
访问限定符说明
① public 修饰的成员,可以在类外面随便访问(直接访问)。
② protected 和 private 修饰的成员,不能在类外随便访问。
(此处 protected 和 private 是类似的,现在你可以认为他们是一样的,后面我们讲继承的时候才能体现出它们两的区别)
这就分出了两个阵营,一个阵营是可以随便访问的,一个阵营是不能随便访问的。
③ class 的默认访问权限为 private,struct 为 public !
这就是为什么我们刚才编译会报错,因为 class 默认访问权限是 private!
而之前我们用 struct 没有问题,是因为 struct 默认访问权限是 public 共有的。
好家伙,这下破案了!原来刚才报错的原因是 class 默认是私有的,
❓ 那好,既然知道问题所在了,我们该如何解决让它成功访问呢?
💡 使用访问限定符就可以了!
💬 使用我们的访问限定符,加一个 public :
#include <iostream> 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; }
🚩 运行结果如下:
搞定!我们再来细说一下刚才加进去的访问限定符。
📚 在这之前我们再说两个知识点:
③ 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
④ 如果后面没有访问限定符,作用域就到 { 类结束。
也就是说,我们刚才加进去的 public ,
从它开始到下一个访问限定符出现为止的这块范围,都是共有的了,
但是因为后面没有再次出现访问限定符,所以作用域就到类结束为止,我们看范围图:
💬 我们再加一个访问限定符 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 出现前为止了,我们再来看看它们的范围图:
这,就是访问限定符在这里起到的一个作用。
📌 注意事项:
① 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
② 我们一般在定义类的时候,建议明确定义访问限定符,不要用 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 什么是封装
📚 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来实现对象进行交互。
① 把数据都封装到类里面。
② 可以给你访问定义成公有,不想给你访问的定义成私有或者保护。
0x02 封装的意义
❓ 封装的意义是什么?
封装是一种更好的严格管理,不封装是一种自由管理。
❓ 那么是严格管理好,还是自由管理好呢?
举一个YQ防控的例子:
某国单日新增一百万,你说是自由的管理好呢?还是严格的管理好呢?
我们和某国其实都是在控制YQ的,但是我们是严格的管理,控制疫情。
而某国是自由管理,虽然人人高呼 "Freedom" ,但是疫情一直都难以得到控制。
0x03 封装的本质
📚 封装的本质是一种管理。
我们是如何管理疫情的呢?
比如进商场,如果YQ期间没有人管理,
让大家都随意进,那YQ就不好控制了。
所以我们要对商场进行很好的防疫管理措施!
那么我们首先要把商场 "封装" 起来,你想进入商场就必须要扫码。
并不是说不让你进商场,而是你必须要走正门扫码才可以进入。
通过扫码,是绿码你才能进商场,在疫情防疫合理的监管机制下进商场。
类也是一样,我们使用类数据和方法都封装到了一起,不想让人随意来访的,
就是用 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; };
📌 注意事项:
成员函数如果在类中定义,编译器可能会将它当作内联函数来处理。
注意,是可能。并不是说一定会成为内联函数,之前讲内联函数的时候我们也说了。
内联函数对编译器来说也只是一个建议。至于到底会不会成为内联是由编译器来决定的。
这也不取决于编译器心情,心情好就让你成为内联,心情差就不让……
而是!取决于编译器看这个函数符不符合条件,一般一个函数太长(大概是十几行左右),
或者函数是一个递归,编译器就不会让它成为内联了。
② 声明和定义分离:
💬 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; }
诶,这函数名前的 : : 是什么?我们继续往下看~
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; }
0x02 类的实例化
首先要说清楚的是:类本身是没有存储空间的。
通过类建立出对象,即实例化,才会有实际的存储空间。
📚 我们把用类类型创建对象的过程称为 —— 类的实例化。
① 类只是一个像 "图纸" 一样的东西,限定了类有哪些成员。定义出一个类,并没有分配实际的内存空间来存储它。
② 一个类可以实例化出多个对象,占用实际的物理空间,存储类成员的变量。
💡 举个例子:
类实例化对象就像是在现实中使用设计图建造房子,类就像是设计图。
你可以根据这个设计图造出很多栋楼出来。
只有楼建好了里面才能住人,你才能放家具、放东西进去。
设计图能住人吗?当然是不能!因为并没有分配实际的内存空间。
只有你照着设计图去建造,即实例化出对象,占用实际的物理空间,才能住人。