文章目录

  • 参考
  • 摘要
  • 为什么要用面向对象?
  • 为什么不直接使用C++?
  • LW_OOPC是什么?
  • LW_OOPC宏介绍
  • LW_OOPC最佳实践
  • LW_OOPC的优点
  • LW_OOPC的缺点

参考

轻量级的面向对象C语言编程框架LW_OOPC介绍
作者: 金永华、陈国栋

摘要

本文介绍一种轻量级的面向对象的C语言编程框架:LW_OOPC。LW_OOPC是Light-Weight Object-Oriented Programming in(with) C的缩写,总共一个.h文件,20个宏,约130行代码,非常的轻量级,但却很好的支持了很多面向对象的特性,比如继承、多态,可以优美的实现面向接口编程。这个框架系由台湾的高焕堂先生以及他的MISOO团队首创,之后由我继续改进优化,最后,经高焕堂同意以LGPL协议开源(开源网址参见后文)。
用C语言实现OO?我没听错吗?这听起来真是太疯狂了!… 大家都知道,C++支持了面向对象和面向泛型编程,比C要更强大些。那么,为什么要在C语言中实践面向对象呢?为什么不直接使用C++呢?

为什么要用面向对象?

面向过程方式开发的系统,代码复杂,耦合性强,难以维护,随着我们所要解决的问题越来越复杂,代码也变得越来越复杂,越来越难以掌控,而面向对象改变了程序员的思维方式,以更加符合客观世界的方式来认识世界,通过合理的运用抽象、封装、继承和多态,更好的组织程序,从而很好地应对这种复杂性。

为什么不直接使用C++?

C和C++之争由来已久,可能要持续到它们中的一种去世_。C语言以其简洁明快,功能强大的特点,深得开发人员的喜爱,尤其是在嵌入式开发领域,C语言更是占据了绝对老大的地位。在我看来,语言只是工具,作为程序员,我们要做的是:选择合适的语言,解决恰当的问题。我们要尊重事实,考虑开发环境(软硬件环境),考虑团队成员的水平,从商用工程的角度讲,选择团队成员擅长的语言进行开发,风险要小很多。
一些从Java/C#转到C的程序员们,无法从面向对象切换到面向过程,但又必须与C语言同事们在遗留的C系统上开发软件,他们有时会非常困惑:C语言是面向过程的编程语言,如何实践面向对象,甚至面向接口编程呢?此时,就非常需要在C语言中实现面向对象的手段,而LW_OOPC正是应对这一难题的解决之道。

LW_OOPC是什么?

简而言之:LW_OOPC是一套C语言的宏,总共1个.h文件(如果需要内存泄漏检测支持以及调试打印支持,那么还需要1个.c文件(lw_oopc.c,约145行)),20个宏,约130行代码。LW_OOPC是一种C语言编程框架,用于支持在C语言中进行面向对象编程。

LW_OOPC宏介绍

下面,先通过一个简单的示例来展示LW_OOPC这套宏的使用方法。我们要创建这样一些对象:动物(Animal),鱼(Fish),狗(Dog),车子(Car)。显然,鱼和狗都属于动物,都会动,车子也会动,但是车子不是动物。会动是这些对象的共同特征,但是,显然它们不属于一个家族。因此,我们首先考虑抽象出一个接口(IMoveable),以描述会动这一行为特征:

INTERFACE(IMoveable)
{
    void (*move)(IMoveable* t);     // Move行为
};
  • INTERFACE宏用于定义接口,其成员(方法)均是函数指针类型。

然后,我们分析Animal,它应该是抽象类还是接口呢?动物都会吃,都需要呼吸,如果仅仅考虑这两个特征,显然可以把Animal定为接口。不过,这里,为了展示抽象类在LW_OOPC中如何应用。我们让Animal拥有昵称和年龄属性,并且,让动物和我们打招呼(sayHello方法),但,我们不允许用户直接创建Animal对象,所以,这里把Animal定为抽象类:

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 (*eat)(Animal* t);                         // 动物都会吃(抽象方法,由子类实现)
    void (*breathe)(Animal* t);                     // 动物都会呼吸(抽象方法,由子类实现)
    void (*init)(Animal* t, const char* name, int age); // 初始化昵称和年龄
};
  • ABS_CLASS宏用于定义抽象类,允许有成员属性。代码的含义参见代码注释。
    紧接着,我们来定义Fish和Dog类,它们都继承动物,然后还实现了IMoveable接口:

      CLASS(Fish)
      {
          EXTENDS(Animal);        // 继承Animal抽象类
          IMPLEMENTS(IMoveable);  // 实现IMoveable接口
      
          void (*init)(Fish* t, const char* name, int age);
      };
      
      CLASS(Dog)
      {
          EXTENDS(Animal);        // 继承Animal抽象类
          IMPLEMENTS(IMoveable);  // 实现IMoveable接口
      
      
          void(*init)(Dog* t, const char* name, int age);
      };
    

