C语言中的类模拟和多态,继承

转载 2012年03月29日 18:38:16

C语言中的类模拟(C++编程思想)

       在面向对象的语言里面,出现了类的概念。这是编程思想的一种进化。所谓类:是对特定数据的特定操作的集合体。所以说类包含了两个范畴:数据和操作。而C语言中的struct仅仅是数据的集合。(liyuming1978@163.com)

 

1.实例:下面先从一个小例子看起

 

#ifndef C_Class

       #define C_Class struct

#endif

 

C_Class A {

       C_Class A *A_this;

       void (*Foo)(C_Class A *A_this);

       int a;

       int b;

};

 

C_Class B{               //B继承了A

       C_Class B *B_this;  //顺序很重要

       void (*Foo)(C_Class B *Bthis);              //虚函数

       int a;

       int b;

 

       int c;

};

  

void B_F2(C_Class B *Bthis)

{

       printf("It is B_Fun/n");

}

 

void A_Foo(C_Class A *Athis)

{

       printf("It is A.a=%d/n",Athis->a);//或者这里

//     exit(1);

//     printf("纯虚 不允许执行/n");//或者这里

}

 

void B_Foo(C_Class B *Bthis)

{

       printf("It is B.c=%d/n",Bthis->c);

}

 

void A_Creat(struct A* p)

{

       p->Foo=A_Foo;

       p->a=1;

       p->b=2;

       p->A_this=p;

}

  

void B_Creat(struct B* p)

{

       p->Foo=B_Foo;

       p->a=11;

       p->b=12;     

       p->c=13;

       p->B_this=p;

}

  

int main(int argc, char* argv[])

{

       C_Class A *ma,a;

       C_Class B *mb,b;

 

       A_Creat(&a);//实例化

       B_Creat(&b);

 

       mb=&b;

       ma=&a;

 

       ma=(C_Class A*)mb;//引入多态指针

       printf("%d/n",ma->a);//可惜的就是 函数变量没有private

       ma->Foo(ma);//多态

       a.Foo(&a);//不是多态了

       B_F2(&b);//成员函数,因为效率问题不使用函数指针

       return 0;

}

输出结果:

11

It is B.c=13

It is A.a=1

It is B_Fun

 

2.类模拟解说:

       我在网上看见过一篇文章讲述了类似的思想(据说C++编程思想上有更加详细的解说,可惜我没空看这个了,如果有知道的人说一说吧)。但是就象C++之父说的:“C++C是两种语言。所以不要被他们在语法上的类似就混淆使用,那样有可能会导致一些不可预料的事情发生。

       其实我很同意这样的观点,本文的目的也不是想用C模拟C++,用一个语言去模拟另外一个语言是完全没有意义的。我的目的是想解决C语言中,整体框架结构过于分散、以及数据和函数脱节的问题。

       C语言的一大问题是结构松散,虽然现在好的大型程序都基本上按照一个功能一个文件的设计方式,但是无法做到更小的颗粒化――原因就在于它的数据和函数的脱节。类和普通的函数集合的最大区别就在于这里。类可以实例化,这样相同的函数就可以对应不同的实例化类的变量。

       自然语言的一个特点是概括:比如说表。可以说手表,钟表,秒表等等,这样的描述用面向对象的语言可以说是抽象(继承和多态)。但是我们更要注意到,即使对应于手表这个种类,还是有表链的长度,表盘的颜色等等细节属性,这样细微的属性如果还用抽象,就无法避免类膨胀的问题。所以说类用成员变量来描述这样的属性。这样实例并初始化不同的类,就描述了不同属性的对象。

       但是在C语言中,这样做是不可能的(至少语言本身不提供这样的功能)。C语言中,如果各个函数要共享一个变量,必须使用全局变量(一个文件内)。但是全局变量不能再次实例化了。所以通常的办法是定义一个数组。以往C语言在处理这样的问题的时候通常的办法就是这样,比如说socket的号,handel等等其实都是数组的下标。(不同的连接对应不同的号,不同的窗口对应不同的handel,其实这和不同的类有不同的成员变量是一个意思)

       个人认为:两种形式(数组和模拟类)并无本质的区别(如果不考虑虚函数的应用的话),它们的唯一区别是:数组的办法将空间申请放在了模块内,而类模拟的办法将空间申请留给了外部,可以说就这一点上,类模拟更加灵活。

 

