【C++要笑着学】面向对象总结 | 瞎编的C++小故事 | 再次理解封装 | 再次理解面向对象

简介: 我是柠檬叶子C。本篇将对之前讲的面向对象的内容进行一个梳理,通过举一些例子去感受C和C++之间的区别和联系。举了一个比较有意思的胡编乱造的故事(bushi)。文章的最后会再次理解一些概念,强调封装的意义,加深对 "面向对象" 的理解。如果觉得文章不错,可以 "一键三连" 支持一下博主!你们的关注就是我更新的最大动力!

前言



我是柠檬叶子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 {
};

大佬又想了想,嗯!不能让你们随便访问数据了!不再纵容数据和方法分离了!

94b1644e39f142ce5dca317337d9ce09_325af2fde3654a2585da74ec20c4e771.png

把数据和函数都往这里面放。我也不想让人能随随便便调这些数据,然后跑过来问我怎么回事,


我直接不让你们访问了,看你们还怎么乱玩:


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);

大佬搞了一个通宵,把构造函数和析构函数的细节敲定了。


于是用的时候就会自动调用构造函数和析构函数了。

34a15c1c8efbeefb310fe79ca89ac695_a292bac870a44ff0b1c13a6982034f4c.png

大佬过几天用的时候又想起了每次调用一些接口都要传一下地址的事,


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(&copy)
}

5f79abf0d2203165f99c4e3ed321ffa8_114fa468d2f841fbaba30ce266313d30.png


它们是浅拷贝,拷贝之后它们指向的是同一块空间,


所以这里 Destroy 会面临释放两次的问题。


所以,又有了拷贝构造和拷贝赋值。这里就是对深浅拷贝问题的完善。(于C++98定档)


Ⅲ. 再次理解


0x00 再次理解封装

C++ 是基于面向对象的程序,面向对象有三大特性 —— 封装、继承、多态。


C++ 通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西都打包在一起。通过访问限定符选择性地将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,也没必要知道,就算知道了有些情况下也没用,反而增加了使用或维护的难度,导致整个事情变得复杂化。


封装是为了更好的管理,我想让你访问的我定义成公有,不想给你访问的我定义成私有或者保护。


0x01 封装的意义和本质(重温)

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


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


举一个最简单的例子:


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


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


而漂亮国是自由管理,虽然人人高呼 "Freedom" ,但是疫情一直都难以得到控制。


所以,封装的本质是一种管理。

6a222620445106a32f42d60ecd33d02f_5714edd03d0f4f70aaff4cbc46c43ea7.png

再举个例子……


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


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


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


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


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


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


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


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


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


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


0x02 再次理解面向对象

实际上,我们写的东西就是对现实进行映射,我们定义类就是对一类事情的描述。


类就是对现实世界中一类事物的抽象类别的描述,类可以实例化出很多对象,

1fb2edd0f9065a97909d08e1f8a5e1e3_9455bb4f721440f6bdf42bbf53eb627b.png


相关文章
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
51 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
53 5
|
1月前
|
存储 C++
【C++面向对象——输入输出流】处理二进制文件(头歌实践教学平台习题)【合集】
本任务要求使用C++读取二进制文件并在每行前添加行号后输出到控制台。主要内容包括: 1. **任务描述**:用二进制方式打开指定文件,为每一行添加行号并输出。 2. **相关知识**: - 流类库中常用的类及其成员函数(如`iostream`、`fstream`等)。 - 标准输入输出及格式控制(如`cin`、`cout`和`iomanip`中的格式化函数)。 - 文件的应用方法(文本文件和二进制文件的读写操作)。 3. **编程要求**:编写程序,通过命令行参数传递文件名,使用`getline`读取数据并用`cout`输出带行号的内容。 4. **实验步骤**:参考实验指
39 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
41 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4
|
1月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
34 3
|
4月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
112 11
|
5月前
|
存储 安全 编译器
【C++核心】一文理解C++面向对象(超级详细!)
这篇文章详细讲解了C++面向对象的核心概念,包括类和对象、封装、继承、多态等。
44 2
|
4月前
|
存储 编译器 C语言
【C++】初识面向对象:类与对象详解
【C++】初识面向对象:类与对象详解