Nesty框架提供了在C下进行面向对象编程的技术,下面将以一些简短的例子来说明其如何工作。
从第一个简单的例子开始
NOBJECT是NOOC(Nesty Object-Oriented C)框架中所有面向对象类型的基类,NOBJECT作为面向对象的编程接口提供了,对象拷贝,对象比较,对象哈希,对象字符串化,以及运行时类型识别和安全向下类型转换等功能。因此所有NOOC都必须从NOBJECT派生, 以下例子定义了一个派生自NOBJECT的对象MY_CLASS:
NOBJECT_PRED(MY_CLASS);
NOBJECT_DEC(MY_CLASS, NOBJECT);
struct tagMY_CLASS
{
NOBJECT_BASE(NOBJECT);
};
NOBJECT_IMP(MY_CLASS, NOBJECT);
其中使用到了4个关键的宏:NOBJECT_PRED用于前置声明对象;NOBJECT_DEC用于实际声明对象,指明对象之间的继承关系,从代码例子中可以看出,MY_CLASS继承自NOBJECT;由于对象实际上是用C的数据结构来模拟的,因此必须同时声明一个同名的数据结构用于存储对象的数据区,并tag作为前缀,如例子中的tagMY_CLASS;由于在继承关系中,子类需要包含父类的数据,因此还需要在数据结构的最前端使用NOBJECT_BASE宏类声明父类的数据区,例子中的则声明了父类NOBJECT的数据区。以上部分是对NOBJECT的声明,通常位于头文件中。
除了声明NOBJECT之外,还需要通过宏NOBJECT_IMP来给对象提供实现,NOBJECT_IMP置于源文件中,否则编译连接时将引发函数未提供实现的错误。NOBJECT_IMP同NOBJECT_DEC匹配,其指明的对象继承关系要一直,例如指明其父类为NOBJECT。
一旦对象成功声明,便可以创建和使用对象,如下列代码所示:
// 创建对象
MY_CLASS Obj = NNEW(MY_CLASS);
// 验证其类型
NASSERT(NObjectClass(Obj) == NCLASSOF(MY_CLASS));
// 验证继承关系
NASSERT(NISA(Obj, NOBJECT));
// 释放对象
NRELEASE(Obj);
由于对象是通过NEW产生的,在C语言无法提供自动垃圾回收,因此用户使用完对象需要调用统一的接口NRELEASE来释放对象,正如上例所示。
创建继承关系的类
NOBJECT_PRED(MY_SUB);
NOBJECT_DEC(MY_SUB, MY_CLASS);
struct tagMY_SUB
{
NOBJECT_BASE(MY_CLASS);
};
NOBJECT_IMP(MY_SUB, MY_CLASS);
其语法与之前的一模一样的,当成功声明了MY_SUB后,便可以开始使用,如下所示:
MY_CLASS ObjSub = (MY_CLASS)NNEW(MY_SUB);
// 验证继承关系
NASSERT(NISA(ObjSub, MY_SUB));
// 安全向下类型转换
MY_SUB ObjSub_2 = NCAST(ObjSub, MY_SUB);
NASSERT(ObjSub_2 != NULL);
在本例中,注意ObjSub是MY_CLASS类型的指针的,而实际上却是指向了一个MY_SUB类型的实例,在NOOC的规则中,向上转换必须使用强制类型转换。而后值得关注的是,我们还需要将ObjSub向下转换为MY_SUB类型,以方便操作MY_SUB的数据和接口。在面向对象的规则中,基类(父类)通常作为程序的接口来使用,因此基类的指针可以指向任何一个派生自基类的实例,就像上一个例子,ObjSub可以指向任何派生自MY_CLASS的类的实例,为了进行有效的向下转换,需要调用NCAST方法来实现动态转换,NCAST会验证指针实例的类层级关系,如上例中,只有ObjSub只MY_SUB的一个实例时,才会返回有效指针,否则返回空指针。
如同上例一样,使用完毕后还要释放对象:
// 释放对象
NRELEASE(ObjSub);
创建带接口和数据的类
// xxx.h
NOBJECT_PRED(MY_CLASS);
NOBJECT_DEC(MY_CLASS, NOBJECT,
NINT (*GetValue)(MY_CLASS InObj);
);
struct tagMY_CLASS
{
NOBJECT_BASE(NOBJECT);
NINT Value;
};
NINLINE NINT MyClassGetValue(MY_CLASS InObj) { return NOBJECT_VCALL(InObj, MY_CLASS, GetValue)(InObj); }
// xxx.c
void MyClassCtor(MY_CLASS InObj) {
InObj->Value = 0;
}
NINT __MyClassGetValue__(MY_CLASS InObj) {
return InObj->Value;
}
NOBJECT_IMP(MY_CLASS, NOBJECT,
NCTOR_BIND(MyClassCtor)
NVCALL_BIND(MY_CLASS, GetValue, __MyClassGetValue__)
);
注意上例中的注释,其指明两部分代码是分别位于头文件和源文件中的,在上例中,在NOBECT_DEC的声明最后插入了一个函数指针定义GetVallue,GetValue将作为MY_CLASS的一个虚拟接口;注意在上一节的声明中NOBJECT_DEC只描述了继承关系,而本节中新增加了函数的声明,这使用到了C99的宏可变参数的特性。同样,上例中在tagMY_CLASS的声明中新加了数据成员Value。
MY_CLASS Obj = NNEW(MY_CLASS);
NASSERT(Obj->Value == 5);
上例验证调用NNEW创建MY_CLASS将触发MY_CLASS的构造函数,将Value 初始化为5。接下来我们通过NOBJECT_VCALL宏来调用MY_CLASS定义的虚拟接口。
NINT Val = NOBJECT_VCALL(Obj, NOBJECT, GetValue)(Obj);
NASSERT(Val == 5);
NOBJECT_VCALL包含了两部分(注意观察圆括号),第一部分NOBJECT_VCALL(Obj, NOBJECT, GetValue)用于获取虚拟函数的地址。在面向对象类型的实例中,实例绑定了与其相关的类的类型(类类型),因此需要传递Obj实例来获取Obj类类型信息(即NCLASS)。有了类类型,接着要获取虚表,第二个参数传递MY_CLASS即告诉NOBJECT_VCALL,我要定位到MY_CLASS的虚表,最后一个参数GetValue及告诉我需要调用当前虚表中的哪个函数。当成功放回了虚拟函数的指针后,还要通过该函数指针调用实际的函数,从NINT (*GetValue)(MY_CLASS InObj);的定义,我们知道GetValue接受一个MY_CLASS实例作为参数。如果你发觉上面的例子比较难以看懂,则下面例子分解出来的步骤:
typedef NINT (*NPfnMyClassGetValue)(MY_CLASS InObj);
NPfnMyClassGetValue FnGetValue = NOBJECT_VCALL(Obj, MY_CLASS, GetValue);
NINT Val = FnGetValue(Obj);
实际上NOBJECT_VCALL是一个很难用的接口(我很希望能把它设计得更简单些),但为了方便调用,你可以使用宏或者内联(C99)去对NOBJECT_VCALL进行一次性封装便可,很方便,不困难。例如上例中,我们可以给MY_CLASS定义一个虚拟调用的接口,如:
// 用内联 需要C99支持
NINLINE NINT MyClassGetValue(MY_CLASS InObj) { return NOBJECT_VCALL(InObj, MY_CLASS, GetValue)(InObj); }
// 或者用宏包装 结果取决于你如何使用
#define MyClassGetValue(InObj) NOBJECT_VCALL(InObj, MY_CLASS, GetValue)(InObj)
经过封装后,对虚拟函数的调用将简化为下例所示:
NINT Val = MyClassGetValue(Obj);
NASSERT(Val == 5);
覆盖虚拟接口
NOBJECT_PRED(MY_SUB);
NOBJECT_DEC(MY_SUB, MY_CLASS);
struct tagMY_SUB
{
NOBJECT_BASE(MY_CLASS);
NINT SubValue;
};
void MySubCtor(MY_SUB InObj) {
InObj->SubValue = 2;
}
NINT __MySubGetValue__(MY_CLASS InObj) {
MY_SUB Obj = (MY_SUB)InObj;
return Obj->SubValue;
}
NOBJECT_IMP(MY_SUB, MY_CLASS,
NCTOR_BIND(MySubCtor)
NVCALL_BIND(MY_CLASS, GetValue, __MySubGetValue__)
);
所使用的语法与之前是一模一样的,不再介绍。唯一值得注意的是,在MY_SUB的NOBJECT_IMP实现中,将GetValue重新进行了绑定。因此当你通过调用NNEW来创建MY_SUB对象,并调用GetValue,这时执行的是__MySubGetValue__,而不是__MyClassGetValue__。到目前为止,这便是NOOC中最为重要的技术。
MY_CLASS ObjSub = (MY_CLASS)NNEW(MY_SUB);
// 访问MY_CLASS.Value
NASSERT(ObjSub->Value == 5);
MY_SUB ObjSub_2 = NCAST(ObjSub, MY_SUB);
// 访问MY_SUB.SubValue
NASSERT(ObjSub_2->SubValue == 2);