在上一篇中简单的提了一下面向对象以及c语言实现面向对象,写得一塌糊涂,这里就不管了,本篇主要介绍lw_oopc这个东东。
lw_oopc是由高焕堂及其MISOO团队创作的(在《UML+OOPC嵌入式C语言开发精讲》一书中有相关介绍),本文说的lw_oopc并非《UML+OOPC嵌入式C语言开发精讲》中原版的lw_oopc,而是source forge上下 载的lw_oopc(在原版的基础上有经过优化和功能增加,如增加了ABS_CLASS(虚基类),且是以LGPL协议开源),其作者是 金永华,在其readme文档中有对两者(原版和这个1.2版本)的对比:
原有高焕堂先生及其MISOO团队创作的宏(总共6个宏),清单如下:
高焕堂及MISOO创作的宏 | 是否存在问题? | 是否修改? |
---|---|---|
INTERFACE | 没有问题 | 否 |
CLASS | 无法支持继承 | 是 |
CTOR | 对申请不到内存的情况未保护 | 是 |
END_CTOR | 没有问题 | 否 |
FUNCTION_SETTING | 没有问题 | 否 |
IMPLEMENTS | 没有问题 | 否 |
为了更好的支持面向对象以及面向接口编程,我(金永华)增加了14个宏:
金永华创作的宏 | 创作目的(为了解决什么问题?) |
---|---|
DTOR | 为了支持析构函数的概念 |
END_DTOR | |
ABS_CLASS | 为了支持抽象类的概念 |
ABS_CTOR | 为了支持可继承的抽象类的构造函数 |
END_ABS_CTOR | |
EXTENDS | 为了让熟悉Java的人容易理解(与IMPLEMENTS宏等同) |
SUPER_CTOR | 为了支持子类调用父类的构造函数 |
SUPER_PTR | 为了支持向上转型 |
SUPER_PTR_2 | |
SUPER_PTR_3 | |
SUB_PTR | 为了支持向下转型 |
SUB_PTR_2 | |
SUB_PTR_3 | |
INHERIT_FROM | 为了支持访问直接父类的数据成员 |
由于篇幅所限,并一定会每个宏都做介绍,因为这些宏都比较简单,所以并不会过多的介绍宏本身,更多的是以使用角度来讲。
首先咱就来说说这个最基本的关键字(CLASS),在lw_oopc中CLASS这个宏定义如下:
#define CLASS(type) /*其中的type为所要定义的类名*/ \
typedef struct type type; /*使用typedef定义一个名为‘type’的类名*/ \
type* type##_new(lw_oopc_file_line_params); /*创建对象的函数,类似c++中的new*/ \
void type##_ctor(type* t); /*类的构造函数声明*/ \
int type##_dtor(type* t); /*类的析构函数声明*/ \
void type##_delete(type* t); /*释放对象的函数,类似c++中的delete*/ \
struct type /*名为‘type’的结构体定义*/
也以上一篇中Person类为例,定义如下:
CLASS(Person)
{
void (*Init)(struct Person* pPerson, int age, const char* name);
void (*sayHello)(struct Person* pPerson);
int mAge;
char* mName;
};
与上一篇中使用c语言的结构体来模拟类相比,这里就少了一个Uninit方法,因为这个方法的行为将在Person的析构函数中进行。至于ABS_CLASS和INTERFACE宏与CLASS基本类似,只是没有type##_new以及type##_delete方法。
这组宏用于定义类的构造函数,具体宏定义如下:
#define CTOR(type) \
type* type##_new() { /*实现创建对象的函数*/ \
struct type *cthis; \
cthis = (struct type*)malloc(sizeof(struct type)); /*为对象(即struct)分配内存*/ \
if(!cthis) \
{ \
return 0; \
} \
type##_ctor(cthis);/*调用类的构造函数*/ \
return cthis; \
} \
\
void type##_ctor(type* cthis) {/*定义类的构造函数*/
#define END_CTOR } /*类构造函数的结束大括号*/
CTOR宏中的malloc可以使用其他的接口函数代替,以便记录一些debug信息,在lw_oopc中在memory leak检测的时 候就会使用:
lw_oopc_malloc(sizeof(struct type), #type, file, line);
代替malloc,他会记录类的名字,源码的文件名,以及在文件中的行号。
这组宏是在金永华创作的lw_oopc当中新增的,定义类的析构函数,与CTOR相对应:
#define DTOR(type) \
void type##_delete(type* cthis) /*实现释放对象的函数*/ \
{ \
if(type##_dtor(cthis)) /*调用类的析构函数*/ \
{ \
lw_oopc_free(cthis);/*释放对象的内存*/ \
} \
} \
int type##_dtor(type* cthis) /*定义类的析构函数*/ \
{
#define END_DTOR } /*类析构函数结束大括号*/
这个宏用于注册成员函数,它的定义也比较简单,基本上就是一个赋值语句(当然,如果有需要的话也可以丰富这个宏的内容,让其功能更强大):
#define FUNCTION_SETTING(f1, f2) cthis->f1=f2;/*将成员函数赋值给struct中的函数指针*/
以上几个定义和注册函数的使用方式如下:
/* 声明Person类 */
CLASS(Person)
{
void (*Init)(struct Person* pPerson, int age, const char* name);
void (*sayHello)(struct Person* pPerson);
int mAge;
char* mName;
};
/*定义成员函数,为了封装性,建议在.c档案中定义static类型的函数*/
static void init(struct Person* pPerson, int age, const char* name)
{
pPerson->mAge = age;
pPerson->mName = name;/*为了简单表示,这里不去关心字符串内存问题*/
}
static void sayHello(struct Person* pPerson)
{
printf("hello, my name is:%s and i'm %d years old\n", pPerson->mName, pPerson->mAge);
}
/*定义构造函数*/
CTOR(Person)
cthis->mAge = 0;
cthis->mName = NULL; /*初始化成员函数*/
FUNCTION_SETTING(Init, init);
FUNCTION_SETTING(sayHello, sayHello); /*注册成员函数*/
END_CTOR
/*定义析构函数*/
DTOR(Person)
//todo release member variable
return lw_oopc_true; //如果需要释放对象就分会true,反之返回false(这样可以实现引用计数之类的应用)
END_DTOR
/*具体使用*/
int main(int argc, char* argv[])
{
Person* pPerson = Person_new(); /*创建Person的对象实例*/
pPerson->Init(pPerson, 12, "LiLei");/* 初始化对象 */
pPerson->sayHello(pPerson);
Person_delete(pPerson);
return 0;
}
ABS_CLASS宏对应于c++中的虚基类(因为只是c语言的宏,所以无法从语法规则上去约束其只是虚基类,也就是说具体使用的时候也可能将其作为普通的基类(即所有成员函数都有实现))。这个宏的作用就是可以让c语言实现继承以及多态。当然这里也有不少缺陷和约束,比如因为不是语法规则上的约束,所以无法在编译阶段就知道是不是所有成员函数都有对应的实现。
其宏定义和CLASS差不多,只是没有type##_new以及type##_delete方,所以这里就不在将其列出来了,下面就以一个例子来说明其使用方式。
这里以lw_oopc的demo中关于动物的例子(会有适当的修改,详情请到lw_oopc的源码包中去查看),动物是一个基类,而狗,鱼等都属于动物,是动物的子类(is-a关系):
#include "lw_oopc.h"
#include
ABS_CLASS(Animal)/*声明动物 基类*/
{
char name[128]; // 动物的昵称(假设小于128个字符)
int age; // 动物的年龄
void (*setName)(Animal* t, const char* name); // 设置动物的昵称
void (*setAge)(Animal* t, int age); // 设置动物的年龄
void (*sayHello)(Animal* t); // 动物打招呼
void (*init)(Animal* t, const char* name, int age); // 初始化昵称和年龄
};
CLASS(Fish) /*声明 鱼 类*/
{
EXTENDS(Animal); // 继承Animal抽象类
void (*init)(Fish* t, const char* name, int age);
};
CLASS(Dog) /*声明 狗 类*/
{
EXTENDS(Animal); // 继承Animal抽象类
void(*init)(Dog* t, const char* name, int age);
};
/
/
/* 设置动物的昵称 */
void Animal_setName(Animal* t, const char* name)
{
// 这里假定name小于128个字符,为简化示例代码,不做保护(产品代码中不要这样写)
strcpy(t->name, name);
}
/* 设置动物的年龄 */
void Animal_setAge(Animal* t, int age)
{
t->age = age;
}
/* 动物和我们打招呼 */
void Animal_sayHello(Animal* t)
{
printf("Hello! 我是%s,今年%d岁了!\n", t->name, t->age);
}
/* 初始化动物的昵称和年龄 */
void Animal_init(Animal* t, const char* name, int age)
{
t->setName(t, name);
t->setAge(t, age);
}
ABS_CTOR(Animal) /*定义 动物 类的构造函数,并注册成员函数*/
FUNCTION_SETTING(setName, Animal_setName);
FUNCTION_SETTING(setAge, Animal_setAge);
FUNCTION_SETTING(sayHello, Animal_sayHello);
FUNCTION_SETTING(init, Animal_init);
END_ABS_CTOR
/
/* 初始化鱼的昵称和年龄 */
void Fish_init(Fish* t, const char* name, int age)
{
Animal* animal = SUPER_PTR(t, Animal);
animal->setName(animal, name);
animal->setAge(animal, age);
}
CTOR(Fish)/*定义 鱼 类的构造函数,并注册成员函数*/
SUPER_CTOR(Animal);
FUNCTION_SETTING(init, Fish_init);
END_CTOR
/* 初始化狗的昵称和年龄 */
void Dog_init(Dog* t, const char* name, int age)
{
Animal* animal = SUPER_PTR(t, Animal);
animal->setName(animal, name);
animal->setAge(animal, age);
}
CTOR(Dog)/*定义 狗 类的构造函数,并注册成员函数*/
SUPER_CTOR(Animal);
FUNCTION_SETTING(init, Dog_init);
END_CTOR
///
///
int main(int argc, char* argv[])
{
Fish* fish = Fish_new(); // 创建鱼对象
Dog* dog = Dog_new(); // 创建狗对象
Animal* animal = NULL;
// 初始化鱼对象的昵称为:小鲤鱼,年龄为:1岁
fish->init(fish, "小鲤鱼", 1);
// 初始化狗对象的昵称为:牧羊犬,年龄为:2岁
dog->init(dog, "牧羊犬", 2);
// 将fish指针转型为Animal类型指针
animal = SUPER_PTR(fish, Animal);
animal->sayHello();
// 将dog指针转型为Animal类型指针
animal = SUPER_PTR(dog, Animal);
animal->sayHello();
lw_oopc_delete(fish);
lw_oopc_delete(dog);
return 0;
}
从上面的例子可以看出,c模拟面向对象毕竟不是原生支持,必然会有很多缺陷和约束,比如这个狗类中继承动物类的sayHello函数,就无法使用dog对象去调用,必须将其转换为animal对象方能调用。
这里的继承其实可以用在c的模块开发当中,ABS_CLASS定义模块的接口,CLASS再定义各种子模块,比如设计一个stream模块(各种流操作),就可以使用ABS_CLASS定义出stream模块的对外接口,以及一些stream模块内部各个子模块可共用的接口,然后在stream模块就可以使用CLASS去定义一些具体的子模块,如本地文件CLASS(FileStream),网络文件CLASS(HttpStream)等等。
这个概念应该源自java中的interface,以interface 关键字定义一套接口方法(这里只有方法的声明,没有实现 ),然后再在具体的类中去实现这些接口方法。
那么问题就来了,既然有了ABS_CLASS,那还要INTERFACE干嘛呢? 就以lw_oopc的demo中的例子来说,动物,除了有呼吸,吃东西等行为之外,还有一个共同的特征就是“移动”。如 果以ABS_CLASS的方式实现的话,就会在Animal类中定义一个move方法。
那是不是只有动物才会“移动”呢? 显然不是,最常见的比如汽车,也是会移动的物体,当它却不是动物,如果以 ABS_CLASS方式实现,那么就需要在Animal和Car类中都声明move方法,为何要如此呢? 这是因为动物和汽车是不 同的类别。
当然这里还有另一种实现方式——INTERFACE,move是一种抽象的行为,谁有这种行为,就去实现这个INTERFACE,汽车能够移动,那么汽车类就去实现汽车的移动方式,动物能够移动,那动物类就去实现动物的移动方式。
这里还是以lw_oopc中的例子来说明 INTERFACE的具体应用:
#include "lw_oopc.h"
#include
INTERFACE(IMoveable)
{
void (*move)(IMoveable* t); // Move行为
};
/*这里为了简单,就不用Animal基类了*/
CLASS(Fish)
{
IMPLEMENTS(IMoveable); // 实现IMoveable接口
void (*sayHello)(Fish* t); // 动物打招呼
void (*init)(Fish* t, const char* name, int age);
char name[128]; // 动物的昵称(假设小于128个字符)
int age; // 动物的年龄
};
///
///
/* 鱼的移动行为 */
void Fish_move(IMoveable* t)
{
printf("鱼在水里游!\n");
}
/* 初始化鱼的昵称和年龄 */
void Fish_init(Fish* t, const char* name, int age)
{
// 这里假定name小于128个字符,为简化示例代码,不做保护(产品代码中不要这样写)
strcpy(t->name, name);
t->age = age;
}
/* 动物和我们打招呼 */
void Fish_sayHello(Fish* t)
{
printf("Hello! 我是%s,今年%d岁了!\n", t->name, t->age);
}
CTOR(Fish)/*定义 鱼 类的构造函数,并注册成员函数*/
FUNCTION_SETTING(IMoveable.move, Fish_move);
FUNCTION_SETTING(init, Fish_init);
FUNCTION_SETTING(sayHello, Fish_sayHello);
END_CTOR
///
///
int main(int argc, char* argv[])
{
Fish* fish = Fish_new(); // 创建鱼对象
IMoveable* moveObj = NULL;
// 初始化鱼对象的昵称为:小鲤鱼,年龄为:1岁
fish->init(fish, "小鲤鱼", 1);
//鱼打招呼
fish->sayHello();
// 将fish指针转型为IMoveable接口类型指针
moveObj = SUPER_PTR(fish, IMoveable);
//鱼移动
moveobj->move();
lw_oopc_delete(fish);
return 0;
}
从以上简介可以看出,虽然其可以模拟面向对象编程,但显然其中有不少缺陷和约束。而且这些宏的功能虽然基本的都有了,用于简单的学习是完全足够,但如果要用于实际的开发项目,可能就还需要更多的功能扩展和优化(这个下一章会有相关说明),现在我们来总结一下lw_oopc宏的一些优缺点(都是寡人的门户之见,并不一定正确,如有不同看法,欢迎拍砖):
- 优点:
1. 比较简洁,基本上一看就懂,学习曲线比较平缓;
2. 能在一定程度上实现面向对象编程;
3. 有利于功能模块以及子模块的划分和隔离。
- 缺点:
1. 功能还不完善,需要进一步增强和优化才能更好的用于实际项目;
2. 由于是使用的宏定义,会使用预处理机制,所以在这些宏定义中就无法在编译时进行类型检测;
3. 由于是模拟的面向对象编程,所以没有原生支持的编程语言使用起来方便;
4. 无法在编译时就检测成员函数是否已经实现,如未实现,就会在运行时产生segmentation fault错误。
5. 阅读不方便,如source insight这样的工具,无法判断以CLASS等宏定义的结构体,在阅读以 及代码跳转查看的时候不太方便