为了让Fish对象或Dog对象在创建之后,能够很方便地初始化昵称和年龄,Fish和Dog类均提供了init方法。
下面,我们来定义Car,车子不是动物,但可以Move,因此,让Car实现IMoveable 接口即可:

CLASS(Car)
{
  IMPLEMENTS(IMoveable);  // 实现IMoveable接口(车子不是动物,但可以Move)
};

接口,抽象类,具体类的定义都已经完成了。下面,我们开始实现它们。接口是不需要实现的,所以IMoveable没有对应的实现代码。Animal是抽象动物接口,是半成品,所以需要提供半成品的实现:

/* 设置动物的昵称*/
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
  • ABS_CTOR表示抽象类的定义开始,ABS_CTOR(Animal)的含义是Animal抽象类的“构造函数”开始。在C语言里边其实是没有C++中的构造函数的概念的。LW_OOPC中的CTOR系列宏(CTOR/END_CTOR,ABS_CTOR/END_ABS_CTOR)除了给对象(在C语言中是struct实例)分配内存,然后,紧接着要为结构体中的函数指针成员赋值,这一过程,也可以称为函数绑定(有点类似C++中的动态联编)。
  • 函数绑定的过程由FUNCTION_SETTING宏来完成。

对于Fish和Dog类的实现,与Animal基本上是类似的,除了将ABS_CTOR换成了CTOR,直接参见代码:

/* 鱼的吃行为 */
void Fish_eat(Animal* t)
{
    printf("鱼吃水草!\n");
}
/* 鱼的呼吸行为 */
void Fish_breathe(Animal* t)
{
    printf("鱼用鳃呼吸!\n");
}
/* 鱼的移动行为 */
void Fish_move(IMoveable* t)
{
    printf("鱼在水里游!\n");
}
/* 初始化鱼的昵称和年龄 */
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(Animal.eat, Fish_eat);
FUNCTION_SETTING(Animal.breathe, Fish_breathe);
FUNCTION_SETTING(IMoveable.move, Fish_move);
FUNCTION_SETTING(init, Fish_init);
END_CTOR

上面是Fish的实现,下面看Dog的实现:

/* 狗的吃行为 */
void Dog_eat(Animal* t)
{
    printf("狗吃骨头!\n");
}
/* 狗的呼吸行为 */
void Dog_breathe(Animal* t)
{
    printf("狗用肺呼吸!\n");
}
/* 狗的移动行为 */
void Dog_move(IMoveable* t)
{
    printf("狗在地上跑!\n");
}
/* 初始化狗的昵称和年龄 */
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(Animal.eat, Dog_eat);
FUNCTION_SETTING(Animal.breathe, Dog_breathe);
FUNCTION_SETTING(IMoveable.move, Dog_move);
FUNCTION_SETTING(init, Dog_init);
END_CTOR
  • SUPER_CTOR这个宏是提供给子类用的,用于调用其直接父类的构造函数(类似Java语言中的super()调用,在这里,其实质是要先调用父类的函数绑定过程,再调用自身的函数绑定过程),类似Java那样,SUPER_CTOR如果要出现,需要是ABS_CTOR或者CTOR下面紧跟的第一条语句。

最后,我们把Car类也实现了:

void Car_move(IMoveable* t)
{
    printf("汽车在开动!\n");
}

CTOR(Car)
FUNCTION_SETTING(IMoveable.move, Car_move);
END_CTOR

下面,我们实现main方法,以展示LW_OOPC的威力:

#include "animal.h"