3.其他的话:

       我的上述思想还是很不成熟的,我的目的是想让C语言编程者能够享受面向对象编程的更多乐趣。我们仅仅面对的是浩瀚的黑箱,我们的工作是堆砌代码,而且如果要更改代码功能的时候,仅仅换一个黑箱就可以了。

       而更大的目的是促使这样的黑箱的产生。或许有一天,一种效率很好,结构很好的语言将会出现。那个时候编程是不是就会象说话一样容易了呢?

C言的多态实现

       相信很多人都看过设计模式方面的书,大家有什么体会呢?Bridge,Proxy,Factory这些设计模式都是基于抽象类的。使用抽象对象是这里的一个核心。

      

       其实我觉得框架化编程的一个核心问题是抽象,用抽象的对象构建程序的主体框架,这是面向对象编程的普遍思想。用抽象构建骨架,再加上多态就形成了一个完整的程序。由于C++语言本身实现了继承和多态,使用这样的编程理念(理念啥意思?跟个风,嘿嘿)在C++中是十分普遍的现象,可以说Virtual(多态)是VC的灵魂。

 

       但是,使用C语言的我们都快把这个多态忘光光了。我常听见前辈说,类?多态?我们用的是C,把这些忘了吧。很不幸的是,我是一个固执的人。这么好的东西,为啥不用呢。很高兴的,在最近的一些纯C代码中,我看见了C中的多态!下面且听我慢慢道来。

 

1.         VC中的Interface是什么

       Interface:中文解释是接口,其实它表示的是一个纯虚类。不过我所要说的是,在VC中的Interface其实就是struct,查找Interface的定义,你可以发现有这样的宏定义:

       #Ifndef Interface

              #define Interface struct

       #endif

而且,实际上在VC中,如果一个类有Virtual的函数,则类里面会有vtable,它实际上是一个虚函数列表。实际上C++是从C发展而来的,它不过是在语言级别上支持了很多新功能,在C语言中,我们也可以使用这样的功能,前提是我们不得不自己实现。

 

2.C中如何实现纯虚类(我称它为纯虚结构)

       比较前面,相信大家已经豁然开朗了。使用struct组合函数指针就可以实现纯虚类。

例子: typedef struct {

                     void  (*Foo1)();

                     char  (*Foo2)();

                     char*  (*Foo3)(char* st);

              }MyVirtualInterface;

      

       这样假设我们在主体框架中要使用桥模式。(我们的主类是DoMyAct,接口具体实现类是Act1,Act2)下面我将依次介绍这些“类”。(C中的“类”在前面有说明,这里换了一个,是使用早期的数组的办法)

 

主类DoMyAct: 主类中含有MyVirtualInterface* m_pInterface; 主类有下函数:

                            DoMyAct_SetInterface(MyVirtualInterface* pInterface)

                            {

                                   m_pInterface= pInterface;

                            }

                            DoMyAct_Do()

                            {

                                   if(m_pInterface==NULL) return;

                                   m_pInterface->Foo1();

                                   c=m_pInterface->Foo2();

                            }

子类Act1:实现虚结构,含有MyVirtualInterface  st[MAX]; 有以下函数:

                    MyVirtualInterface* Act1_CreatInterface()

                     {

                            index=FindValid() //对象池或者使用Malloc !应该留在外面申请,实例化

                            if(index==-1) return NULL;

                            St[index].Foo1=Act1_Foo1; // Act1_Foo1要在下面具体实现

                            St[index].Foo2=Act1_Foo2;

                            St[index].Foo3=Act1_Foo3;

                            Return &st [index];

                     }

子类Act2同上。

 

在main中,假设有一个对象List。List中存贮的是MyVirtualInterface指针,则有:

       if( (p= Act1_CreatInterface()) != NULL)

       List_AddObject(&List, p); //Add All

 

       While(p=List_GetObject()){

              DoMyAct_SetInterface(p);//使用Interface代替了原来大篇幅的Switch Case

              DoMyAct_Do();//不要理会具体的什么样的动作,just do it

       }

 

       FREE ALL。

       在微系统里面,比如嵌入式,通常使用对象池的技术,这个时候可以不用考虑释放的问题(对象池预先没有空间,使用Attach,在某个函数中申请一个数组并临时为对象池分配空间,这样函数结束,对象池就释放了)

 

       但是在Pc环境下,由于程序规模比较大,更重要的是一些特殊的要求,使得对象的生命周期必须延续到申请的那个函数体以外,就不得不使用malloc,实际上即使在C++中,new对象的自动释放始终是一个令人头疼的问题,新的标准引入了智能指针。但是就我个人而言,我觉得将内存释放的问题完全的交给机器是不可信任的,它只能达到准最佳。

 

       你知道设计Java的垃圾回收算法有多困难吗?现实世界是错综复杂的,在没有先验条件下,要想得到精确的结果及其困难。所以我说程序员要时刻将free记在心上,有关程序的健壮性和自我防御将在另外一篇文章中讲述。

 

