3.2.6 封装
封装是指通过将对象的状态和行为集中在一起,并规定其与外部接口来进行抽象化的过程。以C语言角度理解,状态就是结构体中函数指针以外的成员,行为就是函数指针成员。假设以下列子:
typedef struct _Validator {
bool (* const validate)(struct _Validator *pThis, int val);
} Validator;
调用validate函数时,程序就会按照校验器内部的规则对输入值进行校验,假设validator的子校验器RangeValidator的定义如下:
typedef struct {
Validator base;
const int min;
const int max;
} RangeValidator;
这里对于调用方来说min、max成员是不需要在意的内部信息,调用者只需关注”将validate函数自身(pThis)和校验对象的值(val)传递过去,就会得到bool类型的校验结果“这个外部接口,而不用在意RangeValidator内部具体实现。
在C语言中,我们无法提供访问控制功能,无法实现数据隐藏,但我们可以使用const修饰符,避免外部修改内部数据。此外,还可以规定命名规则来回避这个问题,如不允许直接访问的成员在命名时以下划线"_"开头。
3.2.7 虚函数表
各对象都持有函数指针时,可能会导致内存浪费,例如下述定义:
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
};
以上可以看出在生成大量内容相同或相似的对象时,这种方法造成了内存浪费。此时,引入虚函数表就可以避免内存浪费问题。这里的”虚函数“是通过函数指针来实现的,实际被调用的函数会随着对象的不同而有不同的功能。可以用虚函数表来重构上述代码,如下:
#include <stdio.h>
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 pVtbl;
} Foo;
void func0_impl(struct Foo *pThis)
{
printf("func0_impl\n");
}
void func1_impl(struct Foo *pThis)
{
printf("func1_impl\n");
}
void func2_impl(struct Foo *pThis)
{
printf("func2_impl\n");
}
static FooVtbl foo_vtbl = {func0_impl, func1_impl, func2_impl};
int main(void)
{
Foo foo0 = {0, &foo_vtbl};
Foo foo1 = {1, &foo_vtbl};
Foo foo2 = {2, &foo_vtbl};
foo0.pVtbl->func1(&foo0);
return 0;
}
这样的代码结构仅需要在对象中持有指向选函数表的指针,而无需持有函数指针,因此可以节省内存。但另一方面,由于函数调用必须经过虚函数表,所以调用过程和程序结构变得复杂了。
3.2.8 非虚函数
通过在对象内持有函数指针,可以让对象的行为根据对象不同而发生变化。但是某些函数在不同对象中处理也可能是相同的,这时就无需再对象内持有函数指针了。如上述例子中,需要增加一个将count复位为0的函数,可以有两种实现,如下:
typedef struct Foo {
int count;
void (* const func0)(struct Foo *pThis);
void (* const func1)(struct Foo *pThis);
void (* const func2)(struct Foo *pThis);
void (*reset_counter)(struct Foo *pThis);
} Foo;
或
typedef struct Foo {
int count;
void (* const func0)(struct Foo *pThis);
void (* const func1)(struct Foo *pThis);
void (* const func2)(struct Foo *pThis);
} Foo;
void reset_foo_counter(Foo *pThis) {
{
pThis->count = 0;
}
第二种方式由于没有使用函数指针,所以能够节省内存,但也意味着无法根据对象来动态地改变行为。同时还有可能遇到命名空间的问题,引起函数名冲突。第一种方式就可以避免这一问题,只需在函数名前加static修饰,由于函数功能只是清零,没有必要动态地根据对象不同改变函数行为。
一般情况下,请优先使用函数指针,只有在内存非常有限、对象行为不会变化的情况下,或者是非常有把握的情况下才考虑使用非虚函数。