摘要:C语言原本是面象过程编程的语言,C++在C的基础上增加了面向对象的特性包括 封装(class)、继承和多态,面向对象最大的优点就是使日后的维护变得容易,主要体现在软件架构设计和代码的解耦上,如果你按照“高内聚低耦合”的原则设计了一个好的系统,虽然代码量不一定减
C语言原本是面象过程编程的语言,C++在C的基础上增加了面向对象的特性包括 封装(class)、继承和多态,面向对象最大的优点就是使日后的维护变得容易,主要体现在软件架构设计和代码的解耦上,如果你按照“高内聚低耦合”的原则设计了一个好的系统,虽然代码量不一定减少,甚至还会增加,但是后期的维护成本会大大降低。在实际项目中,维护成本恰恰又是在软件的整个生命周期中所占比例最大的一部分,所以将面向对象的概念引入到C语言编程过程中具有重要的现实意义。
当然,你可以会说要想使用C语言进行面向对象的编程可以使用ObjC(Objective C),但支持它的编译器太少,特虽是在嵌入式领域,基本没有编译器支持ObjC。下面我们讨论一下使用ANSI标准的C语言进行面向对像的编译。
一、面向对象的概念在C语言中的实现的可能性讨论
封装、继承和多态是面向对象编程最基本也是最核心的三个概念,我们分别讨论它们在C语言中实现的可行性。
对于封装,C语言不支持C#、JAVA或C++等面各对像编程语言的类(class),但是在C语言中可以实现struct,这个与类非常接近,而在C++中,struct与class实际上并没有本质区别,只是struct的成员的访问权限默认为public,而class成员的默认访问权限为PRIVATE。封装的产物就是类,其实例便是对象,而对象应该是属性和行为(方法)的集合,C语言能否支持这两个要素呢?
首先,对于属性,struct内部可以包含任何类型的数据成员作为对象的属性;可是struct内部不能有函数成员,这对于使对象包含期行为集好像有点儿困难。但是,别忘了,C语言有函数指针,函数指针可以指向任何一个函数,我们如果在struct内定义一系列的函数指针,然后将它们指向相应的函数不就可以了?但是对于数据和方法的隐藏,C语言真是无能为力了,它不能使用private关键字控制客户代码对struct成员的访问,这一点我们可以通过事先的约定来人为地实现该功能,后面的章节中我们将讨论这个问题。
然后,对于继承。C语言不支持继承,不能实现面向对象中的所谓"is-a"关系,但C语言的struct完全可以实现组合(composition),即所谓"has-a"关系。这样,我样可以先定义一个“父”struct,然后再定义一个子struct,在子struct中再添加子struct特有的属性或方法指针,这样基本可以模拟继承的概念。
最后,就是多态。多态包括两个方面,一个是静态的多态,另一个是动态的多态。静态的多态就是指函数重载,这个在C语言中是根本不支持的,无法实现,也很难模拟;动态的多态就是指虚函数了,这在C#、Java、C++中都有相应的内置关键字支持,而在C语言中连继承都没有,更不用说虚函数了,但类似的功能我们可以模拟。如何模拟动态的多态呢?我们上面说了,C语言中的struct具有函数指针,函数指针是一个变量当然可以动态更改设置了,我们就可以根据该结构的类型动态地进行装载函数。
二、C语言模拟面向对象的实现
上节讨论了C语言模拟面向对象编程的可行性,已经证实可以模拟面向对象编程的多数据概念,下面我们以一个简单的实例来演示如何实现。
(一)类的定义和继承的模拟
我们以很多书中都用的一个Shape、circle和Rectangle的例子,例如我们已经通过UML建模,三个类组成的类图如下:
C语言实现代码如下:
// Code List(1)ABSTRACT struct Shape{ PUBLIC ABSTRACT double (*getarea)(void* const shape); PUBLIC ABSTRACT double (*print)(void* const shape);};struct Circle EXTERNS(Shape){ PUBLIC struct Shape shape; PRIVATE double Radius; PUBLIC double (*getRadius)(struct Circle* const cicle);};struct Rectangle EXTERNS(Shape){ struct Shape shape; PRIVATE double Length; PRIVATE double Width; PUBLIC double (*getLength)(struct Rectangle* const rect); PUBLIC double (*getWidth)(struct Rectangle* const rect);};这里的PRIVATE,PUBLIC的定义如下:
#define PUBLIC #define PRIVATE #define ABSTRACT #define EXTERNS(super)
仅仅向程序员声明这些成员的声明方式,因为C语言本身不支持,这里是我们的约定,假设程序员看到这些标识符时将按照它们的语义使用这些函数。从上面的代码我们可以清楚地明白类的各个类的关系,以及方法及属性的使用方法。这里我们模拟了Java的方式,还使用了Java的关键字EXTERNS,当然这里我们用大写表示是用的宏定义,为了看着更清楚我们可以把这些“关键字”写成小写,更像是面向对象的程序设计语言,实际使用中可以使用小写,这里为了显示更醒目还是使用了大写。
(二)创建对象
上节我们定义了三个“类”,这三个类的对象如何创建呢?按照C语言的编程方式,当然可以定义相应的变量,用于操作数据,但是现在要模拟的是面向对象的编程,这们应该使编程方式更像面向对象的编程。显然,不可能使用new关键字创建对象,也没有构造函数可以使用。注意到C语言是区分大小写的,可以调用与类名字相同但全部小写的函数名作为模拟构造函数,再定义一个NEW关键字
#define NEWRectangle* rec = NEW rectangle(12,25);但如果我们要创建几个带有不同参数的构造函数,这种方法就不好用了,因为C语言不允许我们使用函数重戴。但另一种方法可以躲避这个问题,那就是使用类工厂模式(Factory Patern),通过该模式的使用躲避构造函数的问题,比如想使用这样的方式创建对象。
ShapeFactory.createRectangle(12,25);ShapeFactory.createCircle(7.12);ShapeFactory.createSquareRectangle(11.3);这样看起来就更像是面向对象编程了。但Rectangle* rec的定义还是不像是面向对象编程,我们这样处理一下,定义一个IRectangle,好像是一个接口,定义如下:
typedef struct Rectangle* IRectangle;这样以后再定义Rectangle的指针,就可以直接使用
IRectangle rec;掩饰掉了C语言的指针,客户代码看起来更简洁。下面就定义createCircle方法
/* circle **/static void installCircleMethods(ICircle c);static ICircle createCircle(double radius){ ICircle c = (ICircle)malloc(sizeof(struct Circle)); c->Radius = radius; installCircleMethods(c); return c;}static double getCircleArea(ICircle c){ return 3.14 * c->Radius * c->Radius;}static void printCircle(ICircle c){ printf("Circle:radius = %g/n", c->Radius);}static double getCircleRadius(ICircle c){ return c->Radius;}static void installCircleMethods(ICircle c){ ((IShape)c)->getArea = getCircleArea; ((IShape)c)->print = printCircle; c->getRadius = getCircleRadius;}在这里可以看到对象的方法是通过函数installCircleMethods在手动添加上的,这样,客户端代码就可以通过createCircle函数创建ICircle对象了。
c = ShapeFactory.createCircle(3.5); printf("area of c=%g/n", c->shape.getArea(c)); ((IShape)c)->print(c); printf("Radius of c=%g/n",c->getRadius(c));(三)多态
实际上,第一段代码中,我们已经使用了“虚函数”的模拟,也就是实现了动态的多态,而对于静态的多态--函数名重载,在C语言中着实没法实现,因为在C语言中不支持两个函数的函数名相同。
实现了Code List (1)中定义的结构类型,我们就可以全用下面的代码进行测试:
// Code List(7)int i;IShape shapes[4];ICircle c;shapes[0] = ShapeFactory.createCircle(3.5);shapes[1] = ShapeFactory.creatRectangle(3.2, 1.7);shapes[2] = ShapeFactory.creatRectangle(7,5);shapes[3] = ShapeFactory.createCircle(8.2);for(i=0; iprint(shapes[i]);}IShape是使用指针定义的接口,但这里必须注意,我们要使上面的代码Code List (7)能够正常运行,一个很重要的前提就是“派生类(子类)”Rectangle 和Circle中Shape 变量的位置必须是在第一位,如果再从这两个结构派生新的数据库型,也要将这两种类型的变量放在“子类”的最开始,否则Code List(7)将不能得到期望的结果,依次类推。
三、结论
使用C语言模拟了面向对象程序设计语言中的三个重要概念封装、继承和多态,而且使用指针模拟了面向对象程序设计语言中的“接口”的概念,并定义了相应的宏作为面向对象扩展的关键字,其本把C“改造”成为一种面向对象的程序设计语言。
而实际上,程序设计语言的语法就是一种规则,C和JAVA的面向对象的支持是编译器强制执行的一套语言,而本文定义的一些规则是没有编译器强制执行,而是靠程序员遵守的一套规则,只要遵守这些约定,建立在此套约定之上的C语言但基本成为一种面向对象的话言。
实际上C++的内部实现也是通过默认增加了一个对象指针、虚函数表来实现类对象、多态的,相当于是语法糖,面向对象编程是一种理念与思维模式,实现了以后可以将代码进行分组,编程逻辑与自然后思维逻辑更像,将系统复杂了以后更容易维护而已。
面向过程编程主要是以流程图作为主要的表达方式,而面向对象编程主要是以类图、时序图、通讯图、包图等来表达,然后组成不同视图从不同侧面来表达软件架构,UML就是一个很好的表达面向对象设计的工具。
而Java更进一步,增加了接口的特性,表达能力更加丰富:
C#作为后来者,又进一步对字段进行封装,增加了属性概念,至此,从语言表达方面,面向对象达到了成熟阶段。
来源:物联一尘