3.纯虚结构的退化

       下面我们来看看如果struct里面仅仅有一个函数是什么? 这个时候如果我们不使用struct,仅仅使用函数指针又是什么?我们发现,这样就退化为普通的函数指针的使用了。

 

       所以说,有的时候我觉得面向对象仅仅是一种形式,而不是一种技术。是一种观点,而不是一种算法。但是,正如炭,石墨和钻石的关系一样,虽然分子式都是C,但是组成方法不一样,表现就完全不一样了!

       有的时候,我们经常被编程中琐碎的事情所烦恼,而偏离了重心,其实程序可进化的特性是很重要的。有可能,第一次是不成功的,但是只要可进化,就可以发展。

 

4.进阶――类结构树,父类不是纯虚类的类

       前面仅仅讲的是父类是纯虚结构的情况 (面向对象建议的是所有类的基类都是从纯虚类开始的), 那么当类层次比较多的情况下,出现父类不是纯虚结构怎么办呢。嘿嘿,其实在C中的实现比C++要简单多了。因为C中各个函数是分散的。

 

在这里使用宏定义是一个很好的办法:比如两个类Act1,ActByOther1“继承”Act1:

                    MyVirtualInterface* ActByOther1_CreatInterface()

                     {

                            index=FindValid() //对象池或者使用Malloc

                            if(index==-1) return NULL;

                            St[index].Foo1= ActByOther1_Foo1; // Act1_Foo1要在下面具体实现

                            St[index].Foo2= ActByOther1_Foo2;

                            St[index].Foo3= ActByOther1_Foo3;

                            Return &st [index];

                     }

 

       #define ActByOther1_Foo1 Act1_Foo1  //这就是继承 嘿嘿

       ActByOther1_Foo2(){}                    //  可以修改其实现

    ActByOther1_DoByOther() {}         //当然就可以添加新的实现咯

 

5.实例――可以参见H264的源码,其中NalTool就是这样的一个纯虚结构。

 

的性能分析

       类模拟中使用了大量的函数指针,结构体等等,有必须对此进行性能分析,以便观察这样的结构对程序的整体性能有什么程度的影响。

 

1.函数调用的开销

#define COUNTER XX

void testfunc()

{

       int i,k=0;

       for(i=0;i<yy;i++){k++;}

}

 

       在测试程序里面,我们使用的是一个测试函数,函数体内部可以通过改变YY的值来改变函数的耗时。测试对比是 循环调用XX次函数,和循环XX次函数内部的YY循环。

       结果发现,在YY足够小,X足够大的情况下,函数调用耗时成为了主要原因。所以当一个“简单”功能需要“反复”调用的时候,将它编写为函数将会对性能有影响。这个时候可以使用宏,或者inline关键字。

       但是,实际上我设置XX=10000000(1千万)的时候,才出现ms级别的耗时,对于非实时操作(UI等等),即使是很慢的cpu(嵌入式10M级别的),也只会在XX=10万的时候出现短暂的函数调用耗时,所以实际上这个是可以忽略的。

 

2.普通函数调用和函数指针调用的开销

void (*tf)();

tf=testfunc;

 

       测试程序修改为一个使用函数调用,一个使用函数指针调用。测试发现对时间基本没有什么影响。(在第一次编写的时候,发现在函数调用出现耗时的情况下(XX=1亿),函数指针的调用要慢(release版本),调用耗时350:500。后来才发现这个影响是由于将变量申请为全局的原因,全局变量的访问要比局部变量慢很多)。

 

3.函数指针和指针结构访问的开销

struct a {

       void (*tf)();

};

        测试程序修改为使用结构的函数指针,测试发现对时间基本没有什么影响。其实使用结构并不会产生影响,因为结构的访问是固定偏移量的。所以结构变量的访问和普通变量的访问对于机器码来说是一样的。

 测试结论:使用类模拟的办法对性能不会产生太大的影响。

 

 

C语言中的面向对象思想

