面向对象思想是什么?

有的人说C语言是一门面向过程的编程语言,但是面向过程只是一种设计思想,而不是某种语言的私有财产。那什么是面向过程思想,什么是面向对象思想?
面向过程专注于“先A,再B,然后C”的流程,适合解决线性的任务。
面向对象专注于“谁?什么状态?能干什么?”,适合复杂系统,例如linux内核。

为什么我们要学习面向对象思想?

你有没有过这种体验?读别人写的C语言项目,跟着一个函数跳来跳去,数据传来传去,改一个结构要动十个文件… 感觉自己不是在读代码,而是在走迷宫;或者当你自己写一个稍大的程序时,变量越加越多,函数越改越乱,到最后连自己都理不清。这通常不是C语言的错,而是我们被“一步一步来”的面向过程思维框住了。
面向对象思想可以帮助我们把复杂系统拆分成职责清晰的模块,减少函数跳转和全局状态的传播,使代码更易读、可维护并且便于扩展。
下面本文将从介绍C语言如何运用结构体、函数指针实现面向对象编程的核心理念——抽象、封装、继承与多态

面向对象的四大基本特性

抽象

我们可以对现实存在的各种事物进行抽象,把它封装成一种数据类型——类。
无论壁灯、吊灯、吸顶灯,他们都是灯,拥有各自的特点,我们可以将这些共同的东西进行抽象,封装成Lamp这个类。

封装

封装则是将灯的属性和方法都封装到一个结构体里。封装后的结构体就相当于一个类

typedef struct Lamp {
int on;
int brightness;
/* self是一个指向当前结构体实例的指针 */
void (*turn_on)(struct Lamp *self);
void (*turn_off)(struct Lamp *self);
} Lamp;

这样我们就得到了我们的Lamp类啦,但是子类想要使用该类的属性和方法该如何继承呢?

继承

我们封装一个类的目的就是为了继承,从而实现代码的复用。
继承的实现:将基类放在结构体开头来模拟继承。

/* "子类":FlashingLamp */
typedef struct {
Lamp base;
int freq_ms;
} FlashingLamp;

void lamp_turn_on(Lamp *self) { self->on = 1; puts("Lamp: on"); }
void lamp_turn_off(Lamp *self) { self->on = 0; puts("Lamp: off"); }

Lamp *Lamp_new(void) {
Lamp *l = malloc(sizeof(Lamp));
l->on = 0;
l->turn_on = lamp_turn_on;
l->turn_off = lamp_turn_off;
return l;
}

如代码所示,我们在结构体类型FlashingLamp里面内嵌了结构体类型Lamp,此时FlashingLamp就相当于模拟了一个子类,而Lamp相当于父类。通过这种内嵌就实现了子类继承了父类的属性和方法。
子类不仅可以直接复用父类中定义的属性和方法,还可以在父类的基础上拓展自己的属性和方法。
对于不同的灯我们打开的方式也不一样,所以我们可以在继承的过程中重新定义父类

多态

void flashing_turn_on(Lamp *self) {
/* 子类覆盖行为 */
puts("FlashingLamp: start flashing");
self->on = 1;
}

FlashingLamp *FlashingLamp_new(int freq_ms) {
FlashingLamp *f = malloc(sizeof(FlashingLamp));
f->base.on = 0;
f->base.turn_on = flashing_turn_on; /* 覆盖方法 */
f->base.turn_off = lamp_turn_off;
f->freq_ms = freq_ms;
return f;
}

int main(void) {
Lamp *a = Lamp_new();
/* 强制转换成基类指针 */
Lamp *b = (Lamp*)FlashingLamp_new(200);

a->turn_on(a);
b->turn_on(b);

a->turn_off(a);
b->turn_off(b);

return 0;
}

如上所示,我们通过基类指针去调用子类中的不同实现,就叫作多态。