面向对象基本原理
今天终于开始学习面向对象了,b 站大学真是太牛逼啦!
浙大翁恺老师的课讲得很清楚,疯狂安利!
面向对象基本原理
面向对象程序设计,object-oriented-programming (OOP),在这种方式的程序设计中,把每一个变量看作是一个对象,通过调用对象提供的功能函数达到目的。
面向对象与面向过程编程的区别
所谓面向过程,即程序按照主函数从上到下的顺序依次调用各种函数和变量,通过数据与指令流的方式控制整个程序,最终达到目的。
而面向对象不一样,面向对象的思想并不关注程序当前执行到哪一步,它关心的是程序中存在什么东西,以及这些“东西”之间如何联系。通过把这些东西有序的组合起来,再依靠这些“东西”之间传递的消息来完成调用,达到目的。
简单点来说,面向过程就是程序员一步一步告诉程序该怎么做;面向对象是先告诉程序内各个模块:当收到指令 A 的时候怎么做,收到指令 B 的时候又该怎么做,最后只用告诉程序指令 A 或者指令 B ,让程序内的不同模块开始传递信息,按我们预先设定好的方式解决问题,我们只需要等结果就行了。
对象的定义
说了一大顿,那么何为“对象”?实际上,任何可以被称作“东西”的东西,都可以称为“对象”。
比如说一个苹果,一个水杯,坐标系内一个点,等等等等。
这些东西又包含它自己的一些特性,比如苹果有颜色、大小,一个点有它自己的坐标,水杯可以装水,也有它自己的颜色……一切东西都有它自己的特性。
我们把这些物体的特性称为对象的“属性”,对于计算机代码而言,属性也就是一个对象中包含的数据啦。
除了这些特殊的属性,面向对象还要求对象具有一定的功能。
比如水杯,它可以往里装水,也可以往外倒水;平面内一个点,它可以拥有把它的基本信息(也就是坐标)告诉我们的功能。
我们把这些物体所包含的功能称为对象提供的“服务”,也就是“这个对象支持我们对它做什么”的意思。对于计算机代码而言,服务通常以函数的形式出现。
总的来说,对象就是上面说的 属性 + 服务。
类的概念
有了对象(也就是物品)自然也就有了类的区分。
现实生活中,博物学给各种声明都区分了界门纲目科属种的归类,这就是一种类的区分。比如有一只花猫和一只黑猫,我们可以把他们简单的归为一类,即“猫”;又有两个一高一矮的水杯,我们也可以把他们归为一类,即“杯子”。
仔细思考一下我们为什么这么做,究其原因就是他们拥有相同的特征和功能:猫都有胡须毛发(属性),主人发出“叫”的指令,它们可以喵喵叫(功能);水杯都可以储水(特征),也可以接水和倒水(功能)。
简单粗暴一点,对于程序设计中的对象,只要拥有相同的属性和服务,我们就认为它们是同一类。
同一类中的对象,因为他们的服务都是相同的,所以它们也能接受相同的指令。特别的,反过来说,如果两个对象可以接受的所有指令都完全相同,那么我们也可以把它们认为是同一类。
类与对象
在自然界中,人们常常是发现一个新事物之后,再把它归类。但在计科中,是我们先创造一个类,规定他拥有什么样的属性和服务功能,再去创造新事物(即对象)。
在 C++ 中,声明类用关键字 Class
表示。
class Cat
{
public:
Cat(const string a, const string b, const bool c)
{
name = a, color = b, cute = c;
}
void GetInfomation() const
{
cout << name << '\n';
cout << color << '\n';
if(cute)
cout << "cute!" << '\n';
else
cout << "not so cute..." << '\n';
}
void Meow() const
{
cout << "Meow" << '\n';
}
private:
string name, color;
bool cute;
}garfield;
这段代码中,我创建了一个名字是 Cat 的类(电子猫咪),并赋予它三个特征:name, color 和 cute,以及通过函数实现输出基本信息、喵喵叫的功能。
然后我在这个类的框架下声明了一个变量 garfield, 这就是归属 Cat 类的一个对象。它具有 Cat 类规定的所有属性和服务,即上面说的 name, color 和 cute,以及输出它自己的基本信息、喵喵叫的功能。
至于代码里面的 private
和 public
关键字,我们下一节再细说。
面向对象的要求
这里有一个像煎鸡蛋的图,面向对象的核心思想就是这个煎蛋。
煎蛋的蛋黄是数据 (Data),煎蛋的蛋清是操作 (Operations),OOP 要求我们在程序设计的时候不可以直接对里面的蛋黄进行操作,只能通过外面蛋清提供的服务来间接的控制蛋黄里的数据,也即“对象中的元素受到保护”。
对象通过它提供的服务对内数据提供指令交流和保护,对外部的操作进行反馈。这些服务就是所谓的“接口”,即外部程序在访问对象的时候只能通过这些“接口”向对象发布指令和接受对象反馈的信息,但是不可以直接访问对象内的数据。
拿这两段代码举例子,它们都得到了一个二维坐标点的详细坐标信息,但实现方式不同:
struct Point2d{
Point2d(const int a, const int b)
{
x = a, y = b;
}
int x, y;
};
void PrintPoint2d(const Point2d * const p)
{
cout << p -> x << ' ' << p -> y;
}
int main()
{
Point2d A(2, 3);
PrintPoint2d(&A);
}
这段代码是典型的面向过程思想,通过外部函数来直接查询对象内的数据。
class Point2d
{
private:
int x, y;
public:
Point2d(const int a, const int b)
{
x = a, y = b;
}
void PrintPoint2d() const
{
cout << x << ' ' << y;
}
};
int main()
{
Point2d A(2, 3);
A.PrintPoint2d();
}
这段代码是面向对象的思想,通过对象自己内部的功能(接口)获得数据。
而 private
就是对象私有的属性,它表示这些数据是对象私有的,不能从外部直接进行访问;public
就是外面的公用接口,我们只可以调用这些接口中的数据和函数。
为什么要这样做?
面向对象设计的核心目的是让代码的结构变得松散而灵活。
用灯泡和灯管举例子。
-
假设灯泡和灯座是两个不同的对象,他们通过接口(也就是现实中的螺口)相连接,如果我想换一个灯泡,那么只需要把灯泡拧下来换上新的就可以了。面向对象思想就是通过接口的方式实现代码的可更新性与可扩展性,因为要更新灯泡部分代码的话,我们就不用管灯座部分的代码。
-
现在再假设灯泡和灯管是一体的,他们直接出厂的时候就焊在一起了,那么想换一个灯泡就很困难,因为他们之间的联系太过紧密,牵一发而动全身。这导致代码的可扩展性变得差一些。
现实中程序员一般可以分为两类
- class creater,负责写类的程序员,他们负责写类,实现某些功能。
- client programmer,负责利用类给出的功能实现其他功能的程序员。
当写类的程序员想要维护更新他的类的时候,只需要直接改他自己的代码就行了——只要保证原接口的调用方式不变,那么 client programmer 仍可以正常调用,因此他就不用修改他的代码。
这样是很方便的。