在C语言里进行面向对象设计--模拟多态

来源:互联网 发布:lol mac版美服 编辑:程序博客网 时间:2024/05/21 15:04

 

 

在设计领域有面向过程设计和面向对象设计两种设计方法。他们之间的区别这里就不在罗嗦了。

C语言是一种面向过程的编程语言,说它是面向过程的编程语言是因为它不支持类、继承等面向对象领域内的一些概念。但是不是说C语言就不能进行面向对象设计。本文就是想总结一下使用C语言进行面向对象设计的方法。至于方法是否正确,还需要大家指教。

简单概念解释。面向对象的3大特征:封装、继承和多态。

 

1.   封装

隐藏数据。因为如果数据直接开放给用户的话,由于在C/C++里对数据的访问等价于直接操作内存,类无法在用户访问数据时做任何操作或限制,因此引入封装的概念。在C++里,封装的表现形式是privateprotected的成员变量。这2种只是将变量的访问权限开放给不同的类用户,对于封装来说区别是不大的。

封装在C里面是很好模拟的,稍后我在多态的模拟里一齐说出。

 

2.   继承

C++里的继承有2种:继承实现和继承接口。

继承实现指的是子类继承父类的功能。例如,在MFC里,自己写一个类从CDialog派生,不用添加什么代码,就可以使用自己的类创建对话框。这种继承的目的是代码重用。另外一种情况是,有一系列类是一个相似的功能集,它们的主要特征是数据的承载体,也就是我们通常要建立的模型。这一类也是继承实现。因为这种情况下的继承通常都是修改父类的局部操作,并非所有。

接口指的是两个模块间函数调用的契约,实际上就是一组函数标签的集合(规定好了函数的返回值,函数名和参数)。这种情况下继承的目的是在满足接口函数的条件下,实现不同的功能。和继承实现相比,它的目的不是简单的在父类基础上增加或修改一些功能。

C++的这两种继承在我们设计时增加了很大的设计难度,所以Java/C#里就将接口提出来作为一个独立的概念。这是我的理解。Java/C#的单根继承机制+接口=C++的多根继承。从这个角度来讲,在C++里讨论多根继承好不好是没有什么意义的。可以简单的这样定义:多根继承时,第一父类是继承实现,其它父类是继承接口,这样我想就能清楚很多。

从类使用角度来讲,继承来的东西可以分为2部分:纯粹的继承实现和多态部分。纯粹的继承实现指的就是上面的例子,从CDialog派生,不添加一行代码就可以创建自己的类的对话框。多态部分指的是在运行时可以改变动作的函数。因此,继承实现的第2种情况和继承接口都是通过多态来实现的。

 

3.   多态

简单来说,就是在运行时调用同样名字的函数,不同对象实现的功能不同。多态才是面向对象的精髓。上面两个概念是改变设计角度,多态才是面向对象里面的精华所在。

 

C语言里进行面向对象设计(面向对象模拟)

1.   构造函数。

对象构造时调用的函数。在C++里构造函数是不能声明成多态(virtual)的,也就是说,你必须显式调用某一个构造函数。因此,不要花力气在C语言里模拟这个机制。

2.   this指针

C++里的this是个类的静态指针,切换对象时,编译器自动把this指针给设置好了。C的编译器可没那么好心,所以只有我们自己麻烦一点了,把this指针当作函数的参数传进去就行了。

3.   析构函数

C++里的析构函数在必要的时候可以声明成虚的。所以在C里面这个也是个可选项。

4.   多态

这才重头戏,我要细细讲来。

 

毋庸置疑,多态在C里面是用函数指针来实现的。

由于C语言没有给我们提供那么多的机制,所以我们不要想着在Cstruct里既模拟出虚函数,又模拟出普通函数,那样只会增加复杂度,只要关注虚函数就醒了。

C++对象如果有虚函数存在的话,是有一个虚函数表的,虚函数表保存了所有的虚函数的指针。这个表的内存和C++对象的内存不是存储到一起的,在C++对象里只存储了一个需函数表的指针。由于我们只需要模拟多态,所以我们也没有必要向C++那样管理(内存管理太麻烦)。我通过一个例子来表达我的规则。

例:

    实现一个函数,这个函数有输出字符串的功能,但是字符串输出的目的地可以由用户来指定,例如输出到一个数组里,一个文件里等等。注意输出的目的地是不可列举的,因为将来我还可能要输出到socket里等等。

分析:

    需求中,字符串的组件是函数的内部功能,关键字输出可以看做是该函数和用户的接口。在C++里对应纯虚基类。

 

1.   接口(纯虚基类)的定义

多态都是从基类开始的。所以基类里面要定义好所有的虚函数。基类里可不可以有数据?这要根据需求来确定,不过影响不大。考虑封装,多数情况下还是不要定义数据。基类定义如下:

struct IStream

{

    void (*write)(IStream* pStream, char*pstr);

};

【规则1】基类里的函数指针(虚函数)成员要尽量放到前面。专注于接口设计而不是数据设计。

【规则2】基类里的函数指针的第一个成员是基类的指针类型,它就是this指针。

 

