其实早就想写这篇文章很久了,由于先前有个项目需要用到C来制作其系统架构,后来由于其他原因一直没有时间写出来,现在将把我那里学习到的东西一一道上。
应该很多人都认为面向对象的语言开发起来十分方便。由于其封装、继承等特性使面向对象语言更容易明确其结构,使开发过程中的结构清晰。从另一方面看面向对象语言在运行时将比非面向对象语言更耗资源、运行性能上也有所下降,这可能是导致现有的驱动程序大多使用C语言开发的原因。也许有人会问,现在IC能力都这么强,为什么还会执着着那个资源及运算能力呢?对于消费类电子可能这个影响不大,但对于工业方面来说这可能是致命的(实时要求较高)。好了,不扯远了,现在开始逐步进入正题。

重温面向对象语言:

在说到这里时,我当作大家都了解并开发过C++(及类似的面向对象的语言)。面向对象语言中,有几个重要概念:继承、多态、重载、封装。

继承主要是为了得到父类中的一些变量及函数(或方法),这样可以抽象出类的共性,当子类继承后可以减少一些变量和函数的重复声明,达到类的复用,也可以减少类与变量和函数的硬性绑定(全写在一个类中)。

多态是指利用父类指针/名称(C++中是指针,Java是名称)可以引用子类对象,来达到调用子类的函数。

重载是指一个类中的方法与另一个方法同名,但是参数表不同。

封装主要是通过确定变量和函数的访问类型来达到哪些变量函数可以对外使用(public),哪些变量函数不能对外使用(private),以及哪些变量函数可以被继承(protected)。

技术应用:

Unix和Linux都是利用C语言来开发的,但完全使用结构化式编程会使其结构混乱,据说开发者也把C语言“类面向对象化”,使其结构更加清晰(具体我没有研究过),大家可以看看Linux内核代码来了解这种编程方式。

应用实况:

由于本人是在ARM中开发,而且没有使用操作系统,因此为了防止内存泄漏等问题,所以我没有使用动态创建(malloc、calloc、realloc)。因此有些模仿可能叙述不到。

实例叙述:

要利用C语言模仿面向对象的功能是可以的,但并不能与面向对象的概念相混淆,只能说是功能相近,并不是相同。
下面将讲述本人的见解,如果不当之处,请高手指点!!

一、C语言中的“类”

1)定义“类”的成员变量
C语言没有类,但可以使用结构体充当一个类:

type struct CClass
{
struct CClass this; //获取结构体自身指针
int m_nNum;
char *p;
...    //自行添加更多变量
}CClass;

与类不同,结构体只能定义变量,不能够定义函数,可以通过函数指针的方法来实现其功能。

注:函数指针在很多情况下都不建议使用,这并不是它不好用,而是容易出错,而且出错后也难道测试出出错的位置。其实指针在C语言中是最强大的东西(个人认为),但由于容易出错才有那么多人反对使用。在使用指针(不管什么指针)都要注意,以防出错。


2)定义“类”的成员变量及函数

typedef struct CClass
{
struct CClass this;
int m_nNum;
char *p;
...    //自行添加更多变量
void (*MyFunction)(struct CClass this,int Num);
...    //自行添加更多函数
}CClass;

好了,这里定义了函数指针,感觉是不是很像一个类?也许有人会问,为什么加入一个struct CClass this 在函数第一个参数呢?

原因是这样的,由于结构体不像类,类中可以随意调用类中的成员,但是结构体却不一样,它无法得知结构体中的变量,因此可能通过参数形式传入。这里struct CClass this也方便了函数的调用。


3)定义“类”的构造函数
与面向对象不同,C语言的“类”的构造函数不能放在“类”中,只能放在“类”外。
CClass * CClassCtor(CClass *this);
其中函数名可以任意取(只要一看就明白,就行了),构造函数主要是变量的初始化,以及函数指针的赋值。


4)“类”中函数的绑定

void MyFunction(struct CClass this,int Num)
{
     //具体操作
}
CClass * CClassCtor(CClass *this)
{
     this->m_nNum = 0;
     this->p = 'a';
     this->MyFunction = MyFunction;
}

这里MyFunction函数名称可以任取,主要通过赋值来匹配函数指针。

当调用“类”CClass中的MyFunction时,只要->MyFunction()就可以了,如下。
CClass *pClass,objClass; //定义一指针、一结构体变量
pClass = CClassCtor(&objClass); //调用构造函数
pClass->MyFunction(pClass,10); //调用MyFunction函数


5)“类”中的不足
C语言中的“类”并不能够像面向对象语言那样,通过public、protected、private来控制变量、函数的访问。


