最近帮个同学debug C程序,闲暇之余也没有闲着,查过很多资料后,对C语言的理解深了一点。C语言不愧为世界IT的基石,即使历经几十
年,依然经久不衰。
我也只能这样跟风称赞几句,至于其中的深奥,实在不得领会,或者,根本不用领会。
进入正题,怎样用C语言来实现面向对象模式。为何有这个想法,是在看《设计模式初学者指南》中,序言中提到过,面向对象从来不是语言
,而是方法,在C语言时期,C++尚在酝酿当中,那时便有面向对象的“设计模式”,比如用一个虚函数指针表来维护虚函数等等。语言在发展
,模式就成了语法,条款就成了法律,情人就变成了老婆,世间万物无不如此。
面向对象的设计方法,首先得知道重点是什么,面向对象最重要的地方,我认为在于多态和数据隐藏,类,对象,继承重载等概念同样非常关
键,但是都基于前两位。
当然,在什么时候用合适的方法,函数式的设计从来没有消失过,反而愈发壮大。
为了满足好奇心,我百度花了不少时间,心里大概有个底,如何用C语言“实现”面向对象。至少要看起来像,这个也是最终的目的之一。
好了,要看起来像,用起来像java,c#,首先得有 类,这个可以交给C语言中的结构体,可惜的是结构体只能封装数据,不能封装函数,要说
例外,就是在结构体中声明函数指针,来达到看起来像“成员函数”的样子。
现在,我要实现一个“泛型”容器类 List, 让它用起来就像C#中的List<T>一样:
/**************** C# code:*****************************/
List<double> myList=new Lis<double>();
double data1;
myList.Add(data1);
为什么这里的泛型要打上引号,因为在C语言中无法主动检测类型,也就不能比较"Type1==Type2",无法区分两者的真正类型。
我所知道的方法实现部分泛型就是用void* 指针,void* 指针可以转换为任意的指针,这个作用和C#中Object的装箱拆箱别无二致。
这个容器采用什么样的数据结构呢,要能变长,随意增减大小,最好就是双链表了,否则两遍数据结构这课不是白学了吗.。
所以首先定义双链表的节点:
//List.h
typedef struct _Linklist{
Linklist* pPrvNode; //指向前一个节点
Linklist* pNextNode; //指向后一个节点
void *dataObj; //注意这里,每个NODE储存的是数据的引用--它的指针。由于我们不可能知道数据是什么样的,所以用void*
//代替,这是就是泛型
}Linklist;
然后我们的List容器,需要知道自己链表的首尾指针即可。
我们想想看,容器还要有哪些对外的特性?这决定了它的对外接口
通俗的说
1 要能添加数据 Add()
2要能按索引删除数据 Remove()
3要能通过索引得到数据 GetObjByIndexOf()
4要能返回一个迭代器 Iterator()
//迭代器是什么,javacode:
iter=List.Iterator();
while(iter.hasNext())
{
data= iter.next();
}
最后是所有类的两个函数 构造函数和析构函数。
通过上面的分析,我们能得到List类的大致的设计图,用代码描述出来
//List.h
typedef struct _List{
//"成员变量"
int length; // 数据的总个数
int datasize; //注意这里,这里保存一个数据的大小,是用来初步“类型检查”,如果加入的数据和之前的数据大小不一样,
//则说明类型不同,不让加入 ,仅此而已。
Linklist* _head; //链表的头指针
Linklist* _tail; //链表的尾指针
Iterator* _iter; //迭代器的指针
// “成员”函数,这里声明一些上函数指针。由于C语言函数不能获得之外的局部变量,所以我们给它参数中放上this指针,以便访问到对象
的//"数据"
Iterator* (*GetIterator)(List* thisList); //获得迭代器
Bool (*AddObj)(List* thisList,void* obj,int datasize);
Bool (*Remove)(List* thisList,void* obj);
void* (*GetObjByIndex)(List* thisList,int index);
const char*(*GetType)(); //重点是这两个虚函数,待会我们在子类中重写他们
void (*Delete)(void* thislist);
}List;
List* new_List(int datasize); //构造函数单独使用,不放在成员函数里面,相当于类的静态函数
Bool List_Add(List* thisList,void* Obj,int datasize);
Bool List_Remove(List* thisList,void* Obj);
Iterator* List_GetIterator(List* thisList);
void* List_GetObjByIndex(List* thislist,int index);
const char* List_GetType();
void List_Delete(void* thislist);
头文件定义完毕后,在源文件中实现部分函数:
我们只看其中两个函数的实现,一个构造函数,一个待会会重写的函数
//List.cpp
//构造函数,分别一层一层往下申请空间,并给每个变量一个有意义的值。
List* new_List(int datasize) //ctor
{
List* list;
list=(List *)malloc(sizeof(List)); //申请空间
if(list==NULL){return NULL;}
Linklist* L;
L=(Linklist*)malloc(sizeof(Linklist));
L->pPrvNode=L->pNextNode=NULL;
list->_head=L;
list->_tail=L;
list->length=0;
list->datasize=datasize;
list->_iter=new_Iterator(L);
//这里实现了函数指针的指向,将结构体里面的函数指针指向实际实现的函数,就能使用了。
list->AddObj=List_Add;
list->Remove=List_Remove;
list->GetIterator=List_GetIterator;
list->GetObjByIndex=List_GetObjByIndex;
list->GetType=List_GetType;
list->Delete=List_Delete;
return list;
}
const char* List_GetType()
{
return "Type is List";
}
看完函数指针后大家应该想到了,如果要重写函数,不改变函数名,改变函数的指向即可。
接下来把所有的函数都实现完后,我们在main里面使用就是这样了:
//假如有个Data结构体
typedef struct _Data Data;
//main函数里面:
List* list=new_List(sizeof(Data));
Data data[10];
int i=0;
for(i=0;i<10;i++)
{
list->AddObj(list,&data[i],sizeof(Data)); //加入数据
}
printf("%s\n",list->GetType());
这时候控制台里面就会输出" Type is List"的字样
这里是不是有点像面向对象的语言了,唯一维和的地方就是不管什么函数都要传自己的指针,毕竟它有自己的缺陷。
下面开始实现继承和多态。
有了List类,我们能进一步在这个类上面做些什么呢,比如说,在List上面实现 栈 这种数据结构Stack.
我们想让Stack继承List, 让它有List的所有成员,并且决定是否重写函数,怎么样做到呢,很简单:
//Stack.h:
#include"List.h"
typedef struct _Stack{
List baseObj; //这个包含的“父类”一定要放在第一个
void*(*Pop)(Stack* thisStack);
Bool (*Push)(Stack* thisStack,void* obj,int datasize);
void (*Delete)( Stack* thisStack);
}Stack;。
通过在结构体中声明一个父类结构变量就能达到我们的期望,这个同样基于C语言指针的灵活性等下我们就能看到。
好了,不是一直在扯怎样实现多态吗,构造函数给你看,你就明白了:
Stack* new_Stack(int datasize)//ctor
{
Stack*stack=(Stack*)malloc(sizeof(Stack));
List* list=new_List(datasize);
list->GetType=Stack_GetType; //重点!!!将父类的函数指针指向新的函数,代表重写
stack->baseObj=*list;
//这里不能list->Delete(list)因为会删掉iter,而stack中的List中的*iter正指向它
free(list);
stack->Push=Stack_Push;
stack->Pop=Stack_Pop;
stack->Delete=Stack_Delete;
return stack;
}
而重写后函数为:
const char* Stack_GetType()
{
return "Type is Stack";
}
接下来就是模仿类型转换的时刻了,多态不是梦:
main函数中:
List* list=new_List(sizeof(Data));
List* slist= (List*)new_Stack(sizeof(Data)); //将stack强制转换为父类,注意这个技巧,结构体指针的强制转换。
printf("%s\n",list->GetType()); //用list类的gettype函数
printf"%s\n",slist->GetType()); //slist使用父类的接口.
可以看到,控制台中输出了两句不同的话:
Type is List
Type is Stack
明明都是 List 类型的指针,用同一个函数输出的结果却不一样,这就是让人着迷的多态。乍一看让人惊讶,C语言也能实现多态,但是上面
的讲解会让你明白,多态就是将函数指针指来指去,用同一个名字,身后却是不同的风景。
年,依然经久不衰。
我也只能这样跟风称赞几句,至于其中的深奥,实在不得领会,或者,根本不用领会。
进入正题,怎样用C语言来实现面向对象模式。为何有这个想法,是在看《设计模式初学者指南》中,序言中提到过,面向对象从来不是语言
,而是方法,在C语言时期,C++尚在酝酿当中,那时便有面向对象的“设计模式”,比如用一个虚函数指针表来维护虚函数等等。语言在发展
,模式就成了语法,条款就成了法律,情人就变成了老婆,世间万物无不如此。
面向对象的设计方法,首先得知道重点是什么,面向对象最重要的地方,我认为在于多态和数据隐藏,类,对象,继承重载等概念同样非常关
键,但是都基于前两位。
当然,在什么时候用合适的方法,函数式的设计从来没有消失过,反而愈发壮大。
为了满足好奇心,我百度花了不少时间,心里大概有个底,如何用C语言“实现”面向对象。至少要看起来像,这个也是最终的目的之一。
好了,要看起来像,用起来像java,c#,首先得有 类,这个可以交给C语言中的结构体,可惜的是结构体只能封装数据,不能封装函数,要说
例外,就是在结构体中声明函数指针,来达到看起来像“成员函数”的样子。
现在,我要实现一个“泛型”容器类 List, 让它用起来就像C#中的List<T>一样:
/**************** C# code:*****************************/
List<double> myList=new Lis<double>();
double data1;
myList.Add(data1);
为什么这里的泛型要打上引号,因为在C语言中无法主动检测类型,也就不能比较"Type1==Type2",无法区分两者的真正类型。
我所知道的方法实现部分泛型就是用void* 指针,void* 指针可以转换为任意的指针,这个作用和C#中Object的装箱拆箱别无二致。
这个容器采用什么样的数据结构呢,要能变长,随意增减大小,最好就是双链表了,否则两遍数据结构这课不是白学了吗.。
所以首先定义双链表的节点:
//List.h
typedef struct _Linklist{
Linklist* pPrvNode; //指向前一个节点
Linklist* pNextNode; //指向后一个节点
void *dataObj; //注意这里,每个NODE储存的是数据的引用--它的指针。由于我们不可能知道数据是什么样的,所以用void*
//代替,这是就是泛型
}Linklist;
然后我们的List容器,需要知道自己链表的首尾指针即可。
我们想想看,容器还要有哪些对外的特性?这决定了它的对外接口
通俗的说
1 要能添加数据 Add()
2要能按索引删除数据 Remove()
3要能通过索引得到数据 GetObjByIndexOf()
4要能返回一个迭代器 Iterator()
//迭代器是什么,javacode:
iter=List.Iterator();
while(iter.hasNext())
{
data= iter.next();
}
最后是所有类的两个函数 构造函数和析构函数。
通过上面的分析,我们能得到List类的大致的设计图,用代码描述出来
//List.h
typedef struct _List{
//"成员变量"
int length; // 数据的总个数
int datasize; //注意这里,这里保存一个数据的大小,是用来初步“类型检查”,如果加入的数据和之前的数据大小不一样,
//则说明类型不同,不让加入 ,仅此而已。
Linklist* _head; //链表的头指针
Linklist* _tail; //链表的尾指针
Iterator* _iter; //迭代器的指针
// “成员”函数,这里声明一些上函数指针。由于C语言函数不能获得之外的局部变量,所以我们给它参数中放上this指针,以便访问到对象
的//"数据"
Iterator* (*GetIterator)(List* thisList); //获得迭代器
Bool (*AddObj)(List* thisList,void* obj,int datasize);
Bool (*Remove)(List* thisList,void* obj);
void* (*GetObjByIndex)(List* thisList,int index);
const char*(*GetType)(); //重点是这两个虚函数,待会我们在子类中重写他们
void (*Delete)(void* thislist);
}List;
List* new_List(int datasize); //构造函数单独使用,不放在成员函数里面,相当于类的静态函数
Bool List_Add(List* thisList,void* Obj,int datasize);
Bool List_Remove(List* thisList,void* Obj);
Iterator* List_GetIterator(List* thisList);
void* List_GetObjByIndex(List* thislist,int index);
const char* List_GetType();
void List_Delete(void* thislist);
头文件定义完毕后,在源文件中实现部分函数:
我们只看其中两个函数的实现,一个构造函数,一个待会会重写的函数
//List.cpp
//构造函数,分别一层一层往下申请空间,并给每个变量一个有意义的值。
List* new_List(int datasize) //ctor
{
List* list;
list=(List *)malloc(sizeof(List)); //申请空间
if(list==NULL){return NULL;}
Linklist* L;
L=(Linklist*)malloc(sizeof(Linklist));
L->pPrvNode=L->pNextNode=NULL;
list->_head=L;
list->_tail=L;
list->length=0;
list->datasize=datasize;
list->_iter=new_Iterator(L);
//这里实现了函数指针的指向,将结构体里面的函数指针指向实际实现的函数,就能使用了。
list->AddObj=List_Add;
list->Remove=List_Remove;
list->GetIterator=List_GetIterator;
list->GetObjByIndex=List_GetObjByIndex;
list->GetType=List_GetType;
list->Delete=List_Delete;
return list;
}
const char* List_GetType()
{
return "Type is List";
}
看完函数指针后大家应该想到了,如果要重写函数,不改变函数名,改变函数的指向即可。
接下来把所有的函数都实现完后,我们在main里面使用就是这样了:
//假如有个Data结构体
typedef struct _Data Data;
//main函数里面:
List* list=new_List(sizeof(Data));
Data data[10];
int i=0;
for(i=0;i<10;i++)
{
list->AddObj(list,&data[i],sizeof(Data)); //加入数据
}
printf("%s\n",list->GetType());
这时候控制台里面就会输出" Type is List"的字样
这里是不是有点像面向对象的语言了,唯一维和的地方就是不管什么函数都要传自己的指针,毕竟它有自己的缺陷。
下面开始实现继承和多态。
有了List类,我们能进一步在这个类上面做些什么呢,比如说,在List上面实现 栈 这种数据结构Stack.
我们想让Stack继承List, 让它有List的所有成员,并且决定是否重写函数,怎么样做到呢,很简单:
//Stack.h:
#include"List.h"
typedef struct _Stack{
List baseObj; //这个包含的“父类”一定要放在第一个
void*(*Pop)(Stack* thisStack);
Bool (*Push)(Stack* thisStack,void* obj,int datasize);
void (*Delete)( Stack* thisStack);
}Stack;。
通过在结构体中声明一个父类结构变量就能达到我们的期望,这个同样基于C语言指针的灵活性等下我们就能看到。
好了,不是一直在扯怎样实现多态吗,构造函数给你看,你就明白了:
Stack* new_Stack(int datasize)//ctor
{
Stack*stack=(Stack*)malloc(sizeof(Stack));
List* list=new_List(datasize);
list->GetType=Stack_GetType; //重点!!!将父类的函数指针指向新的函数,代表重写
stack->baseObj=*list;
//这里不能list->Delete(list)因为会删掉iter,而stack中的List中的*iter正指向它
free(list);
stack->Push=Stack_Push;
stack->Pop=Stack_Pop;
stack->Delete=Stack_Delete;
return stack;
}
而重写后函数为:
const char* Stack_GetType()
{
return "Type is Stack";
}
接下来就是模仿类型转换的时刻了,多态不是梦:
main函数中:
List* list=new_List(sizeof(Data));
List* slist= (List*)new_Stack(sizeof(Data)); //将stack强制转换为父类,注意这个技巧,结构体指针的强制转换。
printf("%s\n",list->GetType()); //用list类的gettype函数
printf"%s\n",slist->GetType()); //slist使用父类的接口.
可以看到,控制台中输出了两句不同的话:
Type is List
Type is Stack
明明都是 List 类型的指针,用同一个函数输出的结果却不一样,这就是让人着迷的多态。乍一看让人惊讶,C语言也能实现多态,但是上面
的讲解会让你明白,多态就是将函数指针指来指去,用同一个名字,身后却是不同的风景。