面向对象(一)

相信各位学习各种语言的时间都不短了,那么为什么感觉自己总是只能做点小脚本或者小工具呢?

那是因为你没有找到对象啊!(笑)

image-20221230202442459

面向对象是现代软件工程的一个基本工具,几乎所有的现代高级语言都支持面向对象编程。本篇文章分上下篇,以C++为例介绍面向对象这一计算机世界中最基本的元素。

一、什么是面向对象

我们先来看看权威的定义:

面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的编程典范,同时也是一种程序开发的抽象方针。它可能包含数据特性代码方法。对象则指的是(class)的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象[1][2]

——https://zh.wikipedia.org/

概念总是很玄乎的,需要注意的是这段概念中的几个名词:

对象程序与数据

与面向对象(Procedure Oriented 简称PO)相对的就是面向过程(Procedure Oriented 简称PO)

1.一个小例子

下来我将用通俗易懂的语言描述一下面向对象(还是推荐有一定编程基础的童鞋们看哦):

让我们假设一个场景,我们需要烧一顿饭;

如果是面向过程:

1.执行淘米方法

2.执行煮饭方法

3.执行煮肉方法

4.执行煮菜汤方法

可见,面向过程的思想就是把一整个解决方案拆解成一个个分动作,再通过一定的顺序执行,最终得到结果

如果是面向对象:

1.人.淘米

2.电饭锅.煮饭

3.锅.煮肉

4.锅.煮菜汤

可见,在解决同一个问题时,面向对象思想就是先把执行的对象抽象出来,然后通过执行对象的动作来完成整个解决方案。

明显地看出,面向对象是以功能来划分问题,面向对象以步骤来划分问题。

2.有何区别

那么面向对象和面向过程有何优劣呢?

还是以上面的例子为例,明显第一种面向过程的方法思路更加清晰,但是如果下次烧另一顿饭呢?或者下次想先烧汤呢?或者我们下次用电饭锅煮汤呢?

我们可能需要修改整段代码来达到我们的目的。

如果是面向对象则要方便地多,比如我们下次用电饭锅煮汤,我们只需要重新调用封装在电饭锅类里的煮汤就行了。

回归到编程本身,我们知道结构化编程中程序=算法+数据结构,在实际编程过程中,面向过程只是一个“方法”或是“算法”,投入数据后根据特定过程得出结果。而面向对象不仅包含“过程”也包含“数据”,且以功能来划分问题,这意味着下次若是换一种问题,我们需要的只是把功能重新组合,而不是想面向过程一样重新设计。

那么面向过程就一无是处吗?

也不尽然,面向对象面临着需要先定义对象,在把对象实例化的过程,这意味着对于性能的开销将会提升。而对于面向过程来说,性能开销则会小的多。比如单片机、嵌入式开发、 Linux/Unix等性能是最重要的因素,一般采用面向过程开发。

总结一下:

面向对象

优点:易于维护,易于扩展,易于复用,同时由于面向对象的三大特性(封装、继承、多态性),使得通过面向对象开发的程序在维护和灵活性方面具有巨大的优势

缺点:性能开销大

面向过程

优点:性能开销较小

缺点:维护性、扩展性、复用性相对较差

大家在不同的场景下可以选择不同的开发思维。

最后配上一张梗图(笑)

img

二、面向对象的特性

三大基本特性:封装,继承,多态性

下面我将一一介绍

1.封装

封装从字面意思来说很好理解,我们平时把一堆东西打包时,也可以用“封装”这个词。

面向对象编程方法中,封装(英语:Encapsulation)是指,一种将抽象性函数接口的实现细节部分包装、隐藏起来的方法。同时,它也是一种防止外界调用端,去访问对象内部实现细节的手段,这个手段是由编程语言本身来提供的。封装被视为是面向对象的四项原则之一。

适当的封装,可以将对象使用接口的程序实现部分隐藏起来,不让用户看到,同时确保用户无法任意更改对象内部的重要资料,若想接触资料只能通过**公开接入方法(Publicly accessible methods)**的方式( 如:“getters” 和"setters")。它可以让代码更容易理解与维护,也加强了代码的安全性。

——https://zh.wikipedia.org/

简而言之就是,为了放置破坏对象内部的实现的方法,外部只能调取“公开接入方法”来执行对象。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。例如我们上面的例子中的:电饭锅.煮饭,为了防止我们破坏电饭锅的一些内部方法,我们只能通过煮饭这个方法执行这个对象,而他内部的(例如:煮饭的温度、时长?)我们无法接触到。

可见这种特性使得代码的安全性大大提高,同时在执行对象时也不需要考虑对象内部,使得代码可读性也提高。