2.   接口(基类)的使用——即在函数里使用这个接口

简单起见,我们的函数就输出Test这个字符串。函数的实现看起来是下面这个样子。

void outputString(IStream*pStream)

{

    pStream->write(pStream, “Test”);

}

 

3.   具体类的实现

正如需求里面所讲,我要实现2个功能:输出到字符数组和输出到文件。这样就需要2个具体类。

StringStream的实现大概是这样:

struct StringStream

{

    IStream base;

  char    buf[8];

};

void string_write(IStream*pStream, char* pStr)

{

    StringStream* pSS = (StringStream*)pStream;

    strcpy(pSS->buf, pStr);

}

 

FileStream的实现大概是这样:

struct FileStream

{

    IStream base;

    FILE*  f;

}

void file_write(IStream* pStream,char* pStr)

{

    FileStream* pFS = (FileStream*)pStream;

    fprintf(pFS->f, pStr);

}

 

看到这里,我想你不禁会有疑问:StringStream*pSS = (StringStream*)pStream;这一句不会有问题么?是的,如果不采取一些手段的话,这一句很容易出现问题。请接着往下看。

 

 

4.   使用

现在演示调用过程。

void main()

{

    //输出到字符串

    StringStream ss;

    ss.base.write=string_write;  //到这里是StringStream对象的构造

    outputString(&ss.base);

 

    //输出到文件

    FileStream fs;

    fs.base.write=file_write;    //到这里是FileStream对象的构造。

    fs.f = fopen(“test.txt”, “w+”);

    outputString(&fs.base);

}

 

实际上多态是不需要显示调用函数的,也就是说,不需要显示的调用string_writefile_write,既然这样,将它们暴漏给用户其实是多余的。3里面的代码中还没有对代码所在的文件进行分配,现在我将它分配一下:

StringStream的定义            StringStream.h

string_write的定义            StringStream.c中,安全起见,将它声明为static

增加StringStreamInit函数     声明放到StringStream.h中,实现放到StringStream.c中,代码如下。

//声明 StringStream.h

void StringStreamInit(StringStream*pSS);

 

//实现 StringStream.c

void StringStreamInit(StringStream*pSS)

{

    pSS->base.write=string_write;

}

 

这样一来,你就可以看到,string_writeStringStream的绑定是在StringStream.c文件中进行的,而且只在一处进行,所以在string_stream函数里将参数pStream进行强制转换是完全没有问题的,因为不按照正规流程走(不调用StringStreamInit函数),你是不知道StringStream里的成员到底该怎么设置,除非你存心搞破坏。

FileStream的分配和StringStream类似。

 

总结一下具体类实现的规则和特征

【规则3】派生时,将基类作为派生类的第一成员。

         这个不是必须的,因为这样做使用起来方便一些,因为在C语言里,结构体的地址就是它第一个成员的地址,所以可以进行上面的强制转换。

         如果不做成第一成员的话,那么使用时就要计算一下内存的偏移量。

         如果是多根继承的话,没办法,必须要计算内存偏移量了。

【规则4】完全隐藏多态用的函数。把它们只放到.c文件中,并且声明称static的。

【规则5】提供一个初始化函数,相当于构造函数,在该函数里将多态函数和具体类的对象绑定。

          你甚至可以将这个函数修改为对象构建函数,例如:

         IStream*  CreateStringStream()

          {

             StringStream* pSS = (StringStream*)malloc(sizeof(StringStream));

             pSS->base.write = string_write;

          }

          如果是这样的话,那么你就可以将StringStream放到StringStream.c文件里,用户甚至都看不到这个数据。

          至此,封装的模拟我也已经说的很清楚了。

【特征1】实现面向对象时,必然要有基类到派生类的强制转换。

         因为使用时是使用基类的指针,所以传给具体类的多态函数时,也只能传递基类的指针。只有派生类才知道基类指针的实际类型,所以必须要进行强制转换。也有一种情况,那就是该接口不需要存储任何数据,那么直接用基类的结构体好了。

【特征2】使用时,基类中的函数必然要出现多态。

         如果不出现多态,那就是你设计过度了。举个例子,如果你想下面这样做,就是不出现多态。

        StringStream ss;

        StringStreamInit(&ss);

        ss.base.write(&ss, “Test”);

         这个例子中,调用StringStreamInit()函数后,ss.base.write肯定是string_write,所以不可能出现多态。如果你的全篇代码都是这样用的,那就是多余了。费牛劲做一件多余的事情,你不郁闷么?

 

虽然C没有提供面向对象机制,但是我想在C里面更有助于进行面向对象设计。因为它没有提供那个机制,所以在设计的时候就必须要专注需要设计的部分,否则很麻烦。其实反过来瞧瞧,在C语言里进行面向对象设计也不是想象中的那么复杂。

 

以上是我对在C语言里进行面向对象设计的一些总结,希望能对大家有所帮助。如果你有过这方面的经验,可以将上面这些规则和特征对照一下。如果有错误,欢迎指出。

 

 

原创粉丝点击