从结构体走向对象

从结构体走向对象

本文能够让你大致理解面向对象的一些概念,但是以下一切内容均不适用于参加OO课程的考试,如果爆零,后果自负.

部分语法的介绍

  1. C++语言中,定义结构体变量时,类型中的struct可以省略,"struct结构体名 变量名"可以简写为"结构体名 变量名".以下均为简写形式.
  2. Java语言中,所有内置的大写开头的类型都是类,所有对象都是"引用变量",可以理解为不能做运算的指针.
    (指针运算指,数组首项地址+1等于第二项地址的这种运算.)
    :
    1
    2
    3
    4
    5
    6
    // int不是类, 是基本类型
    // a是普通变量:
    int a = 0;
    // Integer是类, b,c是引用变量:
    Integer b = 0;
    Integer c = 0;
    上面的代码可以简单地理解为,a是一个int类型变量,内容是0.
    b是一个指向Integer变量的指针,他指向的值是0.
    c同理.
    因此,b==c判断的是bc是否指向同一个对象.判断bc相等应该用b.equals(c).
  3. 这篇推送只讲概念,不需要完全理解代码是什么意思,具体语法部分都做了注释.之后大概还会有推送来讲语法.如果那里有代码不明白什么意思欢迎联系我或者后台给我留言.

啥是类?最简单的来说,就是C语言中的结构体.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 定义一个类, 叫做Student.
*/
public class Student {
/**
* Student中的一个字符串类型的变量
*/
String name;

/**
* Student中一个int类型的变量
*/
int ID;
}

在最开始非面向对象的编程中,我们经常会用多个变量来描述同一对象的多个属性:比如定义一个名字变量,一个学号变量.后来,随着计算机科学的发展,编程语言中支持了 结构体 这样一个特性,能够让我们把 描述同一个东西不同属性 的多个变量统一管理.结构体这一个概念,到了面向对象的语言中,结构体发展为了类的概念.

可以把类理解为C语言中的结构体+方法.

方法

在很多情况下,我们之前编写的一些函数是针对某一个结构体提供的.比如,链表的删除某个节点的函数就可以认为是针对某个节点定义的,效果是删除这个节点后面的一个节点.

我们可以把这样的函数定义为一个全局的函数,把一个结构体变量作为参数传入这个函数:

1
2
3
node* remove(node *now){
...
}

但是,如果这样自定义我们就会在别的任何地方都没法再次定义一个 remove 函数了.这个只针对 node* 的函数占据了一个全局的名字(或者说,符号).无疑,这样的实现非常 不优雅.因此,我们更应该把进和 node* 这一个变量相关的函数定义成一个 node*成员函数,也叫 方法.

1
2
3
4
5
6
7
8
9
10
11
struct node {
int data;
node *;

node* remove(){
// 使用this指针访问当前的变量
// 这里仅仅演示如何使用
// 本程序并无法真的工作.
return this->next;
}
};

如果这样定义remove函数,就不会污染全局的作用域,同时也不用我们显示的声明参数,只需要如下调用:

1
2
node* a;
a->remove();

即可自动把 aa的地址 作为 this指针 参数传入给 remove 函数.

因此,方法就是针对某一个类的函数,为了方便我们把它放到类里面.构造一个对象的函数是一个特殊的成员函数,叫做类的构造函数.

继承

其实, 编程语言演进的过程就是一代代程序程序员偷懒的过程 .自从有了结构体的方法这东西之后,人们就在想如果好几个结构体都有相同的方法,能不能直接重复用一下?

比如对于如下几个结构体:

1
2
3
4
5
6
7
8
struct shirt{ 
double price = 9.15;
void printPrice();
};
struct trousers {
double price = 2.33;
void printPrice();
};

这两个结构体都分别有一个printPrice函数,内容也都是一样的,但是要写两次,非常的不优雅,于是人们引入了继承的概念:

May there be inheritance!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Clothes {
double price;
void printPrice(){
// 输出price
}
}
// 定义Shirt类, 继承 Clothes 类.
class Shirt extends Clothes{
double price = 9.15;
}
// 同上, 继承 Clothes 类.
class Trousers extends Clothes{
double price = 2.33;
}

这样,printPrice函数只需要写一次.从某个类继承,或者说派生出来的类,会拥有父类的所有属性以及方法.子类也可以对应的重写这些方法.

同时,对于以上的代码中,显然 Clothes 类和别的类有一个很大的区别.我们会创建一个 Shirt 类的对象,但是我们不会执行new Clothes().Clothes类存在的意义在于提供给别人继承,我们不会实例化这个类,因此我们把这个类定义为抽象类(abstract class):

