zoukankan      html  css  js  c++  java
  • 用C表达面向对象语言的机制——C#版

    PS:本文PDF版在这里(格式更好看一些)。最新的源代码请在本页面文末下载,PDF中的链接不是最新的。

    用C表达面向对象语言的机制——C#版

    我一直认为,面向对象语言是对面向过程语言的封装。如果是这样,那么就应该能够用C来模拟C#的代码风格,写出面向对象形式的代码。本文逐步展示了与C#对应的C代码是如何实现的。

    1. 目标

    面向对象语言的三大特性(封装、继承、多态)中,封装和继承的C版写法需要研究,而多态似乎不关乎新的写法。

    所以本文就展示如何用C来模拟C#的封装、继承、虚方法、关键字as和interface的写法。

    2. 封装字段和方法

    例如如下的C#代码,写了一个典型的类。

    class BaseClass
        {
            public int baseField1 = 0;
    
            public void BaseMethod1() 
            {
                Console.WriteLine("BaseClass.BaseMethod1()");
            }
        }

    其典型的使用方式如下。

    // create a BaseClass object
       BaseClass pBaseClassObj = new BaseClass();
       // use BaseClass object's field
       Console.Write("BaseClass obj : pBaseClassObj->baseField1({0})
    ", pBaseClassObj.baseField1);
       // use BaseClass object's method            
       pBaseClassObj.BaseMethod1();

    如果想用C来实现类似的使用方式,应该如何写呢?

    1) 用struct代替class

    用C的struct代替C#的class的字段

    typedef struct _BaseClass
    {
        int baseField1;
    
    } BaseClass;

    但是struct没有“在类型里面写方法”这种功能。我们用变通的方法来实现。

    void BaseMethod1(BaseClass * pThis) 
    {
        printf("BaseClass.BaseMethod1()
    ");
    }

    就是说,每个方法都加上指向这个结构体的指针。

    2) 用New[ClassName]代替new

    一个class类型都有至少一个构造方法,是专门给new关键字用的。所以我们写一个C版的new方法。

    BaseClass * NewBaseClass()
    {
        // initialize base class
        // alloc for space
        BaseClass * pResult = (BaseClass * )malloc(sizeof(BaseClass));
        // initialize fields
        pResult->baseField1 = 0;
        // return result
        return pResult;
    }

    C版的New方法用malloc来申请内存空间,这样就把对象new到了堆上,和面向对象语言的处理方式相同。

    其使用方法如下。

    // create a BaseClass object
        BaseClass * pBaseClassObj = NewBaseClass();
        // use BaseClass object's field
        printf("BaseClass obj : pBaseClassObj->baseField1(%d)
    ", pBaseClassObj->baseField1);
        // use BaseClass object's method
        BaseMethod1(pBaseClassObj);

    与C#版的使用方法异曲同工。

    3. 实现继承

    在上文的BaseClass基础上,来展示如下代码的实现。

    class DerivedClass : BaseClass
        {
            public int derivedField1 = 0;
    
            public void DerivedMethod1()
            {
                Console.WriteLine("DerivedClass.DerivedMethod1()");
            }
        }

    3) 用组合代替继承

    仍然用struct来代替class,但DerivedClass继承了BaseClass这一性质,如何实现呢?C语言虽然没有“继承”的概念,但是struct可以“组合”(与C#的组合概念一样),我们用一个BaseClass的指针来表示“继承”的概念。

    用组合模拟继承的机制

    在C 版的构造函数NewDerivedClass()里,首先构造了一个“基类”BaseClass的对象,然后赋值给DerivedClass对象的pBase指针。这里也符合面向对象语言“先调用父类的构造函数,再调用子类的构造函数”的特点。具体地说,是“首先,子类的构造函数调用父类的构造函数;然后,完成对父类构造函数的调用;最后,完成对子类函数的调用”。

    子类的典型使用方式是:

    // create a DerivedClass object
                DerivedClass pDerivedClassObj = new DerivedClass();
                // use DerivedClass object's field
                Console.Write("DerivedClass obj : pDerivedClassObj->derivedField1({0})
    ", pDerivedClassObj.derivedField1);
                // use DerivedClass object's base class' field
                Console.Write("DerivedClass obj : pDerivedClassObj->baseField1({0})
    ", pDerivedClassObj.baseField1);
                // use DerivedClass object's method
                pDerivedClassObj.DerivedMethod1();
                // use DerivedClass object's base class' method            
                pDerivedClassObj.BaseMethod1();

    C版的DerivedClass使用方式,仍然与此类似。

    // use DerivedClass object's field
        DerivedClass * pDerivedClassObj = NewDerivedClass();
        // use DerivedClass object's field
        printf("DerivedClass obj : pDerivedClassObj->derivedField1(%d)
    ", pDerivedClassObj->derivedField1);
        // use DerivedClass object's base class' field
        printf("DerivedClass obj : pDerivedClassObj->baseField1(%d)
    ", pDerivedClassObj->pBase->baseField1);
        // use DerivedClass object's method
        DerivedMethod1(pDerivedClassObj);
        // use DerivedClass object's base class' method 
        BaseMethod1(pDerivedClassObj->pBase);

    这里可以看到,“子类”DerivedClass能够调用自身的字段和方法,也能够借助pBase指针调用父类的字段和方法,完全达到了面向对象语言的要求。

    4) 用函数指针代替virtual

    继承还有一个特性,就是虚方法。C语言如何做到呢?答案就是使用函数指针。

    为展示清楚,我们重新定义两个类。

    class VirtualClass
        {
            public virtual void VirtualMethod1()
            {
                Console.Write("VirtualClass.VirtualMethod1()
    ");
            }
        }
        class OverrideClass : VirtualClass
        {
            public override void VirtualMethod1()
            {
                Console.Write("OverrideClass.VirtualMethod1()
    ");
                //base.VirtualMethod1();
            }
        }

    其典型的使用方式如下。

    // create a VirtualClass object
        VirtualClass virtualClassObj = new VirtualClass();
        virtualClassObj.VirtualMethod1();
        // create a OverrideClass object
        OverrideClass overrideClassObj = new OverrideClass();
        overrideClassObj.VirtualMethod1();
        // OverrideClass object assigned to VirtualClass object
        VirtualClass pVirtualClass = new OverrideClass();
        pVirtualClass.VirtualMethod1();

    其输出应该是:

    VirtualClass.VirtualMethod1()
    OverrideClass.VirtualMethod1()
    OverrideClass.VirtualMethod1()

    虚函数的关键性质,在于父类知道子类的override函数在哪儿,这只能在创建子类的时候,改变父类能够调用的函数。所以很自然就需要函数指针帮忙。按照上文的方法,在New[ClassName]创建父类的时候,让父类对象的函数指针指向父类的virtual方法。等父类对象创建完毕,继续创建子类对象的时候,修改父类对象的函数指针,使其指向子类的override方法。(这就要求父类和virtual方法和子类的override方法的声明完全相同)

    其C版代码如下。

    VirtualClass
    OverrideClass

    C版的使用方法如下。

    // create a VirtualClass object
        VirtualClass * virtualClassObj = NewVirtualClass();
        virtualClassObj->pVirtual1(virtualClassObj);
        // create a OverrideClass object
        OverrideClass * overrideClassObj = NewOverrideClass();
        overrideClassObj->pBase->pVirtual1(overrideClassObj->pBase);
        // OverrideClass object assigned to VirtualClass object
        VirtualClass * pVirtualClass = NewOverrideClass()->pBase;
        pVirtualClass->pVirtual1(pVirtualClass);

    其使用方式异曲同工,而输出和C#版是一样的。

    如果基类有多个virtual方法,其声明相同,就可以使用函数指针数组;有几种不同声明的virtual方法,基类就要有几个函数指针(或数组)指着他们。

    4. 用Convert2Type代替as

    5) 使用关键字as

    看如下的例子。

    class FullClassBase
        {
        }
        class FullClassDerived : FullClassBase
        {
            public int fcdField1 = 0;
    
            public void fcdMethod1()
            {
                Console.Write("FullClassDerived.fcdMethod1()
    ");
            }
        }

    其使用方式如下。

    FullClassBase baseObj = new FullClassDerived();
        FullClassDerived derivedObj = baseObj as FullClassDerived;
        if (null != derivedObj)
        {
            derivedObj.fcdMethod1();
        }

    上一节展示了在C中,如何用父类的指针指向子类的对象。而这里的C#的关键字“as”将父类的指针还原为子类的指针,这是如何实现的?

    答案:as可以用一个函数代替。函数名叫做Convert2Type(随便你喜欢什么名字)。as的本质就是一个函数。

    我们要做的是:已知一个对象的指针,已知要转换出来的目标类的类型,求出该对象指向目标类的指针。若不存在,则返回NULL。

    我们需要一些准备工作。

    6) 准备类型标识结构

    1

    每个类型都要有一个标识符(用int即可),用以区分不同的类型

    2

    基类对象要有一个指向子类对象的指针,由于子类可能有多种,显然只能用void *类型

    每一个类型都要记录父类指针、子类指针、唯一标识符等内容。另外,Convert2Type函数只能有一个声明,不可能把所有类型的指针都传给他。为了给他尽可能多的数据,我们需要将类型的父类指针、子类指针、唯一标识符等信息封装为一个单独的struct Metadata。具有继承关系的类型,其Metadata对象也相互指向。这样,Metadata就包含了描述全部继承关系的数据。

    typedef struct _Metadata Metadata

    为方便说明Convert2Type的实现原理,我们再定义C版的FullClassBase和FullClassDerived。

    FullClassBase in C
    FullClassDerived in C

    现在,Metadata持有父类、自身、子类的信息,将其作为参数传入Convert2Type函数,是足够找到目标类型的。

    7) 实现Convert2Type

    Convert2Type

    C版的使用方式如下。

    FullClassBase * baseObj = (FullClassBase *)(NewFullClassDerived()->metaInfo->pBaseIdentifier->pThis);
        FullClassDerived * derivedObj = (FullClassDerived *)Convert2Type(baseObj->metaInfo, FullClassDerivedTypeId);
        if (NULL != derivedObj)
        {
            fcdMethod1(derivedObj);
        }

    关键字“as”通过Convert2Type函数和一个C语言的强制类型转换实现了。

    5. 用链表代替interface

    C#中的一类可以实现多个interface,这在C中如何表达?

    答案是:首先,interface也用struct代替。Interface只不过是一个特殊的类类型,其内部字段都是函数指针。C#的类实现interface,实际上仍然是继承了这个interface类型。这和继承父类类型没有区别。但一个类可以实现多个interface,这就需要用链表来记录这些interface了。

    8) 创建链表类型LinkNode

    typedef struct _LinkNode LinkNode

    其中的void * pValue;用于保存这个类型实现的接口的Metadata。

    9) 为Metadata添加用于记录interface的链表指针

    新的Metadata

    10) 修改Convert2Type函数

    新的Convert2Type函数

    11) 验证

    下面就来验证一下。

    我们创建一个interface类型,让FullClassBase实现他。

    interface InterfaceClass
        {
            void Method4InterfaceClass();
        }
        class FullClassBase : InterfaceClass
        {
            public void Method4InterfaceClass()
            {
                Console.Write("FullClassBase.Method4InterfaceClass()
    ");
            }
        }

    对应的C代码如下。

    typedef struct _InterfaceClass
    {
        Metadata * metaInfo;
        void (*pInterfaceMethod1)(_InterfaceClass *);
    } InterfaceClass;
    
    static int InterfaceClassTypeId = 6;
    
    InterfaceClass * NewInterfaceClass()
    {
        InterfaceClass * result = (InterfaceClass *)malloc(sizeof(InterfaceClass));
        
        result->metaInfo = NewMetadata(result, InterfaceClassTypeId, NULL);
        result->pInterfaceMethod1 = NULL;
    
        return result;
    }
    typedef struct _FullClassBase
    {
        // basic info
        Metadata * metaInfo;
        // fields
    
        // virtual methods
    
    } FullClassBase;
    
    // type id
    static int FullClassBaseTypeId = 4;
    
    // method declarations
    void Method4InterfaceClass(InterfaceClass * pInterfaceClass);
    
    // the new method
    FullClassBase * NewFullClassBase()
    {
        // alloc for space
        FullClassBase * pResult = (FullClassBase *)malloc(sizeof(FullClassBase));
    
        // initialize basic info
        pResult->metaInfo = NewMetadata(
            pResult, 
            FullClassBaseTypeId, 
            NULL); 
        InterfaceClass * interfacePart = NewInterfaceClass();
        interfacePart->metaInfo->pDerivedIdentifier = pResult->metaInfo;
        interfacePart->pInterfaceMethod1 = Method4InterfaceClass;
        LinkNode * nextNode = NewLinkNode();
        nextNode->pValue = interfacePart->metaInfo;
        pResult->metaInfo->pInterfaceList->pNext = nextNode;
        // initialize fields
    
        // initialize virtual methods
    
        // return result
        return pResult;
    }
    
    void Method4InterfaceClass(InterfaceClass * pInterfaceClass)
    {
        printf("FullClassBase.Method4InterfaceClass()
    ");
    }

    C#版的使用方式如下。

    FullClassBase obj = new FullClassBase();
        obj.Method4InterfaceClass();
        InterfaceClass interfaceObj = obj as InterfaceClass;
        interfaceObj.Method4InterfaceClass();

    对应的C版代码的使用方式如下。

    FullClassBase * obj = NewFullClassBase();
        Method4InterfaceClass((InterfaceClass *)Convert2Type(obj->metaInfo, InterfaceClassTypeId));
        InterfaceClass * interfaceObj = (InterfaceClass *)Convert2Type(obj->metaInfo, InterfaceClassTypeId);
        Method4InterfaceClass(interfaceObj);

    12) 关于interface的性质

    有了interface以后,实际上就是出现了“多继承”。

    一个接口可以被多个类型继承,所以C版代码里,interface里的函数指针类型的第一个参数只能是interface本身。即:一个类型A,实现了interface X里的方法,就意味着这个方法的第一个参数类型变成了X的指针(而不再是A的指针)。

    基于这样的C版设计,在调用接口函数时,一定会发生类型转换(调用Convert2Type函数)。

    所以,如果在interface里的方法,需要用this指针的时候,会发生从X的指针到A的指针的类型转换。

    6. 虚函数与接口与多次继承

    如果接口的方法在基类里标记为virtual,并且在子类和孙类里都override了,会怎么样?

    虚函数与接口与多次继承

    对于这样的情况,下面的情形会输出什么?

    VirtualAndInterfaceDerived2 obj = new VirtualAndInterfaceDerived2();
        VirtualAndInterfaceBase baseObj = obj;
        VirtualAndInterfaceInterface interfaceObj = obj;
        obj.InterfaceMethod();
        baseObj.InterfaceMethod();
        interfaceObj.InterfaceMethod();

    答案是三行“VirtualAndInterfaceDerived2.InterfaceMethod()”,即全部调用了最后override的方法。就是说,转换出来的C代码中,除了基类的virtual方法的函数指针外,接口对象的函数指针也要修改为指向最后override的函数。

    这又带来一个问题。如果我们定义一个新的接口。

    interface VirtualAndInterfaceInterface2
        {
            void InterfaceMethod();
        }

    这个新接口和VirtualAndInterfaceInterface所拥有的方法声明相同。现在让VirtualAndInterfaceDerived2实现这个接口。

    class VirtualAndInterfaceDerived2 : VirtualAndInterfaceDerived, VirtualAndInterfaceInterface2
        {
            public override void InterfaceMethod()
            {
                Console.WriteLine("VirtualAndInterfaceDerived2.InterfaceMethod()");
            }
        }

    我们发现无需其他改动即可编译。

    根据之前的结论,在C版代码中,接口类型的函数指针的第一个参数类型只能是接口类型本身。这也使得class类型的C版类型中,其函数类型的第一个参数只能是接口类型。但是在这个例子里,InterfaceMethod方法同时成为VirtualAndInterfaceInterface和VirtualAndInterfaceInterface2的方法。那么InterfaceMethod的第一个参数类型就无从选择了。

    为解决这个问题,我们重新审视接口的定义。在C版代码中,InterfaceMethod第一个参数类型,就应该是接口本身,这无法改变。实现了此接口的类类型,应该单独为InterfaceMethod添加一个声明和InterfaceMethod相同的函数,以满足该接口的需要。在类类型里的InterfaceMethod,借助Convert2Type函数,直接调用自己内部的InterfaceMethod函数。

    C版的代码如下。

    VirtualAndInterfaceInterface
    VirtualAndInterfaceInterface2
    VirtualAndInterfaceBase
    VirtualAndInterfaceDerived
    VirtualAndInterfaceDerived2

    简单来说,类型C实现了接口IC,那么,C就要为IC的各个函数分别创建声明完全相同的函数,这些函数通过Convert2Type得到需要的类型指针,再调用C内部的函数。

    7. public、protected和private

    这三个关键字用C是无法实现的,他们是面向对象语言在编译器进行语义分析时进行处理的。如果不符合规定(例如类型外调用了private的字段),编译器就报错,不给你生成代码。仅此而已。

    8. 结论

    我们规定:

    CA表示基类,CA实现的接口为ICA1,ICA2,。。。
    CB继承自CA,CB实现的接口为ICB1,ICB2,。。。
    CC继承自CC,CC实现的接口为ICC1,ICC2,。。。
    某类型X包含的字段为XF1,XF2,。。。
    某类型X包含的方法为XM1,XM2,。。。

    那么,可以用如下规则将C#代码翻译为C代码。

    类型X用struct代替,X中的字段用相应的字段代替;X中的方法用以X的指针为第一个参数的函数代替。
    
    为X添加一个Metadata结构体的指针,用于记录X的实例的信息:基类实例、子类实例、接口实例(链表)、类型编号、实例本身。
    
    为Metadata编写Convert2Type函数,返回给定实例关联的目标类型的实例。
    
    为X分配一个唯一的整数作为类型编号。
    
    为X编写NewX函数,返回X的实例。
    
    CB的NewCB函数,
    
    先创建CA的实例,
    
    然后创建ICB1、ICB2。。。的实例,
    
    然后创建CB的实例,
    
    然后修改CA实例的子类实例指针(指向CB实例),
    
    然后修改ICB1、ICB2。。。的函数指针(指向CB的函数),
    
    然后修改CA中virtual方法的函数指针(指向CB的函数),若此virtual方法也是ICAn的方法,也要修改ICAn的函数指针(指向CB的函数)。

    最后强调一下容易混淆的地方。

    CA中的virtual方法,在CB中可以override掉,然后还可以继续在CC中override掉。(而不是不可以继续override)其函数指针指向最后override的方法。
    
    CA中的virtual方法,如果恰好也是ICA1中的方法,则ICA1中的函数指针也要指向最后override的方法。(而不是CA中的virtual方法)

    2016-05-15

    感想

    至此,本文展示了面向对象语言中的class、new、virtual/override、as、interface等关键字的实现机制,展示了将C#翻译为C的方法。

    很早就在想,面向对象语言到底是如何实现的。封装还简单,用struct代替class即可。继承的虚函数特性,只听过是通过“晚绑定”实现的,然后就找不到其他资料了。这几天趁国庆假期好好想了想,边想边做,用C实现了面向对象的语言特性,也证实了“面向对象语言是对面向过程语言的封装”这句话。

    现在有了把面向对象语言的代码翻译为面向过程语言的代码的途径。这让我开始反思,为什么要把C封装为面向对象语言?面向对象语言是如何从无到有的?最开始的那个人是怎么设计出这样一套机制的?他之前没有面向对象的任何概念,他的思路是什么?

    最后贴上自己总结的一段话。

    机器语言(01串)是对数字电路的计算和控制逻辑的封装,人可以用打孔纸带来控制计算机。汇编语言(指令代码)是对机器语言的封装,人可以用易于理解、记忆和维护的名称来(间接)写机器语言。面向过程语言(例如C)是对汇编语言的封装,人可以用模块化的设计思路编写代码。面向对象语言(例如C#)是对面向过程语言的封装,人可以用模拟现实世界的思路编写代码。

    点此下载源代码

  • 相关阅读:
    图书管理系统登录界面
    图书管理系统的管理者界面
    图书管理系统-servlet层
    图书管理系统的dao层
    Linux 内核优化
    第十一章 Nginx之服务代理
    第十章 Nginx之LNMP拆分
    第九章 Nginx之LNMP架构
    第八章 Nginx常用模块
    第七章 WEB服务之Nginx
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/Write_CSharp_In_C.html
Copyright © 2011-2022 走看看