C语言下的面向对象编程技术

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来释放对象,正如上例所示。


创建继承关系的类

下例创建了一个继承自MY_CLASS的子类MY_SUB:

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。

现在转到源文件,MyClassCtor将作为MY_CLASS的构造函数,在调用NNEW创建MY_CLASS对象的时候将被调用,该函数需要通过NCTOR_BIND宏来绑定到MY_CLASS的类实现中的,如上例所示。  

__MyClassGetValue__作为虚拟接口GetValue的默认实现通过NVCALL_BIND绑定到了类实现中。NVCALL_BIND需要知道这个接口是哪个类型中定义,以便调用虚拟函数时先获取该类型的虚表,因此要往NVCALL_BIND先传递MY_CLASS作为参数,因为GetValue是在MY_CLASS中定义的。其次,需要告诉NVCALL_BIND当前要绑定的是哪个接口,因此需要传递GetValue,最后需要传递GetValue 的实现__MyClassGetValue__。 由于__MyClassGetValue__仅仅是一个实现,从多态的特性中我们都知道,虚拟接口是可以被覆盖(重写)的,因此__MyClassGetValue__不应该通过头文件暴露给用户,在定义中添加双下划线__表明这是一个“受保护的”的方法。

现在我们通过实际例子使用MY_CLASS:
	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);

覆盖虚拟接口

接下来将实现面向对象的终极武器,即运行时绑定(或多态)。多态机制提供了覆盖虚拟函数的功能,这是面向对象的核心,可以令到不同类型的对象拥有不同的行为,这恰好模拟了现实中多数同类事物在个别方面具备不同性质的事实。因此下面的例子将在MY_SUB的定义覆盖MY_CLASS的虚拟接口,使其执行的是MY_SUB定义的行为。
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_SUB所定义的构造函数MySubCtor将SubValue初始化为2,但MY_SUB的基类MY_CLASS也定义了相应的构造函数,当调用NNEW创建MY_SUB的实例时,MY_CLASS和MY_SUB的构造函数将分别被调用,调用顺序和C++构造函数的顺序是一样的。为了验证我的说法,请看下面的例子:
	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);

结束语

至此为止,已经将NOOC最为核心的功能介绍完毕,或许对于大多数人来说,这些例子都不足以说明NOOC的威力,为此我在测试工程编写了两个单元测试:test_shooter_game和test_object_shooter_game(请查看test_shooter_game.cpp和test_object_shooter_game.cpp)。前者使用了C++语言来开发一个射击游戏的框架,其中包含了实际情形下用到各种C++的面向对象的技术,然后在test_object_shooter_game我使用NOOC重新实现整个框架,事实上证明这两者完全吻合。不过NOOC也有其不足之处,例如接口不够人性化,我不得不承认这点,但在C语言如此之多的限制之下,这已经是我能开发出来的最为简单的模式,如果您有更好的建议,不妨联系我。OOC即面向对象的C语言编程是一个非常有趣的话题,曾经有个国外作者著过一本名为《Object-Oriented Programming With ANSI-C》书,有兴趣的朋友可以去参考下,另外如果你在sourceforge.net下搜索OOC,也会出现一大堆的开源项目,有兴趣的朋友大可以对比其他实现和NOOC实现之间的区别。关于Nesty的下载站点,请前往 点击打开链接
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
学习C语言的好书. OOPC是指OOP(Object-Oriented Programming)与C语言的结合,它是一个面向对象C语言编程框架。它是一套C语言的宏,定义了OOP概念的关键字,借助于这一套宏,实现面向对象的特性,如类、对象、继承、接口、多态、消息等。   C++对于大型软件架构的良好可控性,和对以后程序员维护代码时良好的可读性;然而就目前来说,在嵌入式领域广泛的使用C++显然是不现实的事情。一般的嵌入式系统开发中只用到了其中的一小部分功能,而不需要全部的机制,比如多重继承、运算符重载等。因此,许多嵌入式系统的开发者就舍弃了C++的庞大身躯而回归到精简的C环境中。 一般情况下,一个更容易扩展、维护的软件通常采用的是OOP的思想,添加一个原本不存在的相对无关单独的个体,总比在一个已经存在的过程内硬塞进去一个对象要简单;而且面向过程更容易导致混乱的维护。然而舍弃C++的同时也舍弃了珍贵的OOP能力,实在太可惜了。 C语言良好的可移植性,对内存等良好的操作性以及执行之速度均是一般嵌入式产品的不二首选。我们要应此放弃C++吗?当然不,幸好已经有很多优秀的设计师为我们指明了C语言OOP化的道路。 虽然OOPC语法不如C++那么简洁,但是OOPC也有亮丽的特色,就是编译后的程序所占的内存空间比C++小的多,执行效率高,适用于Embedded System。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值