• C语言的面向对象技术(转)


    来源:http://www.hackbase.com/tech/2005-07-21/27153.html

    在面向对象的语言里面,出现了类的概念。这是编程思想的一种进化。所谓类:是对特定数据的特定操作的集合体。所以说类包含了两个范畴:数据和操作。而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() {}         //当然就可以添加新的实现咯


    类模拟的性能分析
           类模拟中使用了大量的函数指针,结构体等等,有必须对此进行性能分析,以便观察这样的结构对程序的整体性能有什么程度的影响。
     
    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多用于指一个指示,比如什么程序发生了什么操作命令等等。

    四、           小结

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

    本系列文章是我这些阶段的一些心得,目的是抛砖引玉,希望能和大家交流,得到更多的知识。Liyuming1978@163.com (这个信箱以前发了一个文章 C优化之路,现在都快成垃圾信箱了,呵呵,网络的力量真是强大呀)

    作者Blog:http://blog.csdn.net/liyuming1978/

    ==================================================

    作者: Panderen

    博客: http://panderen.cnblogs.com

    签名: 机会总是为有准备的人而准备的!

  • 相关阅读:
    3D开发基础知识和简单示例
    六大设计原则详解
    InnoSetup 根据选择的安装语言显示不同语言的(协议)License和更新说明
    ASP.NET Core如何在.NET Core中构建和运行时禁用浏览器启动
    DevExpress 设置DateEdit显示年月季度
    DevExpress设置单元格不可编辑,选单元格则选中一行
    sql server数据库只有mdf文件,没有ldf,如何加载
    .net core 整洁架构入门
    在ASP.NET Core使用Entity Framework Core的日志显示sql语句
    在ASP.NET Core使用Entity Framework Core的日志
  • 原文地址:https://www.cnblogs.com/panderen/p/2506930.html
Copyright © 2020-2023  润新知