1
2
3
4
5
6
7
// 定义抽象类
abstract class Clothes {
double price;
void printPrice(){
System.out.println(this.price);
}
}

一个抽象类的意义是提供给别人来继承.代码中的 abstract 标记了这是一个抽象类.

多态

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
// 定义抽象类 Animal
abstract class Animal {
// 要求继承这个类的类必须实现
// bark方法, 同时提供一个默认的
// bark方法.
void bark(){
System.out.println("bark");
}
}

// 定义dog继承自animal
class Dog extends Animal{
// 重写bark方法
void bark(){
System.out.println("Woof");
}
}

// 定义cat继承自animal
class Cat extends Animal{
// 重写bark方法
void bark(){
System.out.println("Meow~");
}
}

上述代码中,Animal类规定了继承他的类要实现 bark() 方法.那么,对于如下的代码,会输出什么呢?

1
2
3
4
5
Animal a = new Dog();
// 创建一个Dog, 进行隐式类型转换,
// 转换为 Animal.

a.bark();

上面说到, Java中类的变量都是引用变量,因此上面代码可以不严谨的理解为:定义一个 指向Animal类型的指针a,

在这里,a是一个Animal类型的引用,指向一个Dog对象.但是,尽管他是 Animal 类型的引用,调用 a.bark()时仍然调用的是 Dogbark() 方法.

多态,狭义上指同一个名字(符号)指代多个物体.在上面代码中,如果不知道a指向的类型是什么,调用 a.bark() 有三种可能的情况.这就利用了多态的性质.

例子以及面向对象的好处.

C语言中,由于没有这些特性,极大地存在着代码冗余重复的现象,:printf函数用于格式化并向控制台输出内容,sprintf用于格式化并向字符串写入内容,fprintf用于格式化并向文件写入内容.

上面的三个函数,都完成了格式化这一个步骤,但是代码被编写了三次.如果需要向网络连接中格式化并写入内容,则又需要重复一遍格式化的操作.面向对象就能很好地解决这个问题.

,如果我们要向一个文件写入内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 注: 下面的注释从里往外看

// 把新建的缓存输出流对象作为参数
// 传入格式化输出流
PrintWriter out = new PrintWriter(

// 把新建的编码输出流对象
// 作为参数传入给缓存输出流
// 让缓存输出流对象往
// 编码输出流输出
new BufferedWriter(

// 把新建的文件输出流对象
// 作为参数, 传入编码输出流,
// 规定编码输出流把编码后
// 的内容输出到文件输出流
new OutputStreamWriter(

// 新建一个文件输出流对象
// 写入filename.txt
new FileOutputStream("filename.txt")
)
)
);

上面代码中,几个类的构造函数分别为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FileOutputStream(String name)
// 文件输出流, 参数是文件名
// 向指定文件输出内容
// 只能输出二进制的数据

OutputStreamWriter(OutputStream out)
// 编码输出流, 向某个输出流输出
// 除了接受二进制数据还可以接受字符串
// 将字符串编码后输出

BufferedWriter(Writer out)
// 缓存编码输出流
// 输入的东西缓存后输出

PrintWriter(Writer out)
// 格式化流
// 把输入的东西按照格式要求
// 变成字符串, 传入底层流.

可以看到,FileOutputStream接受文件名作为参数,单纯负责向文件中写入内容.

OutputStreamWriter 接受任何 OutputStream 对象,用于转换写入内容的编码.

BufferedWriter 接受一个Writer,用于缓存即将写入 Writer 的内容.

PrintWriter 接受一个Writer,用于格式化输入,写入 Writer.

高内聚,低耦合的设计模式就体现在了这里:一个 OutputStream 类只需要实现 write 方法,只能写入二进制字节数据,而一个 Writer 负责处理编码问题,可以写入字符串,负责把写入的字符串编码为二进制字节数据,BufferedWriter 则负责缓存上层写入的内容,也同样只提供了写入字符串的方法.PrintWriter负责提供格式化的方法,可以格式化并写入 int double char string 等多种类型.

这样的设计,使得功能的拓展变得很方便,比如我们要向一个自定义的东西中写入数据,完全可以只实现一个 OutputStream 而复用OutputStreamWriter,BufferedWriter,PrintWriter等很多和写入数据有关的类.

所以,这样的实际模式大概是被不断加需求改需求的产品经理逼出来的

感谢 ☁️学长,

(如果可以的话,能关注一下这个公众号吗)