前言
随着编程经验逐渐丰富,我发现编程的思想并不局限于任何一种语言,比如我们就可以使用C语言去实现类似于C++和Java的面向对象(OOP)编程。我惊讶于C语言的强大,因为它可能写出像linux这样庞大的系统。对于现在的大型工程来说,面向对象是必不可少的一种东西了。因此本篇博文详细介绍如何使用C这样一种简单语言去实现复杂的面向对象编程。
之前我的一篇博文已经介绍了如何使用C语言来模拟面向对象的编程范式,但是那篇其实也是借鉴了其他人的博客内容,而且思路有写混乱。现在,我从工程角度再次梳理这个问题,并举了一个设计模式之一——观察者模式
的应用案例。
约定
约定,是面向对象的基础内容。我们约定了一此面向对象特性如何使用C语言来实现。对于熟知面向对象的人来说,面向对象主要包含了封装、继承、多态等内容。约定基本包含这些。
封装约定
(1) 类定义
定义类其实就是定义类型。我们倾向于使用结构体来存储数据成员。在这里,使用typedef 我们把一个结构体定义成为了一种类型。这与面向对象中的类型是一致的。它的作用是将零散的数据封装在一起。一般我们让类名以大驼峰
方式命名, 类的成员变量以小驼峰
式命名。
typedef struct {
char name[100];
int age;
short sex;
} Person;
(2)构造函数
这个约定包含有内容如下:
- 构造函数名称由
<类名>Construct
命名 - 构造函数第一个参数为:
<类名> * self
之所以这样约定,是因为保持规范可以让人很容易接受。如果没有此规范,你可能会取名为 init
、‘init_person
、person_constructor
…。不够简洁且让人迷惑。构造函数名称主要借鉴了C++和Java语言的习惯。 构造函数其实就是一种普通的函数,它的第一个参数接受一个对象的指针,也就相当当于C++和Java中的this指针
。其它参数为构造函数的参数。像任何面向对象语言一样,你可以在构造函数里对对象成员进行初始化。
void PersonConstruct(Person* self, const char name, int age, short sex) {
strcpy(self->name, name);
self->age = age;
self->sex = sex;
}
(3)成员函数
成员函数也是普通函数,它在最前面加上了类名, 同时以小驼峰
方式命名。同样,它的第一个参数必须为一个对象的指针。下面的例子展示了如何书写getter种setter函数。
char* personGetName(Person* self) {
return self->name;
}
void personSetName(Person* self, const char* name) {
strcpy(self->name, name);
}
(4)析构函数
C语言需要手动管理内存,所以析构函数也是十分有必要的。也要注意它的命名, 由 <类名>Destruct
组成。管理内存是十分重要的一件事情,处理不好会导致程序内存泄露(Out Of Memory, OOM)。一般的管理原则是:谁创建,就由谁为管理,即在类里面申请的内容,就应当在类里面释放。
void PersonDestruct(Person* self) {
// 调用 free() 释放内存
// 或者 调用 成员变量 的 析构函数
}
(5)对象创建与销毁
由于C语言的限制,对象在创建之后要立即调用构造函数进行初始化。对象不用之后要立即调用析构函数释放在运行期间申请的内存。
Person person;
PersonConstruct(&person, "张三", 18, 0);
personSayHi(&person);
PersonDestruct(&person);
(6)书写规范
一个类可以单独写在一个文件之中。 我也推荐开发者们都这样做。为了能让类可以方便地定义和使用,最好把类定义在头文件中。在使用的里使用#include
包含进来就可以了。以下是一个完整的示例。
#ifndef __Person__
#define __Person__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[100];
int age;
short sex;
} Person;
void PersonConstruct(Person* self, const char* name, int age, short sex) {
strcpy(self->name, name);
self->age = age;
self->sex = sex;
}
void PersonDestruct(Person* self) {
// free some memory
// or call member's release function
}
char* personGetName(Person* self) {
return self->name;
}
void personSetName(Person* self, const char* name) {
strcpy(self->name, name);
}
int personGetAge(Person* self) {
return self->age;
}
void personSetAge(Person* self, int age) {
self->age = age;
}
short personGetSex(Person* self) {
return self->sex;
}
void personSetSex(Person* self, short sex) {
self->sex = sex;
}
void personSayHi(Person* self) {
printf("Hi~ 我的名字是%s, 今年%d岁, 是%s生\n",
self->name,
self->age,
self->sex == 0 ? "男" : "女");
}
#endif
PS:你甚至可以自己定义析构函数、拷贝构造函数等。
继承约定
有时候我们需要继承某些类以便不用重复造轮子。实践证明,继承可以使用组合
的方式来替代。
(1)继承
继承的方式是直接组合
基类实例作为结构体的第一个
对象,这样就可以方便地实现对象的上转型
。下面的例子演示了Student类继承Person类的书写方式。
typedef struct {
Person person; //直接组合
int studentID;
} Student;
(2)构造函数
规范的继承方式是在构造函数中先调用一下基类的构造函数先初始化基类对象,再执行构造子类的代码。如下所示。
void StudentConstruct(Student* self, const char* name, int age, short sex, int studentID) {
PersonConstruct((Person*)self, name, age, sex); //先调用基类的构造函数
strcat(self->person.name, "(student)");
self->studentID = studentID;
}
(3)析构函数
在析构对象的时候也要注意先析构当前对象,再析构基类对象。
void StudentDestruct(Student* self) {
// 释放当前对象申请的内存
// 最后调用基类析构
PersonDestruct((Person*)self);
}
(4)访问基类成员
在子类中我们也可以方便地访问基类的成员(成员变量或成员函数)。但是这种函数调用方式不能实现多态。在之后的小节中将介绍如何解决这个问题。
void StudentSayHi(Student* self) {
personSayHi((Person*)self);
printf("我是一个学生哦, 学号是:%d \n", self->studentID);
}
(5)书写规范
我们建议把子类也单独放在一个头文件中。下面是完整的示例
#ifndef __Student__
#define __Student__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "Person.h"
typedef struct {
Person person; //直接组合
int studentID;
} Student;
void StudentConstruct(Student* self, const char* name, int age, short sex, int studentID) {
PersonConstruct((Person*)self, name, age, sex); //调用基类的构造函数
strcat(self->person.name, "(student)");
self->studentID = studentID;
}
void StudentDestruct(Student* self) {
// 释放当前对象申请的内存
// 最后调用基类析构
PersonDestruct((Person*)self);
}
void StudentSayHi(Student* self) {
personSayHi((Person*)self);
printf("我是一个学生哦, 学号是:%d \n", self->studentID);
}
#endif
调用子类 SayHi函数:
多态约定
多态有两种,一种是编译时的多态,叫做函数的重载
,这种多态用处不大,另一种是运行时多态,这才是面向对象编程的核心。我们主要实现的就是运行时多态。它的表现为:当使用基类指针调用某个函数时,实现上执行的是子类中的函数,因为在子类中把基类对应函数覆盖
了。能够实现这种神奇的功能,主要得益于 虚拟表
这个东西。大家可以查看我的博文 : 用c模拟面向对象编程 。 为了简化描述,这里就不使用虚拟表了,但是同样可以实现多态哦。
(1)基类的虚函数
直接定义在基类中作为成员变量的函数,我们称之为虚函数
。虚函数也应该像成员函数那样,把第一个参数作为对象的指针。
typedef struct ObjectTag{
void (*toString)(ObjectTag* self, char* str);
int (*equals)(ObjectTag* self, ObjectTag* other);
} Object;
(2)抽像基类与非抽象基类
如果虚函数没有被赋值或赋值为0,那么这个基类就是抽象的
。对于抽象的基类,我们不能实例化,因为如果有一个抽象基类的实例,它的成员函数指针并没有被赋值,调用时会出现运行时异常。但是如果我们给它们都附值了,那么这个基类将变成非抽像的
。非抽象基类必须在构造函数中对虚函数指针附值:如下代码所示。为了规范,给虚函数附的实际函数名应该是 __<类型><虚函数名>
, 并且以小驼峰命名。
int __objectEquals(Object* self, Object* other) {
return self == other;
}
void __objectToString(Object* self, char* str) {
strcpy(str, "object");
}
void ObjectConstruct(Object* self) {
self->equals = __objectEquals; //给虚函数指针附值
self->toString = __objectToString; //给虚函数指针附值
}
void ObjectDestruct(Object* self) {
// ....
}
(3)覆盖规约
第一步应该是继承具有虚函数的基类,要不然覆盖何从谈起呢。 其实,要定义一个子类自己的用于覆盖基类某个虚函数的函数。这里以覆盖Objet中的toString()函数为例。子类Person定义了 __personToString() 用于覆盖。最后,在子类的构造函数中覆盖。覆盖的时机我们约定为:在调用基类构造函数之后立即进行覆盖。
typedef struct {
Object object; //step 1: 继承抽象/ 非抽象基类
char name[100];
int age;
short sex;
} Person;
//step 2: 定义一个用于覆盖的新函数
void __personToString(Object* self, char* str) {
Person* personSelf = (Person*)self;
strcpy(str, "person");
}
void PersonConstruct(Person* self, const char* name, int age, short sex) {
ObjectConstruct((Object*)self);
((Object*)self)->toString = __personToString; //step 3: 覆盖!
strcpy(self->name, name);
self->age = age;
self->sex = sex;
}
(4)调用示例
写一段代码测试一下效果。
#include <stdlib.h>
#include "Person.h"
#include "Student.h"
void show(Object* object) {
char buf[100];
object->toString(object, buf); //执行的是子类Person的函数
printf("对象:%s \n", buf);
printf("自己和自己不否相等:%d \n", object->equals(object, object)); // 执行的是Object自己的函数
}
int main() {
Student student;
StudentConstruct(&student, "张三", 18, 0, 10086);
show((Object*)(&student));
StudentDestruct(&student);
system("pause");
return 0;
}
示例
本示例,用C语言实现了观察者模式。
首先实现两个抽象的 观察者(Observer)
和观察目标(Subject)
类。
#ifndef __Subject__
#define __Subject__
#include "List.h"
#include "Observer.h"
typedef struct SubjectTag{
Object super;
List observers;
void (*subjectAddObserver)(SubjectTag* self, Observer* observer);
void (*subjectNotifyObservers)(SubjectTag* self);
} Subject;
void SubjectConstruct(Subject* self) {
ObjectConstruct((Object*)self);
ListConstruct((List*)(&(self->observers)), 0);
}
void SubjectDestruct(Subject* self) {
ListDestruct((List*)(&(self->observers)));
ObjectDestruct((Object*)self);
}
#endif
#ifndef __Observer__
#define __Observer__
#include "Object.h"
typedef struct ObserverTag{
Object super;
void (*update)(ObserverTag* self, void*);
} Observer;
void ObserverConstruct(Observer* self) {
ObjectConstruct((Object*)self);
}
void ObserverDestruct(Observer* self) {
ObjectDestruct((Object*)self);
}
#endif
其中, List
是我们自己实现的一个链表类。如下:
#ifndef __List__
#define __List__
#include "Object.h"
typedef struct ListTag {
Object super; //继承Object
Object* val; //链表包含一个值
ListTag* next;
} List;
void ListConstruct(List* self, Object* val) {
ObjectConstruct((Object*)self);
self->val = val;
self->next = 0;
}
void ListDestruct(List* self) {
List* i = self->next;
while (i != 0)
{
List* temp = i;
i = i->next;
free(temp);
}
}
void listAdd(List* self, Object* val) {
List* i = self;
while (i->next != 0)
{
i = i->next;
}
//创建一个新的结点
List* node = (List*)malloc(sizeof(List));
ListConstruct(node, val);
//插入链表最后
i->next = node;
}
void listForEach(List* self, void (*callback)(Object* val)){
List* i = self->next;
while (i != 0)
{
callback(i->val);
i = i->next;
}
}
#endif
然后分别实现这两个抽象类的具体类。
Master
继承自 Subject
。
AniamlDog
和 AnimalCat
继承自 Observer
#ifndef __Master__
#define __Master__
#include <string.h>
#include "Subject.h"
#include "List.h"
typedef struct {
Subject super;
char name[100];
}Master;
void __masterAddObserver(Subject* self, Observer* observer) {
List* observers = &(self->observers);
listAdd(observers, (Object*)observer);
}
void __masterNotifyObservers(Subject* self) {
List* observers = &(self->observers);
for (auto& i = observers->next; i != 0; i = i->next) {
Observer* observer = ((Observer*)(i->val));
observer->update(observer, ((Master*)self)->name);
}
}
void MasterConstrct(Master* self, const char* name) {
SubjectConstruct((Subject*)self);
((Subject*)self)->subjectAddObserver = __masterAddObserver;
((Subject*)self)->subjectNotifyObservers = __masterNotifyObservers;
strcpy(self->name, name);
}
void MasterDestruct(Master* self) {
SubjectDestruct((Subject*)self);
}
void MasterCall(Master* self) {
Subject* subject = ((Subject*)self);
subject->subjectNotifyObservers(subject);
}
#endif
#ifndef __AnimalDog__
#define __AnimalDog__
#include <stdio.h>
#include <string.h>
#include "Observer.h"
typedef struct {
Observer super;
char name[100];
} AnimalDog;
void __animalDogUpdate(Observer* self, void* msg) {
char* masterName = (char*)msg;
char* dogName = ((AnimalDog*)self)->name;
printf("主人%s,听到你叫我了,我是你的宠物狗: %s \n", masterName, dogName);
}
void AnimalDogConstruct(AnimalDog* self, const char* name) {
ObserverConstruct((Observer*)self);
((Observer*)self)->update = __animalDogUpdate;
strcpy(self->name, name);
}
void AnimalDogDestruct(AnimalDog* self) {
ObserverDestruct((Observer*)self);
}
#endif
#ifndef __AnimalCat__
#define __AnimalCat__
#include <stdio.h>
#include <string.h>
#include "Observer.h"
typedef struct {
Observer super;
char name[100];
}AnimalCat;
void __animalCatUpdate(Observer* self, void* msg) {
char* masterName = (char*)msg;
char* catName = ((AnimalCat*)self)->name;
printf("主人%s,听到你叫我了, 我是你的宠物猫: %s \n", masterName, catName);
}
void AnimalCatConstruct(AnimalCat* self, const char* name) {
ObserverConstruct((Observer*)self);
((Observer*)self)->update = __animalCatUpdate;
strcpy(self->name, name);
}
void AnimalCatDestruct(AnimalCat* self) {
ObserverDestruct((Observer*)self);
}
#endif
最后编写主函数测试我们的代码。
#include <stdlib.h>
#include "Master.h"
#include "AnimalDog.h"
#include "AnimalCat.h"
int main() {
Master master; MasterConstrct(&master, "小明");
AnimalDog dog1; AnimalDogConstruct(&dog1, "旺财");
AnimalDog dog2; AnimalDogConstruct(&dog2, "小狗狗");
AnimalCat cat1; AnimalCatConstruct(&cat1, "咪咪");
AnimalCat cat2; AnimalCatConstruct(&cat2, "喵喵");
Subject* subject = ((Subject*)(&master));
subject->subjectAddObserver(subject, (Observer*)(&dog1));
subject->subjectAddObserver(subject, (Observer*)(&dog2));
subject->subjectAddObserver(subject, (Observer*)(&cat1));
subject->subjectAddObserver(subject, (Observer*)(&cat2));
MasterCall(&master); //召唤小动物们
MasterDestruct(&master);
AnimalDogDestruct(&dog1);
AnimalDogDestruct(&dog2);
AnimalCatDestruct(&cat1);
AnimalCatDestruct(&cat2);
system("pause");
return 0;
}
运行结果如下:
总结
原来C语言也可以这么厉害。但是使用C语言来实现面向对象编程还是稍显繁琐。内存管理比C++还难。除了我这种特殊癖好估计也没人会用了吧。本博文的代码已经开源到码云,欢迎一起来学习!