前言
我是柠檬叶子C。本篇将对之前讲的面向对象的内容进行一个梳理,通过举一些例子去感受C和C++之间的区别和联系。举了一个比较有意思的胡编乱造的故事(bushi)。文章的最后会再次理解一些概念,强调封装的意义,加深对 "面向对象" 的理解。如果觉得文章不错,可以 "一键三连" 支持一下博主!你们的关注就是我更新的最大动力!
(预告:下一期排版将会有重大更新,观看体验将会更上一层楼!)
Ⅰ. 面向对象总结
【C++要笑着学】类和对象(1)初识封装 | 访问限定符 | 作用域和实例化 | 类对象模型 | this指针
【C++要笑着学】面向对象(2)类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数
【C++要笑着学】面向对象(3)运算符重载 | 赋值重载 | 取地址重载 | const成员
【C++要笑着学】面向对象(4)从零开始实现日期类 | 体会代码的复用 | 提供完整代码
【C++要笑着学】面向对象(5)友元 | 初始化列表 | explicit关键字 | static 静态成员 | 内部类
Ⅱ. 对比 C++ 和 C 定义的区别和联系
0x00 引入:用栈来举例
为了能加深理解,我们编一个比较有意思的故事,
就拿实现栈()来举例子吧。
0x01 回想:C语言中实现的方式
C语言中定义一个栈,我们需要些什么呢?结构和函数。
💬 Stack.h
#define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> typedef int StackDataType; typedef struct Stack { StackDataType* array; //数组 int top; //栈顶 int capacity; //容量 } Stack; void StackInit(Stack* pst); void StackDestroy(Stack* pst); bool StackIsEmpty(Stack* pst); void StackPush(Stack* pst, StackDataType x); void StackPop(Stack* pst); StackDataType StackTop(Stack* pst); int StackSize(Stack* pst);
💬 Stack.c
#include "Stack.h" /* 初始化 */ void StackInit(Stack* pst) { assert(pst); // 防止pst为空 pst->array = NULL; pst->top = 0; // pst->top = -1 pst->capacity = 0; } /* 销毁 */ void StackDestroy(Stack* pst) { assert(psl); // 防止pst为空 free(pst->array); pst->array = NULL; pst->capacity = pst->top = 0; } /* 判断栈是否为空*/ bool StackIsEmpty(Stack* pst) { assert(pst); //防止pst为空 return pst->top == 0; //等于0就是空,就是真 } /* 进栈 */ void StackPush(Stack* pst, StackDataType x) { assert(pst); // 防止pst为空 // 检查是否需要增容 if (pst->top == pst->capacity) { int new_capacity = pst->capacity == 0 ? 4 : pst->capacity * 2; StackDataType* tmp_arr = realloc(pst->array, sizeof(StackDataType) * new_capacity); // 防止realloc翻车 if (tmp_arr == NULL) { printf("realloc failed!\n"); exit(-1); } // 更新 pst->array = tmp_arr; pst->capacity = new_capacity; } // 填入数据 pst->array[pst->top] = x; pst->top++; } /* 出栈 */ void StackPop(Stack* pst) { assert(pst); //防止pst为空 //assert(pst->top > 0); //防止top为空 assert(!StackIsEmpty(pst)); pst->top--; } /* 返回栈顶数据 */ StackDataType StackTop(Stack* pst) { assert(pst); //防止pst为空 //assert(pst->top > 0); //防止top为空 assert(!StackIsEmpty(pst)); return pst->array[pst->top - 1]; } /* 计算栈的大小 */ int StackSize(Stack* pst) { assert(pst); //防止pst为空 // 因为我们设定 top 是指向栈顶的下一个,所以top就是size return pst->top; }
它是面向过程的,数据和方法是分离的,
数据:定义一个结构,结构体里包含了各种数据,数组的空间、栈顶位置、容量大小……
我们要操纵数据时,我们得把栈对象的地址传过去。
我们在使用它的时候我们要定义一个栈的结构,比如我们要做一个 StackPush 的操作:
💬 如果你此时不取地址,而是直接把 st 传过去 :
void TestStack_in_C() { Stack st; StackPush(&st, 10); }
那么它就是一种传值,形参是实参的一份临时拷贝,那形参里对它进行的改变都是无用功。
因为你改变的只是形参,这没有任何意义,它活在栈帧里,函数销毁了也就跟着销毁了。
我们是怎么做的???
💬 C语言的解决方式是传实参的地址过去,我们来多插点数据:
void TestStack_in_C() { Stack st; StackPush(&st, 10); StackPush(&st, 20); StackPush(&st, 30); StackPush(&st, 40); }
用完之后我们还需要 Destroy 一下:
void TestStack_in_C() { Stack st; StackPush(&st, 10); StackPush(&st, 20); StackPush(&st, 30); StackPush(&st, 40); StackDestroy(&st); }
其实还是挺麻烦的,我举两个例子。
比如你看看我刚才写的,有没有发现到我这里是忘记调 Init 进行初始化的?
有时候就是会写着写着发现忘记调了,不乏有人就会经常忘记调 Init,Destroy。
再举个例子,每次传对象都要取个地址,感觉很烦索,我们调它们都得传:
StackPush(&st, 10); StackPush(&st, 20); StackPush(&st, 30); StackPush(&st, 40);
(还浪费键盘寿命,Shift + 数字7 打出&,开玩笑开玩笑)
0x02 感同身受:把自己带入到故事里
我们来幻想一下五十年前,C++之父当时是怎么想的。
C++之父的编程水平上天入地无所不能,写出的代码鬼斧神工强到令人窒息。
大佬当时在实验室用C语言开发东西,大佬有一天突然觉得C这用起来好像不是很爽,
正好最近他比较闲,大佬想着要么设计一个新语言玩玩。
但是大佬想了想,C语言其实也挺好的,
就是有些地方好像用起来有点不顺手,
那就把它改造一下吧,既然是改造,那就给他取名为 C++ 吧,多酷!
大佬最近写了个栈,他觉得这种的方式实在是把写烦了,他想把这好好的改一改,
而且老是有人乱用,大佬的 StackTop 函数写的好好的,有些人就是不用,非要:
printf("%d\n", st.array[st.top]);
自己擅自访问数据,打印出来还是个随机值,还跑过来质问大佬 —— "这个栈是怎么写的?"
大佬跟这货解释了半天,栈的原理,这货终于能够明白了,
原来大佬写的栈最开始的初始值给的是 0,
只要老老实实用大佬写的 StackTop 就不会有这种问题,
大佬感觉别人可以私自访问这里的数据,是很不爽的事,因为隔三岔五就有人来问。
最近的来他这的实习生也很多,
每次都是这些问题来找大佬,大佬每次都要给他们解释一遍,
大佬实在是不想解释了,所以就想给C语言好好的改一改。
自由不是好事,这太自由了,我要让这些人访问不了我的一些东西,访问了就会报错,
这样它们就会乖乖的用我写的 StackTop,就不会有人来烦我了。
大佬终于决定给这 C语言 好好改造改造了!
五十年前的某一天深夜,大佬就抽着烟坐在电脑旁深思……
我该如何设计这个语言呢~ 先做好能把这些东西封装起来的功能吧!
就像这样……
class Stack { };
大佬又想了想,嗯!不能让你们随便访问数据了!不再纵容数据和方法分离了!
把数据和函数都往这里面放。我也不想让人能随随便便调这些数据,然后跑过来问我怎么回事,
我直接不让你们访问了,看你们还怎么乱玩:
class Stack { // 函数 ... private: int* _array; int _top; int _capacity; };
能给你玩的就是这些函数,让他们拿去用:
class Stack { public: void Init(Stack* ps); void Push(Stack* ps, int x); int Top(Stack* ps); void Destory(Stack* ps); private: int* _array; int _top; int _capacity; };
这里取名也没有必要叫 StackInit、StackPush了,反正都是在 Stack 里面的。
终于搞定封装了,终于把这个麻烦给甩掉了,
以后不会再有人跑过来问我为什么我写的 top 是最后一个数据的下一个位置了。
让他们用都用不了,以后要访问栈顶数据,就要调用我的 Top 接口。
大佬抽着烟,想着第一个问题终于是解决了,非常滴开心。
大佬愉快的喝了几杯酒,睡了一觉,第二天早上起来的时候,
把这些代码用了用 —— 想起了之前,有人总是忘记调用 Init 和 Destroy,
正常情况下定义一个栈出来,我们想让它要初始化的,
能不能让它在创建的时候就自动调用呢?
接着大佬就发明了 构造函数 和 析构函数。
构造函数反正要完成的是初始化的工作,也不需要返回值,就把返回值给去掉吧,
名字就是类的名字好了:
Stack(Stack* ps);
析构函数又是反着的,是完成销毁的工作,C语言有个符号是 ~,是取反,
那就给他这样表示吧:
~Stack(Stack* ps);
大佬搞了一个通宵,把构造函数和析构函数的细节敲定了。
于是用的时候就会自动调用构造函数和析构函数了。
大佬过几天用的时候又想起了每次调用一些接口都要传一下地址的事,
void TestStack_in_CPP() { Stack st; Push(&st, 10); Push(&st, 20); Push(&st, 30); Push(&st, 40); }
想了想,烦死了,以前我就不想传,现在正好一并解决!直接让它自动传好了!
于是搞了一个叫 this 指针 的东西,我直接让编译器让成员函数都自动加上这个 this,
我们这下直接写都不用写了:
class Stack { public: Stack(); void Push(int x); int Top(); ~Stack(); // ... }; void TestStack_in_CPP() { Stack st; Push(10); Push(20); Push(30); Push(40); }
这样多和谐!多美好!大佬现在感觉舒服多了。
(来自于杭哥讲的故事)
这些故事当然都是编的,但是通过这个我们可以体会到大佬一开始对C语言进行魔改,
是有原因的,从这个角度来看,一些事情就很好理解了。
为什么要封装?因为老是有人乱调啊!
为什么会有构造和析构?有人会忘记掉啊,而且每次都要手动调超麻烦的耶!
为什么会有隐藏的 this 指针?因为传起来烦啊!
为什么要把第一个参数取消掉?没必要让人来写了啊!
……
0x03 C语言中容易忽略的缺陷 —— 深浅拷贝问题
💬 C语言这一块也是由深浅拷贝的问题的:
Stack copy = st; // 你定义一块结构,这里拷贝
C语言的结构体默认会完成值拷贝和浅拷贝,和C++是一样的。
void TestStack_in_C() { Stack st; StackPush(&st, 10); StackPush(&st, 20); StackPush(&st, 30); StackPush(&st, 40); Stack copy = st; StackDestroy(&st); StackDestroy(©) }
它们是浅拷贝,拷贝之后它们指向的是同一块空间,
所以这里 Destroy 会面临释放两次的问题。
所以,又有了拷贝构造和拷贝赋值。这里就是对深浅拷贝问题的完善。(于C++98定档)
Ⅲ. 再次理解
0x00 再次理解封装
C++ 是基于面向对象的程序,面向对象有三大特性 —— 封装、继承、多态。
C++ 通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西都打包在一起。通过访问限定符选择性地将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,也没必要知道,就算知道了有些情况下也没用,反而增加了使用或维护的难度,导致整个事情变得复杂化。
封装是为了更好的管理,我想让你访问的我定义成公有,不想给你访问的我定义成私有或者保护。
0x01 封装的意义和本质(重温)
封装是一种更好的严格管理,不封装是一种自由管理。
❓ 那么是严格管理好,还是自由管理好呢?
举一个最简单的例子:
比如最近的疫情,漂亮国单日新增一百万,你说是自由的管理好呢?还是严格的管理好呢?
我们和漂亮国其实都是在控制疫情的,但是我们是动态清零的政策,是非常严格的管理。
而漂亮国是自由管理,虽然人人高呼 "Freedom" ,但是疫情一直都难以得到控制。
所以,封装的本质是一种管理。
再举个例子……
我们是如何管理疫情的呢?
比如进商场,如果疫情期间没有人管理,
让大家都随意进,那疫情就不好控制了。
所以我们要对商场进行很好的防疫管理措施!
那么我们首先要把商场 "封装" 起来,你想进入商场就必须要扫健康码。
并不是说不让你进商场,而是你必须要走正门扫码才可以进入。
通过扫码,是绿码你才能进商场,在疫情防疫合理的监管机制下进商场。
类也是一样,我们使用类数据和方法都封装到了一起,不想让人随意来访的,
就是用 protected / private 把成员封装起来,开放一些共有的成员函数对成员合理的访问。
所以,封装是一种更好、更严格的管理!
0x02 再次理解面向对象
实际上,我们写的东西就是对现实进行映射,我们定义类就是对一类事情的描述。
类就是对现实世界中一类事物的抽象类别的描述,类可以实例化出很多对象,