一、前言
从本文开始,我们就要正式来学习C++中的类和对象了,本文我将带你一步步从C语言的结构体
struct
到C++的类class
,真正搞懂有关C++的==面向对象的三大特征之一 —— 封装==
- 作为读者,可能你正在学习C语言,亦或者已经开始学习C++了,也有可能你是一位C++的资深开发者或者其他领域的从业人员。不过这没关系,都不会影响你阅读本文:book:
- 可能你了解过面向对象的一些语言,像Java、C#、python这些,也知道C++里面也有面向对象的一些思想,但是呢为何又可以写一些C语言的代码,C语言大家一定都学过,是一门面向过程的语言,可是为何C++也可以跑C语言的代码呢?
现在,我提出以下这几个问题,看看你是否都了解👇
- C++是一门面向对象的语言吗?它和面向过程有什么联系?
- 面向对象的三大特征为:封装、继承、多态,你真的有去好好了解过什么是类的封装吗?它的好处在哪里?
- 类和结构体之间似乎很像,它们存在什么关联吗?
- this指针了解多少?存放在哪里?是用来干嘛的?
接下去,就让我们带着疑惑,再度出发,好好地探一探这些知识,可能内容会比较多,但我会尽量用生动的语言和图文并茂的方式,结合一些生活中的实际场景,让你更好地理解每个知识点🤔
二、面向过程与面向对象
👉对于C语言而言,它完全是一门【面向过程】的语言。关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题
👉对于C++是基于【面向对象】的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
- 可是呢,C++为了兼容C,并没有完全面向对象,所以你才可以在一些C++的编译器上去写一些C语言的代码
可是面向过程和面向对象它们之间的区别到底是怎样的呢?可以通过一个在我们生活中最普遍的例子来说明一下
- 若是现在公司要你写一个==外卖订餐系统==,你呢只会C语言和C++,此时若你使用C语言去写的话关注的就是从用户下单到商家接单,最后到骑手送单这整个过程该如何去实现;
- 但如果你使用的是C++这样具有面向对象的语言去编写的话,那此时你要关注的就是各个对象之间会发生什么关系,对象有【用户】、【商家】、【骑手】这三个,那此时你要考虑的就是用户下单到商家接单,最后到骑手送单,它们之间是谁和谁之间发生关系
三、结构体与类
1、C++中结构体的变化
- 之前在C语言中,我们去定义一个结构体都是这么去写的,例如说这里有一个链表结点的结构体,一个是数据域,一个是指针域
struct ListNode { int val; struct ListNode* next; };
- 在C++中,我们也可以这么去写,上面说到过C++兼容C,可是呢有一处却可以发生变化。也就是在定义这个指针域的时候,可以不需要再写
struct
了
struct ListNode { int val; ListNode* next; };
- 通过下面两幅图的对比就可以很清楚地看在C++中确实在使用结构体的时候不需要再去写一遍
struct
这个关键字了,直接使用定义出来的结构体即可;但是在C语言中没有这样的规定,所以是一定要写的
- 不过C++相较于C语言可不只是多了这一点,C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数【但是这在C语言中,是不被允许的】
知道了上面这些,其实就可以回忆我们之前在数据结构中写过的很多代码,在结构体中只是定义了一些成员变量,具体的函数都是写在结构体外,那现在知道了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; }
来看一下运行结果:computer:
通过上面所写,使用C++去代替实现之前使用C语言写的【栈】时,发现完全没问题,这下你应该可以进一步了解为何C++兼容C了,不过呢在C++中,这样变量和函数存放在一起的结构我们不叫做结构体,而叫做【类】,可是对于类来说,在C++中也不常使用struct
这个关键字来定义,而是使用[class]
3、结构体 --> 类
语法格式:
class className { // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号
【注】:class
为定义类的关键字,ClassName
为类的名字,{}
中为类的主体,注意类定义结束时后面分号不能省略
- 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数
类的两种定义方式
知道了一个类长什么样,接下去我们来聊聊一个类该如何去进行规范的定义
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
- 这也就是我们上面讲到过有关【栈】的这种定义,只需要将
struct
换成class
即可,这种类的定义方式简单粗暴,也是我们平常用得最多的,自己练习代码可以直接这样使用,但其实在日常项目的开发中,不建议大家这样使用❌
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名
::
- 重点来讲一讲这一种,这也叫做多文件形式的编写,之间在C语言的学习中我们写的【扫雷】和【三子棋】也是使用的这种分文件编写,如果不了解的读者一定要学会这种思想,在日常企业的开发中是经常使用到的
==stack.h==
#pragma once #include <iostream> #include <stdlib.h> 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++中,对于一个类体而言,其实就属于一个作用域,将成员变量和成员函数包含在里面。那么此时要在另一个
cpp
的文件中访问这个类中定义的成员变量的话也就是访问Stack作用域中的内容,就要加上【域作用限定符::
】,就像下面这样
成员变量命名规则
最后再来普及一点,你可以自己观察我上面在写【栈】的时候对成员变量的命名形式,前面都加上了
_
,可能你看起来会很别扭,但这却是比较规范的一种定义形式
- 其实你可以去看一看库里面一些变量的命名方式,很多都是采用这种下划线的方式进行,原因其实就在于避免造成【成员变量】和【形参】的命名冲突从而引发歧义
- 可以看到,我在下面写了一个日期类,通过
Init()
这个函数对类中的成员变量去进行一个初始化,观察【成员变量】和【形参】可以发现我故意将它们写成了一样的,此时调用函数进行初始化操作的时候会发生什么呢?
class Date { public: void Init(int year, int month, int day) { year = year; month = month; day = day; } int year; int month; int day; };
通过观察可以发现,若是【成员变量】和【形参】的名字一样的话,其实这个时候就会造成歧义,初始化的就不是当前这个对象的成员变量了,如果你自己观察就可以发现,命名一样的话,在VS中二者的字体都会变淡,这其实就是VS在提示你这样的做法其实是无效的❌
那要如何命名才是最规范的呢?
- 这个其实我说了不算,要看你实际的开发的地方是如何规定的,如果是你自己的做开发的话,那建议就是【成员变量】改成
_变量名
或者是m_变量名
,但如果你在公司里面的话,内部是如何规定的你怎么做就行了,这个没有强制,只要别造成相同即可 - 但是你一定在某些地方见过
this->year = year
这种写法,确实这也可以,这里面就用到了C++类和对象中很重要的一块叫做【this指针】,这里先不做详解,见最后一个模块哦😗
this->year = year; this->month = month; this->day = day;