2.继承

继承从字面意思也很好理解,我们的某些对象可以从一些对象中继承他的方法或者数据,使得编程的重复性降低。

继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类的行为在编译期就已经决定,无法在执行期扩展。

——https://zh.wikipedia.org/

某些对象的方法可能与其他对象相似,例如我定义了一个这个类,那么如果我要重新新建一个中国人这个类,很明显中国人可以像一样拥有走路思考等方法,那么中国人这个类就可以从这个类继承,这样就避免我们重新写一些重复的方法。

那么这个类就叫做中国人的“基类”、“父类”或“超类”;中国人就是的“子类”或“派生类”。可见

同时我们也可以在中国人这个类里加上一些独特的数据或方法,例如中华文化等。可见,继承的过程,就是从一般到特殊的过程。

当然继承还有很多细节性问题,例如方法的重写、实现继承与接口继承,这些会在以后仔细讲述。

3.多态

有趣的是,“多态”是来自一个生物学概念:多态(Polymorphism)这个概念最早来自于生物学,表示的是同一物种在同一种群中存在两种或多种明显不同的表型。

编程语言类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口[1],或使用一个单一的符号来表示多个不同的类型[2]

——https://zh.wikipedia.org/

这一特性比较复杂,无法形象化描述,““多态”则是多样性、可扩展性的体现。面对丰富的和可能不断变化的问题域,让我们的程序能够有更大的容纳性去模拟和适应这些变化。它表达的是**’变化‘** ”。

三、C++类 & 对象

1.类定义

定义一个类,本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。

img

以下这个例子定义了一个Box类。有长宽高三个属性,成员函数get()用于得到对象的体积,成员函数set()用于定义对象的三个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
// 成员函数声明
double get(void);
void set( double len, double bre, double hei );
};
// 成员函数定义
double Box::get(void)
{
return length * breadth * height;
}

void Box::set( double len, double bre, double hei)
{
length = len;
breadth = bre;
height = hei;
}

几点注意:

  • 私有的成员(private)和受保护的成员(protect)不能使用直接成员访问运算符 (.) 来直接访问
  • 类的对象的公共数据成员可以使用直接成员访问运算符 . 来访问,例如Box a; printf("%d",a.length);

2.成员函数

成员函数可以简单理解为这个类的一个“方法”

成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。

例如下面的getVolume()就有如下两种定义方式:

1
2
3
4
5
6
7
8
9
10
11
12
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度

double getVolume(void)
{
return length * breadth * height;
}
};

以及:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度

// 成员函数声明
double getVolume(void);
};

// 成员函数定义
double Box::getVolume(void)
{
return length * breadth * height;
}

3.类访问修饰符

数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。

public修饰的方法和数据可以从外部访问,而private、protected不可以,至于private、protected有何区别,在下一章的“继承”中会说到。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>

using namespace std;

class Box
{
public: //public是公开的方法,即可以外部可以访问的
double length; // 长度
double breadth; // 宽度
double height; // 高度
// 成员函数声明

double get(void);
void set( double len, double bre, double hei );
protected:
double calc_length(){ // 计算棱长总和
return length * 2 + breadth * 2 + height * 2;
}
private:
double calc_area(){ // 计算表面积
return length * breath * 2 + breadth * height * 2 + length * height * 2;
}
};
// 成员函数定义
double Box::get(void) //获取长方体的体积
{
return length * breadth * height;
}

void Box::set( double len, double bre, double hei) //设置长方体的长宽高
{
length = len;
breadth = bre;
height = hei;
}

int main(){
//实例化三个对象(可理解为创建了三个“长方体”)
Box Box1;
Box Box2;
Box Box3;
double volume = 0.0; // 用于存储体积

// box 1 详述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.breadth = 7.0;

// box 2 详述
Box2.height = 10.0;
Box2.length = 12.0;
Box2.breadth = 13.0;

// box 1 的体积
volume = Box1.height * Box1.length * Box1.breadth;
cout << "Box1 的体积:" << volume <<endl;

// box 2 的体积
volume = Box2.height * Box2.length * Box2.breadth;
cout << "Box2 的体积:" << volume <<endl;


// box 3 详述
Box3.set(16.0, 8.0, 12.0);
volume = Box3.get();
cout << "Box3 的体积:" << volume <<endl;
// Box1.calc_area()无法执行,因为他的修饰符是private
// Box2.calc_length()无法执行,因为他的修饰符是protected
return 0;
}

输出:

1
2
3
Box1 的体积:210
Box2 的体积:1560
Box3 的体积:1536