一、面向过程和面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题;而C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。我们以洗衣服为例。
面向过程 – 逐步求解问题:
面向对象 – 通过对象之间的交互解决问题:
又比如我们的外卖系统:面向过程关注的是顾客应该如何下单、商家应该如何做出菜品、骑手应该如何将外卖送达;而面向对象关注的是顾客、商家、骑手这四个对象之间的交互,比如顾客下单后商家出餐,然后骑手送餐,而不必关心顾客如何下单、商家如何出餐、骑手如何送达这类面向过程的问题。
二、类的引入
在C语言中我们学习了结构体,知道了结构体可以定义某一种类型,但是不能定义具体的对象;以下面的结构体为例:
struct Student { char name[20]; char id[11]; int weight; int height; }; int main() { struct Student stu1 = { "zhangsan", "2202101001", 60, 180 }; }
struct Student 只是定义了学生这种类型,而我们需要用这种类型来创建出具体的学生,比如张三李四;在C++中,学生类型被简称为 “类”,而具体的学生则被称为 “对象”;
但是我们知道,一个对象除了具有自身的属性 (数据) 之外,还应该拥有相应的方法 (行为),比如学生除了姓名、学号、体重、身高这些属性之外,还应该具有吃饭、睡觉、学习、娱乐等行为;
但是C语言结构体中只能定义变量,不能定义函数 (方法),所以C++对C语言的结构体进行了升级 – 在C++中,结构体内不仅可以定义变量,也可以定义函数。比如,之前在数据结构初阶中,我们用C语言方式实现的栈,结构体中只能定义 top、capacity、a 这些变量,而入栈、出栈、初始化这些函数只能在结构体外部定义;而使用C++我们就可以直接将这些函数定义在结构体内部:
//成员函数与成员变量都定义在结构体中 struct Stack { //成员函数 //初始化 void Init(int N = 4) //缺省参数 -- 初始化空间大小 { _data = (int*)malloc(sizeof(int) * N); if (_data == nullptr) { perror("malloc fail\n"); exit(-1); } _top = 0; _capacity = N; } //入栈 void Push(int x) { if (_top == _capacity) { int* tmp = (int*)realloc(_data, sizeof(int) * _capacity * 2); if (tmp == nullptr) { perror("realloc fail\n"); exit(-1); } _data = tmp; _capacity *= 2; } _data[_top++] = x; } //取栈顶的数据 int Top() { return _data[_top - 1]; } //销毁栈 void Destroy() { free(_data); _data = NULL; } //成员变量(属性) int* _data; int _top; int _capacity; };
同时,C++结构体直接使用 structName 代表类,而不用加 struct 关键字,但是C++兼容C语言结构体的全部用法,使用我们使用 struct + structName 的方式定义变量也是没问题的:
typedef struct SListNode { SListNode* next; //SListNode 可以直接代表这个类,所以此处可以不用加 struct int data; }SL; int main() { //C语言用法 struct SListNode* sl1; SL* sl2; //C++用法 SListNode* sl3; }
最后,在C++中更喜欢用 class 来代替 struct,并且把变量称为属性/成员变量,把函数称为成员函数/成员方法。
三、类的定义
class className { //... };
class为定义类的关键字,ClassName 为类的名字,{} 中为类的主体,注意类定义结束时后面分号不能省略。类体中的内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数;
类的定义方式
C++类一共有两种定义方式:
1、声明和定义全部放在类体中 (注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理) :
2、类声明放在.h文件中,成员函数定义放在.cpp文件中 (注意:成员函数名前需要使用类名+域限定符):
类定义的两个惯例1、类的成员变量使用修饰符修饰 – 与C语言结构体不同,由于类中可以同时定义变量和函数,所以函数的形参与类成员变量就可能会发生冲突,这种情况在 Init 函数中十分常见,如下:
class Date { public: void Init(int year, int month, int day) { year = year; month = month; day = day; } private: int year; int month; int day; };
Init 函数的形参和类成员变量相同,这就导致我们初始化赋值的不确定性,当然我们也可以使用类名+域作用限定符或者this指针来解决这个问题:
void Init(int year, int month, int day) { Date::year = year; Date::month = month; Date::day = day; } void Init(int year, int month, int day) { this->year = year; this->month = month; this->day = day; }
但是这样显然比较麻烦,所以在C++中有一个惯例 – 成员变量使用某种修饰符来修饰,其中常见的有四种:_menber、menber_、m_menber、mMenber,前面两种是在成员变量前/后加一个下划线_,第三种m_表示此变量是成员变量,最后一种m表示成员变量,然后不使用_,使用小驼峰;我习惯于第一种方式,所以可以看到我前面类中的成员变量都会有一个前_。
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
2、成员函数定义在成员变量前面 – C语言编译器寻找变量的规则是先到前面去找,然后再到全局去找,所以在C语言中变量必须定义在函数前面,才可以在函数中使用该变量;但是C++编译器不一样,C++编译器会把类看作一个整体,当我们使用一个变量时,它会到整个类中去寻找,然后再到全局去寻找;所以在C++中,我们是可以将成员变量定义成员函数后面的;
上面解释了成员函数定义在成员变量之前的可行性,下面我借用 《高质量C/C++编程》中的解释来阐述为什么要将成员函数定义在成员变量前面:
四、类的访问限定符及封装
访问限定符
C++为了实现封装,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用:
访问限定符说明:
public 修饰的成员在类外可以直接被访问;protected 和 private 修饰的成员在类外不能直接被访问 (此处 protected 和 private 是类似的);
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;
如果后面没有访问限定符,作用域就到 } 即类结束;
class 的默认访问权限为 private,struct 为 public (因为struct要兼容C);
访问限定符的存在使得用户不能直接修改类中的成员变量,而是只能使用我们提供的特定接口,让类中的数据更加安全,也让用户使用类的方式更加规范。
注意:访问修饰限定符限定的只是类外的访问权限,类内可以随意访问;并且访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
面试题
问题:C++中 struct 和 class 的区别是什么?
回答:C++需要兼容C语言,所以C++中 struct 可以当成结构体使用;另外C++中 struct 还可以用来定义类,和class定义类是一样的,区别是 struct 定义的类默认访问权限是 public,class 定义的类默认访问权限是 private。(注意:在继承和模板参数列表位置上 struct 和 class 也有区别,只是我们暂时还没学习)
封装
面向对象有很多特性,其中最出名的是:封装、继承和多态;在类和对象阶段,我们主要研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在数据结构初阶时,我们曾用C语言来实现栈,其中关于返回栈顶元素的函数接口 – Top就很好的体现了封装的作用:
由于C语言没有访问限定符,也没有封装的概念,所以对于取得栈顶元素就有了两种方法 :一是通过Top函数接口,二是直接访问data数组;
但是这里就出现了一个问题 – 结构体成员top是指向栈顶,还是指向栈顶的下一个位置是不确定的,其取决于Init函数;当 top 被初始化为-1时,top指向栈顶元素;而当其被初始化为0时,则指向栈顶的下一个元素;
所以可能就会出现这样一种情况:用户没有使用Top函数提供的接口,而是直接访问data数组,导致取出的栈顶元素是一个随机值;这种情况在现实中是经常出现的,甚至在有的教材中都是如此;
但是C++就不会出现这种情况,因为C++类成员变量通常都会用 private 修饰,用户不能直接访问类中的数据,只能通过特定的接口 (用 public 修饰的函数) 来操作对象。
在我们现实生活中对于各种文化旅游景点的设置也体现了封装 – 文物用展柜封装起来,使得游客 (类外) 不能直接接触文物,只能是文物相关各种人员 (类内) 才能直接接触文物,从而即达到了观赏的目的,也避免了文物被破坏。