引言
最近尝试看了下《python源码剖析》,看完python
的对象模型后,发现python
是使用C
语言实现了对象和多态,感觉大涨见识,原来C
语言也是能够实现面向对象的编程的。为了加深理解,去图书馆翻了翻,还真的被我找到了本书,《C现代编程》,本文笔记都是出自第三章,这一章举得例子是数据结构中栈的实现,讲述是如何一步步的从经典栈的写法转到面向对象的写法。主要实现技巧是c语言中的函数指针。
栈的经典实现
// stack.h 代码
#ifndef _STACK_H_
#define _STACK_H_
#ifdef __cplusplus
extern "C" {
#endif
bool push(int val);
bool pop(int *pRet);
#ifdef __cplusplus
}
#endif
#endif
#include <stdbool.h>
#include "stack.h"
static int buf[16];
static int top = 0;
static bool isStackFull(void) {
return top == sizeof(buf) / sizeof(int);
}
static bool isStackEmpty(void) {
return top == 0;
}
// true: 成功, false: 失敗
bool push(int val) {
if (isStackFull()) return false;
buf[top++] = val;
return true;
}
// true: 成功, false: 失敗
bool pop(int *pRet) {
if (isStackEmpty()) return false;
*pRet = buf[--top];
return true;
}
这是栈的c
语言的经典实现方法,数据和函数实现都放在.c
文件中,函数声明放在.h
中。
- 为什么变量和某些函数要使用
static
关键字修饰:不加static
关键字时,那几个变量和函数都在全局命名空间中,如果在其他的.c
文件中使用了同样的变量或者函数名,就会发生冲突,如果加了static
关键字修饰,则修饰的变量和函数都只在该文件中有效,可有效避免名字冲突。 -
__cplusplus
是什么:使用c++编译器编译程序的时候,会自动定义这个宏。 -
extern "c"
是什么:在使用c++编译器编译.c
文件时,需要加上这个语句才能编译成功。c编译后和c++编译后,函数被赋予的名字不同,调用时栈中保存的参数的顺序也不同,因此c++无法调用c函数,c亦无法调用c++函数,extern "c"
就是告诉编译器这是c函数。使用__cplusplus
宏和extern "c"
是为了使得不管是在c++中还是在c中使用这些代码都能编译。
使用结构体将数据结构与代码块分离
问题升级:前一种实现中只能有一个栈,如果想实现多个栈该怎么做。
-
方法一:编写两个名字不同,功能相同的函数
static int buf2[16]; static int top2 = 0; // true: 成功, false: 失敗 bool push2(int val) { ... } // true: 成功, false: 失敗 bool pop2(int *pRet) { ... }
这种做法比较累赘,写了很多重复的代码。
-
方法二:使用结构体将栈的数据整合在一起
// stack.h 代码 #ifndef _STACK_H_ #define _STACK_H_ #include <stddef.h> #ifdef __cplusplus extern "C" { #endif typedef struct { int top; const size_t size; int * const pBuf; } Stack; bool push(Stack *p, int val); bool pop(Stack *p, int *pRet); #define newStack(buf) { \ 0, sizeof(buf) / sizeof(int), (buf) \ } #ifdef __cplusplus } #endif #endif
// stack.c 代码 #include <stdbool.h> #include "stack.h" static bool isStackFull(const Stack *p) { return p->top == p->size; } static bool isStackEmpty(const Stack *p) { return p->top == 0; } // true: 成功, false: 失敗 bool push(Stack *p, int val) { if (isStackFull(p)) return false; p->pBuf[p->top++] = val; return true; } // true: 成功, false: 失敗 bool pop(Stack *p, int *pRet) { if (isStackEmpty(p)) return false; *pRet = p->pBuf[--p->top]; return true; }
两个学习的地方:
-
使用结构体将数据封装,实现不同的栈,只需要创造不同的结构体实例,
Stack stack1;
,Stack stack2
。 -
使用宏定义来讲结构体初始化。
一般初始化方法:
int buf[16]; Stack stack = {0, sizeof(buf)/sizeof(int), (buf)};
定义了宏之后:
//#define newStack(buf) { \ // 0, sizeof(buf) / sizeof(int), (buf) \ //} int buf[16]; Stack stack = newStack(buf);
使用宏初始化,更加简洁明了。当然写宏的时候,有很多需要注意的地方,暂不讨论。
-
使用C进行面向对象编程
带有检查功能的栈
问题升级了:如果要求栈中保存的值只能在一定范围内,例如只能将[0,9]范围内的值push
至栈中。
初步想法,可以再编写一个函数包装push
函数:
bool pushWithRangeCheck(Stack *p, int val, int min, int max){
if (val < min || max <val) return false;
return push(p, val);
}
但是这种做法传递的参数太多,而且每次push的时候都要传递最大值和最小值,很麻烦。这两个字应该是在创建栈的时候就确定的,而不是每次调用函数的时候再设置,所以说,这两个值应该是属于栈这个结构体的。改进如下:
// stack.h 部分代码
typedef struct {
int top;
const size_t size;
int * const pBuf;
const bool needRangeCheck;
const int min;
const int max;
} Stack;
#define newStack(buf) { \
0, sizeof(buf) / sizeof(int), (buf), \
false, 0, 0 \
}
#define newStackWithRangeCheck(buf, min, max) { \
0, sizeof(buf) / sizeof(int), (buf), \
true, min, max \
}
static bool isRangeOk(const Stack *p, int val) {
return ! p->needRangeCheck ||
(p->min <= val && val <= p->max);
}
// true: 成功, false: 失敗
bool push(Stack *p, int val) {
if (! isRangeOk(p, val) || isStackFull(p)) return false;
p->pBuf[p->top++] = val;
return true;
}
上面主要增加了:将结构体设计的更加完善,增加了范围检查函数,增加并修改了结构体的初始化宏。但是这种做法依旧存在着若干问题:
- 即使生成的栈是不带范围检查功能的,栈结构体体也要保存
needRangeCheck
、min
和max
等多余的成员,浪费内存。 - 如果还想增加其他校验功能,就必须增加其他成员。这样就必须在结构体内保存所有检查功能的成员,
push
函数也会越来越臃肿和复杂。
将范围检查分离出去
为了解决第一个问题,先将范围检查分离出去:
// stack.h 部分代码
typedef struct {
const int min;
const int max;
} Range;
typedef struct {
int top;
const size_t size;
int * const pBuf;
const Range* const pRange;
} Stack;
#define newStack(buf) { \
0, sizeof(buf) / sizeof(int), (buf), \
NULL \
}
#define newStackWithRangeCheck(buf, pRange) { \
0, sizeof(buf) / sizeof(int), (buf), \
pRange \
}
// stack.c 部分代码
static bool isRangeOk(const Stack *p, int val) {
return p == NULL ||
(p->min <= val && val <= p->max);
}
bool push(Stack *p, int val) {
if (!isRangeOk(p->pRange, val) || isStackFull(p)) return false;
p->pBuf[p->top++] = val;
return true;
}
以上将范围检查所必需的数据结构移动到结构体Range
中,并在isRangeOk
函数中使用Range
进行判断,这样就可以将保存栈数据的Stack
与保存范围检查数据的Range
分离开来。
检查功能的通用化
现在解决第二个问题,如何扩展新的检查功能,例如,要求每次push
到栈中的值都必须比上次的值(默认第一个值和0比)大,之前的设计并不能继续扩展这种需求,所以需要将检查功能设计的更加通用化。
首先,将检查输入值的通用指责转移到Validator
结构体中。
// stack.c stack.h 部分代码
typedef struct _Validator {
bool (* const validate)(struct _Validator *pThis, int val); // 1
void * const pData; // 2
} Validator;
typedef struct { // 3
const int min;
const int max;
} Range;
typedef struct { // 4
int previousValue;
} PreviousValue;
#define rangeValidator(pRange) { \ // 7
validateRange, \
pRange \
}
#define previousValidator(pPrevious) { \ // 7
validatePrevious, \
pPrevious \
}
bool validateRange(Validator *pThis, int val) { // 5
Range *pRange = (Range *)(pThis->pData);
return pRange->min <= val && val <= pRange->max;
}
bool validatePrevious(Validator *pThis, int val) { // 6
PreviousValue *pPrevious = (PreviousValue *)pThis->pData;
if (val < pPrevious->previousValue) return false;
pPrevious->previousValue = val;
return true;
}
- 注释1:Validator结构体中的第一个成员是函数指针。它可以指向不同的校验函数(例如
validateRange
函数,validatePrevious
函数),从而实现不同的校验功能,类似于多态的概念。 - 注释2:第二个是校验是所需要用到的数据。由于不同的校验函数用到的校验数据不一样,为了能够保存任意的数据类型,所以使用了
void
指针。 - 注释3:
Range
结构体,保存范围校验的数据。 - 注释4:
PreviousValue
结构体,保存递增校验的数据。 - 注释5:实现范围校验功能的函数。由于使用范围检验,应该使用
Range
结构体的数据,所以要将传进来的(void *)数据转换为(Range *) ,Range *pRange = (Range *)(pThis->pData);
- 注释6:实现递增校验功能的函数。由于使用递增检验,应该使用
PreviousValue
结构体的数据,所以要将传进来的(void *)数据转换为(PreviousValue *) ,PreviousValue *pPrevious = (PreviousValue *)pThis->pData;
- 注释7:初始化各种校验器的宏。
用法示例:
Range range = {0, 9};
Validator validator1 = rangeValidator(&range);
PreviousValue previous = {0};
Validator validator2 = previousValidator(&previous);
抽象出了Validator
结构体后,接下来该修改Stack
结构体。
// stack.h 部分代码
typedef struct {
int top;
const size_t size;
int * const pBuf;
Validator * const pValidator;
} Stack;
#define newStackWithValidator(buf, pValidator) { \
0, sizeof(buf) / sizeof(int), (buf), \
pValidator \
}
// stack.c 部分代码
bool validate(Validator *p, int val) {
if (! p) return true;
return p->validate(p, val);
}
// true: 成功, false: 失敗
bool push(Stack *p, int val) {
if (! validate(p->pValidator, val) || isStackFull(p)) return false;
p->pBuf[p->top++] = val;
return true;
}
现在Stack
结构体中就需要增加一个校验器的指针,可以指向各种校验器,如果想实现新的校验功能,只需实现新的校验器(包括校验函数、校验函数所需数据的结构体、校验器初始化函数)即可。
面向对象与多态性
面向对象的基本思考方式是将数据和处理数据的行为放在一起,降低耦合性,其要点就是不要将数据和处理数据的行为分开。通过将校验处理和校验处理中所需数据从Stack
结构体中分离出来,将它们移至Validator
结构体中,使得校验处理被解耦,从而可以方便的为栈中添加各种各样的校验功能,同时这些功能也可以很容易的在其他功能中被复用。
面向对象编程有很多特性,其中非常重要的一个特性就是多态性。所谓多态就是指从调用者的角度看对象,会发现它们非常相似,难以区分,但是这些被调用对象内部处理实际上各不相同。在之前的例子中,调用者只会调用Validator
结构体中的validate
函数,从外部并不知道其中到底进行了怎样的校验处理。而实际上内部进行了什么样的校验处理,取决于你调用的是哪一种校验器对象。这样就利用结构体和函数指针实现了多态。
继承
面向对象的一重要概念就是继承。前面的Validator
结构体就像是父类,而其他各种具体的校验器就像是子类。虽然这种关系比较模糊。
前面使用(void *)指针保存各种校验器所需的数据类型,这种方法适用于简单情况,假如现在需要扩展范围校验器的功能,使得栈中只能接受奇数或者偶数。可以直接扩展Range
结构体,
typedef struct {
const int min;
const int max;
const bool needOddEvenCheck; // true表示需要进行奇偶校验
const bool needToBeOdd; // true表示必须是奇数
}
这样做在不需要奇偶校验的时候,多保存了两个多余的成员,浪费内存。还可以直接扩展一个新的OddEvenRange
结构体,但是无法复用范围检测的处理函数。
可以模拟c++的继承来解决这个问题:
// stack.h 部分代码
typedef struct _Validator { // 1
bool (* const validate)(struct _Validator *pThis, int val);
} Validator;
typedef struct { // 2
Validator base;
const int min;
const int max;
} RangeValidator;
typedef struct { // 3
Validator base;
int previousValue;
} PreviousValueValidator;
typedef struct {
int top;
const size_t size;
int * const pBuf;
Validator * const pValidator;
} Stack;
bool validateRange(Validator *pThis, int val);
bool validatePrevious(Validator *pThis, int val);
#define newRangeValidator(min, max) \ // 4
{{validateRange}, (min), (max)}
#define newPreviousValueValidator \ // 4
{{validatePrevious}, 0}
- 注释1:
Validator
结构体删除了void
指针,这相当于基类。 - 注释2:派生出来的范围校验器。其中增加了父类对象作为成员。
- 注释3:派生出的值递增校验器。其中增加了父类对象作为成员。
- 用于生成校验器的宏。
// stack.c 部分代码
bool validateRange(Validator *p, int val) {
RangeValidator *pThis = (RangeValidator *)p; // 1
return pThis->min <= val && val <= pThis->max;
}
bool validatePrevious(Validator *p, int val) {
PreviousValueValidator *pThis = (PreviousValueValidator *)p; // 2
if (val < pThis->previousValue) return false;
pThis->previousValue = val;
return true;
}
验证函数和之前的版本差不多。
- 注释1:
validateRange
函数种使用的是RangeValidator
,所以对父校验器进行了类型转换,转换为子校验器RangeValidator
后访问其中的内部成员。 - 注释2:同样在validatePrevious函数内,对父校验器进行了类型转换,转换为了子校验器
PreviousValueValidator
。
Stack定义:
typedef struct {
int top;
const size_t size;
int * const pBuf;
Validator * const pValidator;
} Stack;
#define newStack(buf) { \
0, sizeof(buf) / sizeof(int), (buf), \
NULL \
}
#define newStackWithValidator(buf, pValidator) { \
0, sizeof(buf) / sizeof(int), (buf), \
pValidator \
}
Stack的定义并没有变,但是其中的Validator
成员结构改变了。具体用法如下:
//生成校验器
RangeValidator validator = newRangeValidator(0, 9);
//生成具体Stack
//Stack stack = newStackWithValidator(buf, &validator.base);
//书上是上面注释里的那种写法,但是我认为下面这样写也是对的
//主要由于结构体中内存地址与第一个成员的内存地址相同
Stack stack = newStackWithValidator(buf, &validator);
如果要扩展范围校验器,扩展后要同时满足奇偶校验,书上并没有详细写,自己实现如下
typedef struct {
RangeValidator base;
const bool needToBeodd;
} oddEvenRangeValidator;
bool validateOddEvenRange(Validator *p, int val);
bool validateOddEvenRange(Validator *p, int val) {
oddEvenRangeValidator *pThis = (oddEvenRangeValidator *)p;
//范围校验
// 第一种写法
// if(pThis->base.min > val || val > pThis->base.max)
// return false;
// 第二种写法
if(!validateRange(pThis, val))
return false;
//奇偶校验
bool bOdd = (bool)(val % 2); //不能整除就是 true
if(pThis->needToBeodd && bOdd) //需要是奇数 确实不能整除
return true;
else if(!pThis->needToBeodd && !bOdd) //需要是偶数 确实不能整除
return true;
else
return false;
}
#define newOddEvenRangeValidator(min, max, needToBeodd) \
{{validateOddEvenRange, min, max}, (needToBeodd)}
// 使用方法
// oddEvenRangeValidator validator = newOddEvenRangeValidator(0, 9, false);
上面新建了一个oddEvenRangeValidator
结构体,将RangeValidator
作为base
进行扩展,也就包含了结构体中的min
和max
成员,相当于继承了父类的成员。开始没有想到什么办法对RangeValidator
的校验函数进行利用,因为只有一个函数指针,如果指向一个奇偶校验的函数,没办把再指向范围校验的函数。所以实现了第一种写法,后来发现其实可以直接在奇偶校验函数中先调用validateRange
函数,因为oddEvenRangeValidator
指针可以转换为RangeValidator
指针。
注:python对象模型使用同样的方法构建。
完整代码如下:
// stack.h 代码
#ifndef _STACK_H_
#define _STACK_H_
#include <stddef.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct _Validator {
bool (* const validate)(struct _Validator *pThis, int val);
} Validator;
typedef struct {
Validator base;
const int min;
const int max;
} RangeValidator;
typedef struct {
Validator base;
int previousValue;
} PreviousValueValidator;
typedef struct {
int top;
const size_t size;
int * const pBuf;
Validator * const pValidator;
} Stack;
typedef struct {
RangeValidator base;
const bool needToBeodd;
} oddEvenRangeValidator;
bool validateRange(Validator *pThis, int val);
bool validatePrevious(Validator *pThis, int val);
bool validateOddEvenRange(Validator *p, int val);
#define newRangeValidator(min, max) \
{{validateRange}, (min), (max)}
#define newPreviousValueValidator \
{{validatePrevious}, 0}
#define newOddEvenRangeValidator(min, max, needToBeodd) \
{{validateOddEvenRange, min, max}, (needToBeodd)}
bool push(Stack *p, int val);
bool pop(Stack *p, int *pRet);
#define newStack(buf) { \
0, sizeof(buf) / sizeof(int), (buf), \
NULL \
}
#define newStackWithValidator(buf, pValidator) { \
0, sizeof(buf) / sizeof(int), (buf), \
pValidator \
}
#ifdef __cplusplus
}
#endif
#endif
// stack.c 代码
#include <stdbool.h>
#include "stack.h"
static bool isStackFull(const Stack *p) {
return p->top == p->size;
}
static bool isStackEmpty(const Stack *p) {
return p->top == 0;
}
bool validateRange(Validator *p, int val) {
RangeValidator *pThis = (RangeValidator *)p;
return pThis->min <= val && val <= pThis->max;
}
bool validateOddEvenRange(Validator *p, int val) {
oddEvenRangeValidator *pThis = (oddEvenRangeValidator *)p;
//范围校验
if(!validateRange(pThis, val))
return false;
//奇偶校验
bool bOdd = (bool)(val % 2); //不能整除就是 true
if(pThis->needToBeodd && bOdd) //需要是奇数 确实不能整除
return true;
else if(!pThis->needToBeodd && !bOdd) //需要是偶数 确实不能整除
return true;
else
return false;
}
bool validatePrevious(Validator *p, int val) {
PreviousValueValidator *pThis = (PreviousValueValidator *)p;
if (val < pThis->previousValue) return false;
pThis->previousValue = val;
return true;
}
bool validate(Validator *p, int val) {
if (! p) return true;
return p->validate(p, val);
}
// true: 成功, false: 失敗
bool push(Stack *p, int val) {
if (! validate(p->pValidator, val) || isStackFull(p)) return false;
p->pBuf[p->top++] = val;
return true;
}
// true: 成功, false: 失敗
bool pop(Stack *p, int *pRet) {
if (isStackEmpty(p)) return false;
*pRet = p->pBuf[--p->top];
return true;
}
虚函数表
假如各个对象都持有好几个相同的函数指针,每个对象就会有重复部分,造成内存浪费。例如:
typedef struct Foo {
int count;
void (* const func0) (struct Foo *pThis);
void (* const func1) (struct Foo *pThis);
void (* const func2) (struct Foo *pThis);
} Foo;
Foo foo0 = {
0, func0_impl, func1_impl, func2_impl
};
Foo foo1 = {
1, func0_impl, func1_impl, func2_impl
};
Foo foo2 = {
2, func0_impl, func1_impl, func2_impl
};
在小规模的时候,并没有什么影响,但是如果结构体内有10个函数指针,或者必须生成1000个对象,此时就会有内存浪费。
如果有以下情况:
- 有多个对象都具有相同的行为(即函数指针集相同)。
- 类中持有较多的函数指针。
- 需要生成较多数量的对象。
此时引入虚函数表(和c++中的虚函数表类似)可以避免内存浪费的问题。虚函数的实现就是使用函数指针,而虚函数表就由一系列函数指针组成的函数指针集,结构体中存在一个指针指向函数集的首地址。
typedef struct FooVtbl {
void (* const func0) (struct Foo *pThis);
void (* const func1) (struct Foo *pThis);
void (* const func2) (struct Foo *pThis);
} FooVtbl;
typedef struct Foo {
const int count;
const FooVtbl * const pVbl;
} Foo;
static FooVtbl foo_vtbl = {func0_impl, func1_impl, func2_impl};
Foo foo0 = {0, &foo_vtbl};
Foo foo1 = {1, &foo_vtbl};
Foo foo2 = {2, &foo_vtbl};
这样的代码结构仅需在对象中持有指向虚函数表的指针即可,而无需持有函数指针,能够节约内存。但另一方法,由于函数调用必须经过虚函数表,所以使用起来更加复杂。
pFoo->pVtbl->func0(pFoo);
pFoo->pVtbl->func1(pFoo);
pFoo->pVtbl->func2(pFoo);
所以,在内存使用和执行效率上需要平衡考虑。
总结
- 多态可以以相同的方式处理行为不同的对象,在结构体内持有函数指针可以实现多态。
- 继承可以方便地将不同代码中的共通部分提取出来,主要利用了结构体中其实内存地址与第一个成员的内存地址相同的特性。
- 封装可以将对象的行为和内部状态集中在一起,方便进行抽象化。
- 虚函数表可以节约内存。
- 函数指针的知识很重要。
网友评论