int main()
{
    Fish* fish = Fish_new();    // 创建鱼对象
    Dog* dog = Dog_new();       // 创建狗对象
    Car* car = Car_new();       // 创建车子对象

    Animal* animals[2] = { 0 };     // 初始化动物容器(这里是Animal指针数组)
    IMoveable* moveObjs[3] = { 0 }; // 初始化可移动物体容器(这里是IMoveable指针数组)

    int i = 0;                  // i和j是循环变量
    int j = 0;

    // 初始化鱼对象的昵称为:小鲤鱼,年龄为:1岁
    fish->init(fish, "小鲤鱼", 1);          

    // 将fish指针转型为Animal类型指针,并赋值给animals数组的第一个成员
    animals[0] = SUPER_PTR(fish, Animal);   

    // 初始化狗对象的昵称为:牧羊犬,年龄为:2岁
    dog->init(dog, "牧羊犬", 2);            

    // 将dog指针转型为Animal类型指针,并赋值给animals数组的第二个成员
    animals[1] = SUPER_PTR(dog, Animal);    

    // 将fish指针转型为IMoveable接口类型指针,并赋值给moveOjbs数组的第一个成员
    moveObjs[0] = SUPER_PTR(fish, IMoveable);

    // 将dog指针转型为IMoveable接口类型指针,并赋值给moveOjbs数组的第二个成员
    moveObjs[1] = SUPER_PTR(dog, IMoveable);    

    // 将car指针转型为IMoveable接口类型指针,并赋值给moveOjbs数组的第三个成员
    moveObjs[2] = SUPER_PTR(car, IMoveable);    

    // 循环打印动物容器内的动物信息
    for(i=0; i<2; i++)
    {
        Animal* animal = animals[i];
        animal->eat(animal);
        animal->breathe(animal);
        animal->sayHello(animal);
    }

    // 循环打印可移动物体容器内的可移动物体移动方式的信息
    for(j=0; j<3; j++)
    {
        IMoveable* moveObj = moveObjs[j];
        moveObj->move(moveObj);
    }

    lw_oopc_delete(fish);
    lw_oopc_delete(dog);
    lw_oopc_delete(car);

    return 0;
}

从上边的代码中,我们惊喜地发现,在C语言中,借助LW_OOPC,我们实现了将不同的动物(Fish和Dog对象)装入Animal容器,然后可以用完全相同的方式调用Animal的方法(比如eat和breathe方法),而实际调用的是具体的实现类(Fish和Dog)的对应方法。这正是面向对象中的多态的概念。同样,我们可以将Fish对象,Dog对象,以及Car对象均视为可移动物体,均装入IMoveable容器,然后用完全相同的方式调用IMoveable接口的move方法。看到了吗?借助LW_OOPC,在C语言下我们竟然可以轻松地实现面向对象和面向接口编程!

LW_OOPC最佳实践

说得简单一点,要想使用好LW_OOPC这套宏,还得首先懂面向对象,要遵循面向对象设计的那些大原则,比如开闭原则等。在C语言中使用面向对象,根据实际使用的情况,给出如下建议:
1)继承层次不宜过深,建议最多三层(接口、抽象类、具体类,参见图 1和图 2)
继承层次过深,在Java/C#/C++中均不推崇,在C语言中实践面向对象的时候,尤其要遵循这一点,只有这样,代码才能简单清爽。
2)尽量避免多重继承
尽可能使用单线继承,但可实现多个接口(与Java中的单根继承类似)。
3)尽量避免具体类继承具体类
具体类继承具体类,不符合抽象的原则,要尽量避免。
4)各继承层次分别维护好自己的数据
子类尽量不要直接访问祖先类的数据,如果确实需要访问,应当通过祖先类提供的函数,以函数调用的方式间接访问。

LW_OOPC的优点

1) 轻量级
2)广泛的适应性,能够适应各种平台,各种编译器(能支持C的地方,基本上都能支持)
3)帮助懂OO的Java/C++程序员写出面向对象的C程序。
4)使用C,也能引入OO的设计思想和方法,在团队的C/C++分歧严重时可能非常有用。

LW_OOPC的缺点

1) 无法支持重载(C语言不支持所致)
2)不完全的封装(无法区分私有、保护和公有)
LW_OOPC的INTERFACE/ABS_CLASS/CLASS三个宏展开后都是C语言的struct,其成员全是公有的,宏本身并无能力提供良好地封装层次的支持,所以,只能从编程规范和编程风格上进行引导。
3)不支持RTTI
既然不支持RTTI,那么显然也无法支持安全的向下转型(C++中的dynamic_cast的转型功能)
4)不支持拷贝构造以及赋值语义
5)转换成接口的表述有点麻烦,表达形式相比C++要啰嗦很多。
6)有学习成本,需要用户学习并习惯这套宏
前四条缺点,实质上并非是LW_OOPC的缺点,而是C相对C++而言的缺点,在这里,之所以也一并列上,是希望用户不要对LW_OOPC抱太高的期望,毕竟它也只是一套C语言的宏而已,C语言有的缺点,LW_OOPC并不能够解决。

更多推荐

LW_OOPC学习01