C语言也能面向对象
C语言也能面向对象(一)——一个简单的类
面向对象不是C++, java, C#等的专利,万能的C语言一样可以面向对象编程。GObject系统已经用C语言实现了一套完整并且功能强大的面向对象系统。但GObject系统十分庞大,并依赖于众多的开发包,在项目本身很小,或者不想依赖于众多于GObject相关的开发包时,就无法使用了。
那么能不能用C语言实现一套小巧简单,易于理解,易于使用的面向对象系统呢?就让我们一起探索一下吧!
所谓类就是数据和方法的集合体。我们使用结构体定义类的数据,如下:
1: struct animal 2: { 3: char name[256]; 4: int weight; 5: };
我们定义了一个animal结构体,成员包括animal的name和animal的weight(重量)。
现在数据已经有了,那么方法如何定义呢?
我们知道在面向对象中有封装的概念,所以结构体中的成员我们不应该直接访问,而应该通过方法访问,我们添加访问结构体成员变量的方法:
1: char* animal_get_name(animal* self); 2: void animal_set_name(animal* self, char* name); 3: int animal_get_weight(animal* self); 4: void animal_set_weight(animal* self, int weight); 5: char* animal_get_name(animal* self) 6: { 7: return self->name; 8: } 9: 10: void animal_set_name(animal* self, char* name) 11: { 12: strncpy(self->name, name, sizeof(self->name)); 13: } 14: 15: int animal_get_weight(animal* self) 16: { 17: return self->weight; 18: } 19: 20: void animal_set_weight(animal* self, int weight) 21: { 22: self->weight = weight; 23: } 24:
方法的第一个参数animal* self是对象的地址,与C++中的this指针是同一个概念,只不过C++中的this指针是由编译器帮我们传递的,无需在代码中显式的写出来。
现在类的数据、方法都有了,那么如何使用类创建、初始化、析构、销毁对象呢?
在C++中有new、delete操作符负责创建和销毁,由构造函数和析构函数负责初始化和析构。这些在我们的C语言面向对象系统中都是要实现的,但对于我们的第一个类,我们可以实现的简单一些,如下:
1: animal* animal_create(void); 2: void animal_destroy(animal* self); 3: 4: animal* animal_create(void) 5: { 6: animal* self = malloc(sizeof(animal)); 7: LW_ASSERT(self); 8: memset(self->name, 0x00, sizeof(self->name)); 9: self->weight = 0; 10: return self; 11: } 12: 13: void animal_destroy(animal* self) 14: { 15: free(self); 16: } 17:
animal_create负责对象创建和初始化,animal_destroy负责对象析构和销毁。
到这里我们第一个类的定义就结束了。测试一下吧:
1: animal* animal1 = NULL; 2: animal* animal2 = NULL; 3: 4: animal1 = animal_create(); 5: animal_set_name(animal1, "Kitty"); 6: animal_set_weight(animal1, 30); 7: printf("animal1, name : %s, weight : %d\n", 8: animal_get_name(animal1), 9: animal_get_weight(animal1)); 10: animal_destroy(animal1); 11: 12: animal2 = animal_create(); 13: animal_set_name(animal2, "Bib"); 14: animal_set_weight(animal2, 10); 15: printf("animal2, name : %s, weight : %d\n", 16: animal_get_name(animal2), 17: animal_get_weight(animal2)); 18: animal_destroy(animal2); 19:
看看测试代码,是不是有些感觉了,其实这里面向对象还远着呢,勉强能称得上是基于对象吧。
相关代码可在此处下载
下一篇我们将介绍如何通过new和delete操作创建和销毁对象,通过构造函数和析构函数初始化和析构对象。
C语言也能面向对象(二)——new和delete
上篇我们介绍了如何使用结构体写一个简单的类animal,并使用animal* animal_create(void)和void animal_destroy(animal* self)来创建和删除animal类的对象。
在C++中创建和删除对象时使用的是new和delete操作符,并会自动调用类的构造函数和析构函数初始化和析构对象,那么使用C语言如何实现这样的机制呢?我们可以用函数模拟new和delete,定义两个函数:lw_new和lw_delete(“lw”意为light-weight,轻量级的意思)。
使用lw_new创建类的对象时,需要为对象申请内存空间,并且需要初始化该对象,所以lw_new函数至少需要知道该类的对象需要多大的内存空间,以及该类的构造函数是谁。我们可以将这些信息封装到一个结构体中,称之为类信息结构体,如下(以上篇介绍的animal类为例):
1 typedef struct _animal_klass_info animal_klass_info; 2 3 struct _animal_klass_info 4 5 { 6 7 size_t size; /*animal's size*/ 8 9 animal* (*ctor)(animal* self); /*constructor*/ 10 11 };
然后我们创建一个该结构体的对象(称之为类信息),将animal类的内存大小、构造函数等信息存入到对象中,再使用一个全局的指针指向该对象(称之为全局类信息指针),如下:
1 extern animal_klass_info* animal_klass; 2 3 static animal_klass_info local_animal_klass = 4 5 { 6 7 sizeof(animal), 8 9 animal_ctor, 10 11 }; 12 13 animal_klass_info* animal_klass = &local_animal_klass;
这样在创建animal类的对象时,我们只需要将animal类的全局类信息指针animal_klass传入到 lw_new函数中,lw_new函数就能够申请内存空间,并自动调用构造函数初始化对象了,lw_new的实现如下:
1 animal* lw_new(animal_klass_info* klass) 2 3 { 4 5 animal* p = ANIMAL(malloc(klass->size)); 6 7 return klass->ctor(p); 8 9 }
现在可以使用lw_new创建animal类的对象了,如下:
animal* animal1 = lw_new(animal_klass);
那么如何使用lw_delete函数删除对象,并自动调用析构函数呢?
在C++中删除对象时,传递给delete操作符的参数只有对象的地址,所以我们传递给lw_delete函数的也只能是对象的地址。使用对象地址我们可以释放对象占用的内存空间,但如何调用对象的析构函数呢?
我们可以将析构函数的地址加入到类信息结构体中,还是以animal类为例,将其类信息结构体修改为:
1 typedef struct _animal_klass_info animal_klass_info; 2 3 struct _animal_klass_info 4 5 { 6 7 size_t size; /*animal's size*/ 8 9 animal* (*ctor)(animal* self); /*constructor*/ 10 11 animal* (*dtor)(animal* self); /*destructor*/ 12 13 };
在创建类信息时加入析构函数的地址,如下:
1 static animal_klass_info local_animal_klass = 2 3 { 4 5 sizeof(animal), 6 7 animal_ctor, 8 9 animal_dtor, 10 11 };
然后在animal类中加入一个类信息结构体的指针,用来保存animal类信息的地址。修改animal类定义如下:
1 typedef struct _animal animal; 2 3 struct _animal 4 5 { 6 7 /*klass info*/ 8 9 animal_klass_info* klass; 10 11 12 13 /*private data*/ 14 15 char name[256]; 16 17 int weight; 18 19 };
在lw_new函数中初始化animal类的类信息结构体指针,如下:
1 animal* lw_new(animal_klass_info* klass) 2 3 { 4 5 animal* p = ANIMAL(malloc(klass->size)); 6 7 p->klass = klass; 8 9 return klass->ctor(p); 10 11 }
这样lw_delete函数就能够根据传入的对象的地址,取得相应的类信息地址,进而取得析构函数了,lw_delete实现如下:
1 void lw_delete(animal* self) 2 3 { 4 5 if(self) 6 7 { 8 9 free(self->klass->dtor(self)); 10 11 } 12 13 }
至此,lw_new和lw_delete都已经实现了,我们测试一下吧.
1 int main(int argc, char* argv) 2 3 { 4 5 argc; 6 7 argv; 8 9 10 11 animal* animal1 = NULL; 12 13 animal* animal2 = NULL; 14 15 16 17 animal1 = lw_new(animal_klass); 18 19 animal_set_name(animal1, "Kitty"); 20 21 animal_set_weight(animal1, 30); 22 23 printf("animal1, name : %s, weight : %d\n", 24 25 animal_get_name(animal1), 26 27 animal_get_weight(animal1)); 28 29 lw_delete(animal1); 30 31 32 33 animal2 = lw_new(animal_klass); 34 35 animal_set_name(animal2, "Bib"); 36 37 animal_set_weight(animal2, 10); 38 39 printf("animal2, name : %s, weight : %d\n", 40 41 animal_get_name(animal2), 42 43 animal_get_weight(animal2)); 44 45 lw_delete(animal2); 46 47 return 0; 48 49 }
完整的代码可以在此处下载。
大家可以看出我们目前实现的lw_new和lw_delete只能用于创建和删除animal类的对象。如何实现通用的lw_new和lw_delete呢?这就是下篇我们将要介绍的内容。
C语言也能面向对象(三)——通用的new和delete
上篇中实现的lw_new和lw_delete函数只能创建和删除animal类的对象,这当然满足不了我们的需要,那么如何实现通用的lw_new和lw_delete函数,用来创建和删除任何类的对象呢?
考虑新定义一个类car,其属性有车标logo和颜色color:
1 typedef struct _car car; 2 3 struct _car 4 5 { 6 7 /*private data*/ 8 9 char logo[256]; 10 11 int color; 12 13 };
成员函数:
1 char* car_get_logo(car* self); 2 3 void car_set_logo(car* self, char* logo); 4 5 int car_get_color(car* self); 6 7 void car_set_color(car* self, int color);
使用lw_new创建car类对象时,必须知道car类的对象占用内存空间大小和构造函数,以便为对象申请内存空间,并初始化该对象,而使用lw_delete函数删除car类的对象时,就必须知道car类的析构函数,以便先析构该对象,然后再释放对象占用的内存空间。可以看出,创建和删除car类的对象所需要的三个必备条件就是:类的对象占用内存空间大小,构造函数和析构函数,与创建和删除animal类时所需的条件一致。那么我们是不是可以创建一个统一的类信息结构体,然后使用结构体的不同对象,保存不同类的类信息呢?
上篇中定义的animal类的类信息结构体如下:
1 struct _animal_klass_info 2 3 { 4 5 animal_klass_info* super; /*animal's super's klass info*/ 6 7 char* name; /*animal klass's name*/ 8 9 size_t size; /*animal's size*/ 10 11 animal* (*ctor)(animal* self); /*constructor*/ 12 13 animal* (*dtor)(animal* self); /*destructor*/ 14 15 };
super为父类的类信息地址,无父类,则其值为NULL,name是该类的名称,这两个成员先不考虑,我们着重考虑size、ctor和dtor这三个成员,看看是否能够将这个animal专用的类信息结构体,改造成统一的类信息结构体。size为类对象所占内存空间大小,类型为size_t,这对任何类都是通用的,所以无需变更。ctor和dtor都是函数指针类型,传入参数为animal对象的地址,在将animal对象初始化或析构后,再将该地址返回。既然要适用于任意类,在类信息结构体中就不能使用animal类的指针了,我们将结构体中这两个函数的参数和返回值类型弱化为void*类型,如下:
1 typedef void* (*voidf)(void*); 2 3 voidf ctor; 4 5 voidf dtor;
注意修改的只是类信息结构体中的函数类型,在实现animal类的构造函数和析构函数时,仍然可以直接使用animal类的指针类型,这样在初始化类信息时,编译器会警告类型不兼容,为了避免警告,我们将类信息结构体中的构造函数和析构函数指针弱化为void*类型,最终类信息结构体的定义为:
1 typedef struct _klass_info klass_info; 2 3 struct _klass_info 4 5 { 6 7 klass_info* super; /*object's klass's super klass*/ 8 9 char* name; /*object's klass's name*/ 10 11 size_t size; /*object's size*/ 12 13 void* ctor; /*object's constructor*/ 14 15 void* dtor; /*object's destructor*/ 16 17 };
lw_new和lw_delete函数的实现也要响应修改,如下:
1 void* lw_new(void* klass) 2 3 { 4 5 klass_info* kls = KLASS(klass); 6 7 void* p = malloc(kls->size); 8 9 *((klass_info**)p) = kls; 10 11 return ((voidf)(kls->ctor))(p); 12 13 } 14 15 void lw_delete(void* self) 16 17 { 18 19 if(self) 20 21 { 22 23 klass_info* kls = *((klass_info**)self); 24 25 free(((voidf)(kls->dtor))(self)); 26 27 } 28 29 }
为了避免警告,lw_new和lw_delete函数的传入参数类型也弱化为void*类型。klass_info* kls = KLASS(klass)中使用了KLASS()宏,定义如下:
#define KLASS(_klass_info_) ((klass_info*)_klass_info_)
用来将其他指针强制转换为klass_info*类型。
我们以car类为例,看看如何使用klass_info类信息结构体定义类。
1 car.h 2 3 extern klass_info* car_klass; 4 5 6 7 #define CAR(_object_) ((car*)_object_) 8 9 10 11 typedef struct _car car; 12 13 struct _car 14 15 { 16 17 /*class info*/ 18 19 klass_info* klass; 20 21 22 23 /*private data*/ 24 25 char logo[256]; 26 27 int color; 28 29 }; 30 31 32 33 char* car_get_logo(car* self); 34 35 void car_set_logo(car* self, char* logo); 36 37 int car_get_color(car* self); 38 39 void car_set_color(car* self, int color); 40 41 car.c 42 43 #include "car.h" 44 45 46 47 static car* car_ctor(car* self); 48 49 static car* car_dtor(car* self); 50 51 52 53 static klass_info local_car_klass = 54 55 { 56 57 NULL, 58 59 "car_klass", 60 61 sizeof(car), 62 63 car_ctor, 64 65 car_dtor, 66 67 }; 68 69 klass_info* car_klass = &local_car_klass; 70 71 72 73 static car* car_ctor(car* self) 74 75 { 76 77 memset(self->logo, 0x00, sizeof(self->logo)); 78 79 self->color = 0; 80 81 } 82 83 84 85 static car* car_dtor(car* self) 86 87 { 88 89 return self; 90 91 } 92 93 94 95 char* car_get_logo(car* self) 96 97 { 98 99 return self->logo; 100 101 } 102 103 104 105 void car_set_logo(car* self, char* logo) 106 107 { 108 109 strncpy(self->logo, logo, sizeof(self->logo)); 110 111 } 112 113 114 115 int car_get_color(car* self) 116 117 { 118 119 return self->color; 120 121 } 122 123 124 125 void car_set_color(car* self, int color) 126 127 { 128 129 self->color = color; 130 131 }
下面测试一下:
1 int main(int argc, char* argv) 2 3 { 4 5 argc; 6 7 argv; 8 9 10 11 animal* animal1 = NULL; 12 13 animal* animal2 = NULL; 14 15 car* car1 = NULL; 16 17 car* car2 = NULL; 18 19 20 21 animal1 = lw_new(animal_klass); 22 23 animal_set_name(animal1, "Kitty"); 24 25 animal_set_weight(animal1, 30); 26 27 printf("animal1, name : %s, weight : %d\n", 28 29 animal_get_name(animal1), 30 31 animal_get_weight(animal1)); 32 33 lw_delete(animal1); 34 35 36 37 animal2 = lw_new(animal_klass); 38 39 animal_set_name(animal2, "Bib"); 40 41 animal_set_weight(animal2, 10); 42 43 printf("animal2, name : %s, weight : %d\n", 44 45 animal_get_name(animal2), 46 47 animal_get_weight(animal2)); 48 49 lw_delete(animal2); 50 51 52 53 car1 = lw_new(car_klass); 54 55 car_set_logo(car1, "Honda"); 56 57 car_set_color(car1, 0); 58 59 printf("car1, logo : %s, color : %d\n", 60 61 car_get_logo(car1), 62 63 car_get_color(car1)); 64 65 lw_delete(car1); 66 67 68 69 car2 = lw_new(car_klass); 70 71 car_set_logo(car2, "BMW"); 72 73 car_set_color(car2, 1); 74 75 printf("car2, logo : %s, color : %d\n", 76 77 car_get_logo(car2), 78 79 car_get_color(car2)); 80 81 lw_delete(car2); 82 83 84 85 return 0; 86 87 }
到这里我们已经能够使用lw_new和lw_delete函数创建和删除任何类的对象,并且自动调用其构造函数和析构函数了,本篇文章相关代码可以到这里下载。
下篇文章我们将讨论继承。
C语言也能面向对象(四)(继承)
在C++中如果一个类有父类,那么这个类的对象中就包含了父类中定义的数据,并且可以使用父类的函数访问或操作该这些数据。在C中如何实现这样的机制呢?
animal类的定义如下:
1 typedef struct _animal animal; 2 3 struct _animal 4 5 { 6 7 /*class info*/ 8 9 klass_info* klass; 10 11 12 13 /*private data*/ 14 15 char name[256]; 16 17 int weight; 18 19 };
现在我们再定义一个dog类,除了包含animal类的属性外,还包括一个age属性,即年龄。如下:
1 typedef struct _dog dog ; 2 3 struct _dog 4 5 { 6 7 /*class info*/ 8 9 klass_info* klass; 10 11 12 13 /*private data*/ 14 15 char name[256]; 16 17 int weight; 18 19 int age; 20 21 };
在保持类的对象的内存布局不变的情况下,我们可以将dog类的定义变换为:
1 typedef struct _dog dog; 2 3 struct _dog 4 5 { 6 7 /*base*/ 8 9 animal base; 10 11 12 13 /*private data*/ 14 15 int age; 16 17 };
两个dog类的定义在内存布局上是完全一致的。因此,只要将dog类对象的指针强制转型为animal类的指针,那么animal类的成员函数就可以访问或操作 dog类对象了,如下:
1 dog* my_dog = lw_new(dog_klass); 2 3 animal_set_weight(ANIMAL(my_dog), 40); 4 5 int weight = animal_get_weight(ANIMAL(my_dog)); 6 7 lw_delete(my_dog);
在C++中创建子类的对象时,要先调用父类的构造函数,然后再调用子类的函数;删除子类对象时,要先调用子类的析构函数,然后再调用父类的析构函数。
animal类是dog类的父类,在定义dog类信息时,要将dog类信息中的super属性初始化为animal类信息的地址,如下:
1 static klass_info local_dog_klass = 2 3 { 4 5 animal_klass, 6 7 "dog_klass", 8 9 sizeof(dog), 10 11 dog_ctor, 12 13 dog_dtor, 14 15 };
在实现dog的构造函数时,就可以使用类信息的super成员获取其父类的构造函数地址,如下:
1 static dog* dog_ctor(dog* self) 2 3 { 4 5 ((voidf)(klass_of(self)->super->ctor))(self); 6 7 self->age = 0; 8 9 return self; 10 11 }
klass_of函数的功能是获取对象的类信息地址。可以看出dog类的构造函数是先调用父类的构造函数,然后再初始化子类的属性。类似,析构函数的实现如下:
1 static dog* dog_dtor(dog* self) 2 3 { 4 5 ((voidf)(klass_of(self)->super->dtor))(self); 6 7 return self; 8 9 }
目前为止,我们都是使用结构体初始化的方式来初始化类信息,这种方式有代码重复、容易犯错,难于维护的缺点,例如,animal类信息的初始化在animal.c中已经实现过,定义dog类时在dog.c中又要再次实现,如果再定义其他继承自animal类的子类,则还要实现。按顺序初始化结构体这种做法本身就很容易犯错,如果这样的代码到处都是,那么维护难度就可想而知了。
所以我们考虑使用类信息初始化函数来初始化类信息,即专门定义一个函数用来初始化类信息,该函数在程序运行后,第一次创建该类的对象时由lw_new函数调用。在类信息结构体中新加入一个属性,即类信息初始化函数的地址,如下:
1 typedef struct _klass_info klass_info; 2 3 struct _klass_info 4 5 { 6 7 void* init; /*initialize function*/ 8 9 klass_info* super; /*object's klass's super klass*/ 10 11 char* name; /*object's klass's name*/ 12 13 size_t size; /*object's size*/ 14 15 void* ctor; /*object's constructor*/ 16 17 void* dtor; /*object's destructor*/ 18 19 };
init属性在定义静态类信息结构体对象时初始化,其他属性在init指向的函数中初始化,以animal类为例:
1 static animal_klass_info local_animal_klass = {animal_init}; 2 3 animal_klass_info* animal_klass = &local_animal_klass; 4 5 6 7 void animal_init(void) 8 9 { 10 11 if(animal_klass->init) 12 13 { 14 15 animal_klass->init = NULL; 16 17 animal_klass->super = NULL; 18 19 animal_klass->name = "animal_klass"; 20 21 animal_klass->size = sizeof(animal); 22 23 animal_klass->ctor = animal_ctor; 24 25 animal_klass->dtor = animal_dtor; 26 27 } 28 29 }
lw_new函数修改如下:
1 void* lw_new(void* klass) 2 3 { 4 5 klass_info* kls = KLASS(klass); 6 7 if(kls->init) 8 9 { 10 11 ((init_fun)kls->init)(); 12 13 } 14 15 16 17 void* p = malloc(kls->size); 18 19 *((klass_info**)p) = kls; 20 21 return ((voidf)(kls->ctor))(p); 22 23 }
类信息初始化函数中会将类信息结构体的init属性置空,所以该函数只会被调用一次,即第一次创建该类的对象时调用。
子类的类信息初始化函数可以直接调用父类的类信息初始化函数,以dog类为例:
1 void dog_init(void) 2 3 { 4 5 if(dog_klass->init) 6 7 { 8 9 animal_init(); 10 11 memcpy(dog_klass, animal_klass, sizeof(animal_klass_info)); 12 13 dog_klass->super = animal_klass; 14 15 dog_klass->name = "dog_klass"; 16 17 dog_klass->size = sizeof(dog); 18 19 dog_klass->ctor = dog_ctor; 20 21 } 22 23 }
至此我们实现了类继承,并使用类信息初始化函数来初始化类信息。
这里是相关代码。
下篇文章我们将讨论多态。
C语言中的面向对象(1)-类模拟和多态,继续
在面向对象的语言里面,出现了类的概念。这是编程思想的一种进化。所谓类:是对特定数据的特定操作的集合体。所以说类包含了两个范畴:数据和操作。而视频教程'>c语言中的struct仅仅是数据的集合。(liyuming1978@163.com)
1.实例:下面先从一个小例子看起
1 #ifndef C_Class 2 3 #define C_Class struct 4 5 #endif 6 7 8 9 C_Class A { 10 11 C_Class A *A_this; 12 13 void (*Foo)(C_Class A *A_this); 14 15 int a; 16 17 int b; 18 19 }; 20 21 22 23 C_Class B{ //B继续了A 24 25 C_Class B *B_this; //顺序很重要 26 27 void (*Foo)(C_Class B *Bthis); //虚函数 28 29 int a; 30 31 int b; 32 33 34 35 int c; 36 37 }; 38 39 40 41 42 43 void B_F2(C_Class B *Bthis) 44 45 { 46 47 printf("It is B_Fun\n"); 48 49 } 50 51 52 53 void A_Foo(C_Class A *Athis) 54 55 { 56 57 printf("It is A.a=%d\n",Athis->a);//或者这里 58 59 // exit(1); 60 61 // printf("纯虚 不答应执行\n");//或者这里 62 63 } 64 65 66 67 void B_Foo(C_Class B *Bthis) 68 69 { 70 71 printf("It is B.c=%d\n",Bthis->c); 72 73 } 74 75 76 77 void A_Creat(struct A* p) 78 79 { 80 81 p->Foo=A_Foo; 82 83 p->a=1; 84 85 p->b=2; 86 87 p->A_this=p; 88 89 } 90 91 92 93 94 95 void B_Creat(struct B* p) 96 97 { 98 99 p->Foo=B_Foo; 100 101 p->a=11; 102 103 p->b=12; 104 105 p->c=13; 106 107 p->B_this=p; 108 109 } 110 111 112 113 114 115 int main(int argc, char* argv[]) 116 117 { 118 119 C_Class A *ma,a; 120 121 C_Class B *mb,b; 122 123 124 125 A_Creat(&a);//实例化 126 127 B_Creat(&b); 128 129 130 131 mb=&b; 132 133 ma=&a; 134 135 136 137 ma=(C_Class A*)mb;//引入多态指针 138 139 printf("%d\n",ma->a);//可惜的就是 函数变量没有private 140 141 ma->Foo(ma);//多态 142 143 a.Foo(&a);//不是多态了 144 145 B_F2(&b);//成员函数,因为效率问题不使用函数指针 146 147 return 0; 148 149 } 150 151
输出结果:
1 11 2 3 It is B.c=13 4 5 It is A.a=1 6 7 It is B_Fun
C语言中的面向对象(2)-C语言的多态实现
相信很多人都看过设计模式方面的书,大家有什么体会呢?Bridge,Proxy,Factory这些设计模式都是基于抽象类的。使用抽象对象是这里的一个核心。
其实我觉得框架化编程的一个核心问题是抽象,用抽象的对象构建程序的主体框架,这是面向对象编程的普遍思想。用抽象构建骨架,再加上多态就形成了一个完整的程序。由于C++语言本身实现了继续和多态,使用这样的编程理念(理念啥意思?跟个风,嘿嘿)在C++中是十分普遍的现象,可以说Virtual(多态)是VC的灵魂。
但是,使用视频教程'>c语言的我们都快把这个多态忘光光了。我常听见前辈说,类?多态?我们用的是C,把这些忘了吧。很不幸的是,我是一个固执的人。这么好的东西,为啥不用呢。很兴奋的,在最近的一些纯C代码中,我看见了C中的多态!下面且听我慢慢道来。
1. VC中的Interface是什么
Interface:中文解释是接口,其实它表示的是一个纯虚类。不过我所要说的是,在VC中的Interface其实就是struct,查找Interface的定义,你可以发现有这样的宏定义:
1 #Ifndef Interface 2 3 #define Interface struct 4 5 #endif
而且,实际上在VC中,假如一个类有Virtual的函数,则类里面会有vtable,它实际上是一个虚函数列表。实际上C++是从C发展而来的,它不过是在语言级别上支持了很多新功能,在C语言中,我们也可以使用这样的功能,前提是我们不得不自己实现。
2.C中如何实现纯虚类(我称它为纯虚结构)
比较前面,相信大家已经豁然开朗了。使用struct组合函数指针就可以实现纯虚类。
例子:
1 typedef struct { 2 3 void (*Foo1)(); 4 5 char (*Foo2)(); 6 7 char* (*Foo3)(char* st); 8 9 } 10 11 MyVirtualInterface;
这样假设我们在主体框架中要使用桥模式。(我们的主类是DoMyAct,接口具体实现类是Act1,Act2)下面我将依次介绍这些“类”。(C中的“类”在前面有说明,这里换了一个,是使用早期的数组的办法)
主类DoMyAct: 主类中含有MyVirtualInterface* m_pInterface; 主类有下函数:
1 DoMyAct_SetInterface(MyVirtualInterface* pInterface) 2 3 { 4 5 m_pInterface= pInterface; 6 7 } 8 9 DoMyAct_Do() 10 11 { 12 13 if(m_pInterface==NULL) return; 14 15 m_pInterface->Foo1(); 16 17 c=m_pInterface->Foo2(); 18 19 } 20 21
子类Act1:实现虚结构,含有MyVirtualInterface st[MAX]; 有以下函数:
1 MyVirtualInterface* Act1_CreatInterface() 2 3 { 4 5 index=FindValid() //对象池或者使用Malloc !应该留在外面申请,实例化 6 7 if(index==-1) return NULL; 8 9 St[index].Foo1=Act1_Foo1; // Act1_Foo1要在下面具体实现 10 11 St[index].Foo2=Act1_Foo2; 12 13 St[index].Foo3=Act1_Foo3; 14 15 Return &st [index]; 16 17 }
子类Act2同上。
在main中,假设有一个对象List。List中存贮的是MyVirtualInterface指针,则有:
1 if( (p= Act1_CreatInterface()) != NULL) 2 3 List_AddObject(&List, p); //Add All 4 5 6 7 While(p=List_GetObject()){ 8 9 DoMyAct_SetInterface(p);//使用Interface代替了原来大篇幅的Switch Case 10 11 DoMyAct_Do();//不要理会具体的什么样的动作,just do it 12 13 } 14 15 16 17 FREE ALL
C语言中的面向对象(3)-类模拟的性能分析
类模拟中使用了大量的函数指针,结构体等等,有必须对此进行性能分析,以便观察这样的结构对程序的整体性能有什么程度的影响。
1.函数调用的开销
1 #define COUNTER XX 2 3 void testfunc() 4 5 { 6 7 int i,k=0; 8 9 for(i=0;i<YY;i++) 10 11 }
在测试程序里面,我们使用的是一个测试函数,函数体内部可以通过改变YY的值来改变函数的耗时。测试对比是 循环调用XX次函数,和循环XX次函数内部的YY循环。
结果发现,在YY足够小,X足够大的情况下,函数调用耗时成为了主要原因。所以当一个“简单”功能需要“反复”调用的时候,将它编写为函数将会对性能有影响。这个时候可以使用宏,或者inline要害字。
但是,实际上我设置XX=10000000(1千万)的时候,才出现ms级别的耗时,对于非实时操作(UI等等),即使是很慢的cpu(嵌入式10M级别的),也只会在XX=10万的时候出现短暂的函数调用耗时,所以实际上这个是可以忽略的。
2.普通函数调用和函数指针调用的开销
1 void (*tf)(); 2 3 tf=testfunc;
测试程序修改为一个使用函数调用,一个使用函数指针调用。测试发现对时间基本没有什么影响。(在第一次编写的时候,发现在函数调用出现耗时的情况下(XX=1亿),函数指针的调用要慢(release版本),调用耗时350:500。后来才发现这个影响是由于将变量申请为全局的原因,全局变量的访问要比局部变量慢很多)。
3.函数指针和指针结构访问的开销
1 struct a { 2 3 void (*tf)(); 4 5 }
测试程序修改为使用结构的函数指针,测试发现对时间基本没有什么影响。其实使用结构并不会产生影响,因为结构的访问是固定偏移量的。所以结构变量的访问和普通变量的访问对于机器码来说是一样的。
C语言中的面向对象(4)-面向对象思想
经常听见别人说面向对象的程序设计,以前在学校上课的时候,也有开面向对象程序设计这门课。可是不幸的是,这些都是以C++,甚至VC++为基础的。而更加不幸的是,多年以来我一直是一个C的使用者。在学校的时候,我主要做的是硬件上的驱动层,和底层功能层。在工作以后,又做的是手机上的软件开发,所有这些都是和C离不开的。虽然我不得不说,C++是一门很好的语言,但是它的编译速度,代码效率,编译后的代码大小都限制了它在嵌入式上的应用。(但现在的嵌入式CPU越来越快,内存容量变大。我觉得用C++也应该没有什么问题。这使我觉得似乎是嵌入式编译器的限制。虽然菲利普和TI似乎都有C++的编译器,但是似乎没人用这个。难道是太贵了? 但不管怎么说,嵌入式应用中,视频教程'>c语言的普遍使用是肯定的)
那么在面向过程的时代产生的C语言能否使用面向对象的思想呢?我认为是肯定可以的,C++不过是在语言级别上加入了对对象的支持,同时提供了丰富的对象库。而在C语言下,我们只好自力更生了。
一、 面向对象思想的目的是框架化,手段是抽象
相信很多人都明白面向对象讲了什么:类,抽象类,继续,多态。但是是什么原因促使这些概念的产生呢?
打个比方说:你去买显示器,然而显示器的品牌样式是多种多样的,你在买的过程中发生的事情也是不可猜测的。对于这样的事情,我们在程序语言中如何去描述呢。面向对象的思想就是为了解决这样的问题。编写一个程序(甚至说是一个工程),从无到用是困难的,从有到丰富是更加困难的。面向对象将程序的各个行为化为对象,而又用抽象的办法将这些对象归类(抽象),从而将错综复杂的事情简化为几个主要的有机组合(框架化)。
其实我们的身边很多东西都是这样组成的:比如说电脑:电脑是由主板,CPU加上各种卡组成的。这就是一个框架化。而忽略不同的CPU,不同的主板,不同的声卡,网卡,显卡的区别,这就是抽象。再比如说现在的教育网:是由主核心节点:清华,北大,北邮等几个,然后是各个子节点,依次组成了整个教育网网络。
所以我觉得面向对象的编程思想就是:一个大型工程是分层次结构的,每层又由抽象的结构连接为整体(框架化),各个抽象结构之间是彼此独立的,可以独立进化(继续,多态)。层次之间,结构之间各有统一的通讯方式(通常是消息,事件机制)。
二、 以前 C 语言编程中常用的“面向对象”方法
其实C语言诞生以来,人们就想了很多办法来体现“面向对象”的思想。下面就来说说我所知道的方法。先说一些大家熟悉的东东,慢慢再讲诡异的。呵呵
1. 宏定义:
有的人不禁要问,宏定义怎么扯到这里来了,我们可以先看一个简单的例子:
1 #define MacroFunction Afunction
然后在程序里面你调用了大量的AFunction,但是有一天,你忽然发现你要用BFunction了,(不过AFunction又不能不要,很有可能你以后还要调用),这个时候,你就可以#define MacroFunction Bfunction来达到这样的目的。
当然,不得不说这样的办法是too simple,sometime naïve的,因为一个很滑稽的问题是假如我一般要改为BFunction,一半不变怎么办? 那就只好查找替换了。
2. 静态的入口函数,保证函数名相同,利用标志位调用子函数:
这样的典型应用很多,比如说网卡驱动里面有一个入口函数Nilan(int FunctionCode,Para*)。具体的参数是什么记不清楚了。不过NiLan的主体是这样的:
1 Long Nilan(int FunctionCode,Para*){ 2 3 4 5 Switch(FunctionCode){ 6 7 8 9 Case SendPacket: send(….) 10 11 12 13 Case ReceivePacket: receive(…) 14 15 16 17 ….. 18 19 20 21 }
写到这里大家明白什么意思了吧。保证相同的函数名就是说:网卡驱动是和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优化之路,现在都快成垃圾信箱了,呵呵,网络的力量真是强大呀)
浙公网安备 33010602011771号