二、C语言中“类”的“继承”
其实结构体并没有继承可言,只是尽量做到类似继承。

1)“继承”方式1
由于没有继承机制,只能简单地重写变量函数(我觉得这方法很蠢,但是最直观,但也最容易出错,而且一但父类修改后,其所有子类都得修改)

typedef struct CFather
{
      struct CFather *this
      char m_Name[10];
      int m_Age;
      void (*Walk)();
}CFather;
typedef struct CSon
{
      struct CSon *this    //这个来自父“类”,但要改为当前“类”指针类型
      //来自父“类”的变量及函数
      char m_Name[10];
      int m_Age;
      void (*Walk)();
      //子“类”定义的函数
      int m_PocketMoney;
      void (*UsePocketMoney)(int nNum);
}CSon;

可能有些人在编写面向对象语言时,会把变量和函数分开。但在C语言的“类”中,如果想达到“继承”,必须先写父“类”变量函数,再写新增的变量函数,而且变量及函数上的数量及顺序得完全一致(后面将讲述原因)。

注:“继承”时,子“类”应包含父“类”的所以变量及函数,而且顺序上完全一致,当写完父“类”变量及函数时,再写子“类”的变量和函数。


2)“继承”方式2
这种方式主要通过内嵌结构体指针来达到“继承”。这样可以减少重复的代码,而且可以减少漏写的问题。(这种方法当修改父类时,并不影响子类结构,只要修改相应函数功能即可。但是调用父类函数时,显得很臃肿)

typedef struct CFather
{
      struct CFather *this
      char m_Name[10];
      int m_Age;
      void (*Walk)();
}CFather;
typedef struct CSon
{
      //来自父“类”的变量及函数
      CFather *m_pfather;
      //(或CFather m_father;)
      //子“类”定义的函数
      int m_PocketMoney;
      void (*UsePocketMoney)(int nNum);
}CSon;

当使用指针时,可以通过malloc、calloc等来动态创建父结点,也可以在全局中定义再向指针赋地址。

三、C语言中“类”的“多态”
前面说过,父“类”的顺序要与子“类”上的数量及顺序一致,主要是为了实现“多态”。那么C语言怎么实现“多态”呢?
由于结构体是一片连续的内存区域,因此当结构体被声明时,将分配特定的顺序,再加上各个类型有各自的大小(通过sizeof()可以得出),可以通过大小要获取某片区域的内容。
父“类”指针就像一个内存映射表,当父类指向子“类”时,父“类”会按其顺序从头到尾与子“类”开始对应,但通常子“类”声明的都比父类多,所以子“类”的部分变量及函数并没有被父“类”指针所以映射,也因此达到了“多态”的效果。
只有在使用“继承”方式1 和 “继承”方式2中的非指针父“类”时,才能够使用其“多态”特性。
使用“继承”方式1 好还是使用“继承”方式2,这就得看具体需求了。
1)方式1虽然比较蠢,但是这样只要“指针->函数()”即可调用其函数。
2)使用“继承”方式2,当子“类”还有多个子“类”时,将使用多个“指针->father.[father. ···]函数()”来调用。

下图为方式一的“类”定义,其中“父类”中的成员,“子类”要全部重复写一遍,当“父类”指针指向“子类”时,可以通过调用printAge();来调用“子类”中对应的函数。

下图为方式二的“类”定义,其中“子类”包含了“父类”中的成员,使用些方法不利于“多态”,因为不管体积指针,都会占用相同大小的内存空间,导致内存地址并不对应,即当调用printAge()时,将会出现指针异常。

如果想使用类似上面的包含关系,而且又想利用“多态”,可以写成如下:

这样使用包含非指针的方法,可以达到又包含,又“多态”的效果,但是有没有不良效果,本人并没有仔细研究。

四、C语言中函数的重载
其实在C语言中,并不可以函数同名,因为重载在C语言中也成为不可能的事件了(可能只是我不会实现而已)。

五、C语言中的封装性
C语言中也并没有像C++那样的封装性,因为只要变量或函数在头文件中声明了,就可以被随意调用。如果不想变量或函数被调用,可以在.c文件中声明静态变量或函数。这样,即使头文件被包含了,也可以把数据封装在.c文件中,即使它没有像C++那优秀的封装性。

总结:
利用上面方法,可以提高代码的利用性,可以类似C++等面向对象语言一样,通过加“类”,来构建自己想要的效果。以上皆是本人学习时使用的方法及自身理解的一些观点。如果有不当之处请提出,方便我去改正。