经常听见别人说面向对象的程序设计,以前在学校上课的时候,也有开面向对象程序设计这门课。可是不幸的是,这些都是以C++,甚至VC++为基础的。而更加不幸的是,多年以来我一直是一个C的使用者。在学校的时候,我主要做的是硬件上的驱动层,和底层功能层。在工作以后,又做的是手机上的软件开发,所有这些都是和C离不开的。虽然我不得不说,C++是一门很好的语言,但是它的编译速度,代码效率,编译后的代码大小都限制了它在嵌入式上的应用。(但现在的嵌入式CPU越来越快,内存容量变大。我觉得用C++也应该没有什么问题。这使我觉得似乎是嵌入式编译器的限制。虽然菲利普和TI好像都有C++的编译器,但是似乎没人用这个。难道是太贵了?但不管怎么说,嵌入式应用中,C语言的普遍使用是肯定的)

那么在面向过程的时代产生的C语言能否使用面向对象的思想呢?我认为是肯定可以的,C++不过是在语言级别上加入了对对象的支持,同时提供了丰富的对象库。而在C语言下,我们只好自力更生了。

一、面向对象思想的目的是框架化,手段是抽象

相信很多人都明白面向对象讲了什么:类,抽象类,继承,多态。但是是什么原因促使这些概念的产生呢?

打个比方说:你去买显示器,然而显示器的品牌样式是多种多样的,你在买的过程中发生的事情也是不可预测的。对于这样的事情,我们在程序语言中如何去描述呢。面向对象的思想就是为了解决这样的问题。编写一个程序(甚至说是一个工程),从无到用是困难的,从有到丰富是更加困难的。面向对象将程序的各个行为化为对象,而又用抽象的办法将这些对象归类(抽象),从而将错综复杂的事情简化为几个主要的有机组合(框架化)。

其实我们的身边很多东西都是这样组成的:比如说电脑:电脑是由主板,CPU加上各种卡组成的。这就是一个框架化。而忽略不同的CPU,不同的主板,不同的声卡,网卡,显卡的区别,这就是抽象。再比如说现在的教育网:是由主核心节点:清华,北大,北邮等几个,然后是各个子节点,依次组成了整个教育网网络。

所以我觉得面向对象的编程思想就是:一个大型工程是分层次结构的,每层又由抽象的结构连接为整体(框架化),各个抽象结构之间是彼此独立的,可以独立进化(继承,多态)。层次之间,结构之间各有统一的通讯方式(通常是消息,事件机制)。

二、以前C语言编程中常用的“面向对象”方法

其实C语言诞生以来,人们就想了很多办法来体现“面向对象”的思想。下面就来说说我所知道的方法。先说一些大家熟悉的东东,慢慢再讲诡异的。呵呵

 

1.  宏定义:
   有的人不禁要问,宏定义怎么扯到这里来了,我们可以先看一个简单的例子:
define MacroFunction  Afunction
   然后在程序里面你调用了大量的AFunction,但是有一天,你突然发现你要用BFunction了,(不过AFunction又不能不要,很有可能你以后还要调用),这个时候,你就可以#define MacroFunction  Bfunction来达到这样的目的。

   当然,不得不说这样的办法是too simple,sometime na?ve的,因为一个很滑稽的问题是如果我一般要改为BFunction,一半不变怎么办? 那就只好查找替换了。

 

2.  静态的入口函数,保证函数名相同,利用标志位调用子函数:
   这样的典型应用很多,比如说网卡驱动里面有一个入口函数Nilan(int FunctionCode,Para*)。具体的参数是什么记不清楚了。不过NiLan的主体是这样的:
Long Nilan(int FunctionCode,Para*){

Switch(FunctionCode){

         Case SendPacket:            send(….)

         Case ReceivePacket:         receive(…)

         …..

}

写到这里大家明白什么意思了吧。保证相同的函数名就是说:网卡驱动是和pNA+协议栈互连的,那么如何保证pNA+协议栈和不同的驱动都兼容呢,一个简单的办法就是仅仅使用一个入口函数。通过改变如果函数的参数值,来调用内部的各个函数。这样的做法是可以进化的:如果以后想调用新的函数,增加相应的函数参数值就好了。如果我们将网卡驱动和pNA+协议栈看作两个层的话,我们可以发现:

层与层之间的互连接口是很小的(这里是一个入口函数),一般是采用名字解析的办法而不是具体的函数调用(利用FunctionCode调用函数,Nilan仅仅实现名字解析的功能)――!接口限制和名字解析

接口限制:层与层之间仅仅知道有限的函数

名字解析:层与层之间建立共同的名字与函数的对应关系,之间利用名字调用功能。

3.CALLBACK函数。

我觉得这是C语言的一个创举,虽然它很简单,就象如何把鸡蛋竖起来一样,但是你如果没想到的话,嘿嘿。如果说静态入口函数实现了一个可管理的宏观的话,CallBack就是实现了一个可进化的微观:它使得一个函数可以在不重新编译的情况下实现功能的添加!但是在最最早期的时候,也有蛮多人持反对态度,因为它用了函数指针。函数指针虽然灵活,但是由于它要访问内存两次才可以调用到函数,第一次访问函数指针,第二次才是真正的函数调用。它的效率是不如普通函数的。但是在一个不太苛刻的环境下,函数调用本身就不怎么耗时,函数指针的性能又不是特别糟糕,使用函数指针其实是一个最好的选择。但是函数指针除了性能,最麻烦的地方就是会导致程序的“支离破碎”。试想:在程序中,你读到一个函数指针的时候,如果你愣是不知道这个函数指针指向的是哪个函数,那个感觉真的很糟糕。(可以看后面的文章,要使用先进的程序框架,避免这样的情况)

三、Event和Message

看了上面的描述,相信大家多少有些明白为什么要使用Event和Message了。具体的函数调用会带来很多的问题(虽然从效率上讲,这样做是很好的)。为了提高程序的灵活性,Event和Message的办法产生了。用名字解析的办法代替通常的函数调用,这样,如果双方对这样的解析是一致的话,就可以达到一个统一。不过Event和Message的作用还不仅仅是如此。

Event和Message还有建立进程间通信的功能。进程将自己的消息发给“控制中心”(简单的就是一个消息队列,和一个while循环不断的取消息队列的内容并执行),控制程序得到消息,分发给相应的进程,这样其他进程就可以得到这个消息并进行响应。

Event和Message是很灵活的,因为你可以随时添加或者关闭一个进程,(仅仅需要添加分发消息的列表就可以了)Event和Message从程序实现上将我觉得是一样的,只不过概念不同。Event多用于指一个动作,比如硬件发生了什么事情,需要调用一个什么函数等等。Message多用于指一个指示,比如什么程序发生了什么操作命令等等。

四、小结

其实编程序和写文章一样,都是先有一个提纲,然后慢慢的丰富。先抽象化得到程序的骨架,然后再考虑各个方面的其他内容:程序极端的时候会发生什么问题?程序的这个地方的功能现在还不完善,以后再完善会有什么问题?程序是不是可以扩展的?

相关文章推荐

C语言模拟实现C++的继承与多态

一、面向过程编程与面向对象编程的区别 众所周知,C语言是一种典型的面向过程编程语言,而C++确实在它的基础上改进的一款面向对象编程语言,那么,面向过程与面向对象到底有什么样的区别呢?【从设计方法角...

C语言模拟实现C++的继承与多态

一、面向过程编程与面向对象编程的区别 众所周知,C语言是一种典型的面向过程编程语言,而C++是在它的基础上改进的一款面向对象编程语言, 那么它俩有什么区别呢?【从设计方法角度看】 面向过程程序设...

C语言模拟实现C++中的继承和多态

先来说明一下C++中的继承和多态 继承:一个类可以使用现有类的所有功能,也就是说一个对象可以直接使用另一个对象的属性和方法,并在无需编写现有类的情况下对这些功能进行扩展。通过继承产生的新类称为子类或...

C语言如何模拟C++实现继承和多态

首先要明白c++的三大特性:封装,继承,多态 而这些C语言统统都没有,那么C语言如何实现继承和多态呢? C++的继承属性是通过访问限定符:public,protected,private来实现. ...

模拟继承和多态--C语言模式实现C++继承和多态

C语言与C++语言的不同?C语言是面向过程的语言。 C++语言是面向对象的语言。面向过程:面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了...

C语言模拟多态

#include #include //模拟一个类A typedef struct A { void *vptr;//虚函数指针 //下面内容可以删掉,写上的目的:看着像一个类 ...

C语言面向对象的封装、继承和多态的思想

#include #include #define Jicheng #define Fengzhuang #define Duotai /*思想为结构体的嵌套*/ #ifdef Jic...

c语言实现继承和多态

继承 概念:继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现由简单到...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)