C 不具备面向对象的功能,因此大型 C 程序往往会从 C 的原语中发展出自己的程序。这包括大型 C 项目,如 Linux 内核、BSD 内核和 SQLite。
假设您正在编写一个函数 pass_match()
,它接受输入流、输出流和模式。它的工作原理有点像 grep。它将与模式匹配的每一行输入传递到输出。模式字符串包含由 POSIX fnmatch()
处理的 shell glob 模式。接口如下所示。
void pass_match(FILE *in, FILE *out, const char *pattern);
Glob 模式非常简单,不需要像正则表达式那样进行预编译。只用 字符串 就行了。
一段时间后,客户希望程序除了 shell 样式的 glob 模式之外还支持正则表达式。为了提高效率,正则表达式需要预编译,因此不会作为字符串传递给函数。相反,它将是一个 POSIX regex_t
对象。一种快速而不整洁的方法可能是接受两者并匹配不为 NULL 的一个。
void pass_match(FILE *in, FILE *out, const char *pattern, regex_t *re);
凡事就怕然而, 然而这很丑陋并且无法很好地扩展。当需要更多种类的过滤器时会发生什么?最好接受一个涵盖这两种情况的单个对象,甚至将来可能接受另一种过滤器。
在 C 中自定义函数行为的最常见方法之一是传递函数指针。例如, qsort()
的最后一个参数是一个比较器,用于确定对象如何排序。
对于 pass_match()
,此函数将传入一个字符串并返回一个布尔值,布尔值决定是否应将字符串传递到输出流。每行输入都会调用一次。
void pass_match(FILE *in, FILE *out, bool (*match)(const char *));
但是,这具有与 qsort()
相同的问题之一:传递的函数指针缺乏上下文。它需要一个模式字符串或 regex_t
对象来操作。在其他语言中,它们将作为闭包附加到函数上,但 C 没有闭包。它需要通过全局变量 传递进来,这很不好。
static regex_t regex; // BAD!!!
bool regex_match(const char *string)
{
return regexec(®ex, string, 0, NULL, 0) == 0;
}
由于全局变量,实际上 pass_match()
既不是可重入的也不是线程安全的。我们可以从 GNU 的 qsort_r()
中吸取教训,接受要传递给过滤器函数的上下文。这模拟了一个闭包。
void pass_match(FILE *in, FILE *out,
bool (*match)(const char *, void *), void *context);
提供的上下文指针将作为第二个参数传递给过滤器函数,并且不再需要全局变量。这对于大多数用途来说可能已经足够了,而且尽可能简单。 pass_match()
的接口将涵盖任何类型的过滤器。
但是,将函数和上下文打包为一个对象不是很好吗?
将上下文放在一个结构上并从中创建一个接口怎么样?这是一个带有tag的union,其行为与其中之一相同。
enum filter_type { GLOB, REGEX };
struct filter {
enum filter_type type;
union {
const char *pattern;
regex_t regex;
} context;
};
有一个函数可以与该结构进行交互: filter_match()
。它检查 type
成员并使用正确的上下文调用正确的函数。
bool filter_match(struct filter *filter, const char *string)
{
switch (filter->type) {
case GLOB:
return fnmatch(filter->context.pattern, string, 0) == 0;
case REGEX:
return regexec(&filter->context.regex, string, 0, NULL, 0) == 0;
}
abort(); // programmer error
}
pass_match()
API 现在看起来像这样。这将是对 pass_match()
实现和接口的最终更改。
void pass_match(FILE *input, FILE *output, struct filter *filter);
它仍然不关心过滤器如何工作,因此它足以覆盖未来的所有情况。它只是在给定的指针上调用 filter_match()
。然而, switch
和taged union对扩展并不友好。确实,这对扩展来说是彻头彻尾的敌人。我们终于有了一定程度的多态性,但它很粗糙。这就像将管道胶带构建到设计中一样。添加新行为意味着添加另一个 switch
案例。这是一种倒退。我们可以做得更好。
使用 switch
我们不再利用函数指针。那么在结构体上放置一个函数指针怎么样?
struct filter {
bool (*match)(struct filter *, const char *);
};
过滤器本身作为第一个参数传递,提供上下文。在面向对象语言中,这是隐式的 this
参数。为了避免要求调用者担心这个细节,我们将其隐藏在新的 switch
无版本 filter_match()
中。
bool filter_match(struct filter *filter, const char *string)
{
return filter->match(filter, string);
}
请注意,我们仍然缺少实际的上下文、模式字符串或正则表达式对象。这些将是嵌入过滤器结构的不同结构。
struct filter_regex {
struct filter filter;
regex_t regex;
};
struct filter_glob {
struct filter filter;
const char *pattern;
};
对于这两种情况,原始filter都是第一个成员。这很关键。我们将使用一种称为类型双关的技巧。第一个成员保证位于结构的开头,因此指向 struct filter_glob
的指针也是指向 struct filter
的指针。注意到与继承有什么相似之处吗?
每种类型(glob 和正则表达式)都需要自己的match方法。
static bool
method_match_regex(struct filter *filter, const char *string)
{
struct filter_regex *regex = (struct filter_regex *) filter;
return regexec(®ex->regex, string, 0, NULL, 0) == 0;
}
static bool
method_match_glob(struct filter *filter, const char *string)
{
struct filter_glob *glob = (struct filter_glob *) filter;
return fnmatch(glob->pattern, string, 0) == 0;
}
我在它们前面加上了 method_
前缀来表明它们的预期用途。我声明了这些 方法为static
因为它们是完全私有的。程序的其他部分只能通过结构上的函数指针来访问它们。这意味着我们需要一些构造函数来设置这些函数指针。 (为简单起见,我不进行错误检查。)
struct filter *filter_regex_create(const char *pattern)
{
struct filter_regex *regex = malloc(sizeof(*regex));
regcomp(®ex->regex, pattern, REG_EXTENDED);
regex->filter.match = method_match_regex;
return ®ex->filter;
}
struct filter *filter_glob_create(const char *pattern)
{
struct filter_glob *glob = malloc(sizeof(*glob));
glob->pattern = pattern;
glob->filter.match = method_match_glob;
return &glob->filter;
}
这才是真正的多态。从用户的角度来看,这非常简单。他们调用正确的构造函数并获取具有所需行为的过滤器对象。这个对象可以简单地传递,程序的其他部分不需要担心它是如何实现的。最重要的是,由于每个方法都是一个单独的函数而不是 switch
情况,因此可以独立定义新类型的过滤器子类型,很容易扩展。用户可以创建自己的过滤器类型,其工作方式与两个“内置”过滤器一样。
麻烦的是,正则表达式过滤器完成后需要销毁,但根据设计,用户不知道如何去做。让我们添加一个 free()
方法。
struct filter {
bool (*match)(struct filter *, const char *);
void (*free)(struct filter *);
};
void filter_free(struct filter *filter)
{
return filter->free(filter);
}
以及每种方法的方法。这些也将在构造函数中分配。
static void
method_free_regex(struct filter *f)
{
struct filter_regex *regex = (struct filter_regex *) f;
regfree(®ex->regex);
free(f);
}
static void
method_free_glob(struct filter *f)
{
free(f);
}
The glob constructor should perhaps strdup()
its pattern as a private copy, in which case it would be freed here.
glob 构造函数也许应该 strdup()
将其模式作为私有副本,在这种情况下,它将在此处被释放。
一个好的经验法则是优先选择组合而不是继承。拥有整洁的过滤器对象为组合开辟了一些有趣的可能性。这是一个由两个任意过滤器对象组成的 AND 过滤器。仅当其两个子过滤器都匹配时才匹配。它支持短路,因此将更快或最具辨别力的过滤器放在构造函数中的第一个(用户的责任)。
struct filter_and {
struct filter filter;
struct filter *sub[2];
};
static bool
method_match_and(struct filter *f, const char *s)
{
struct filter_and *and = (struct filter_and *) f;
return filter_match(and->sub[0], s) && filter_match(and->sub[1], s);
}
static void
method_free_and(struct filter *f)
{
struct filter_and *and = (struct filter_and *) f;
filter_free(and->sub[0]);
filter_free(and->sub[1]);
free(f);
}
struct filter *filter_and(struct filter *a, struct filter *b)
{
struct filter_and *and = malloc(sizeof(*and));
and->sub[0] = a;
and->sub[1] = b;
and->filter.match = method_match_and;
and->filter.free = method_free_and;
return &and->filter;
}
它可以组合一个正则表达式过滤器和一个全局过滤器,或者两个正则表达式过滤器,或者两个全局过滤器,甚至其他 AND 过滤器。它不关心子过滤器是什么。另外,这里的 free()
方法释放了它的子过滤器。这意味着用户不需要保留创建的每个过滤器,只需保留合成中的“顶部”过滤器即可。
为了使合成过滤器更易于使用,这里有两个“恒定”过滤器。它们是静态分配、共享的,并且永远不会真正释放。
static bool
method_match_any(struct filter *f, const char *string)
{
return true;
}
static bool
method_match_none(struct filter *f, const char *string)
{
return false;
}
static void
method_free_noop(struct filter *f)
{
}
struct filter FILTER_ANY = { method_match_any, method_free_noop };
struct filter FILTER_NONE = { method_match_none, method_free_noop };
FILTER_NONE
过滤器通常与(理论上的) filter_or()
一起使用,而 FILTER_ANY
通常与先前定义的 filter_and()
一起使用。
这是一个简单的程序,它将多个全局过滤器组合成一个过滤器,每个过滤器对应一个程序参数。
int main(int argc, char **argv)
{
struct filter *filter = &FILTER_ANY;
for (char **p = argv + 1; *p; p++)
filter = filter_and(filter_glob_create(*p), filter);
pass_match(stdin, stdout, filter);
filter_free(filter);
return 0;
}
请注意,只需调用一次 filter_free()
即可清理整个过滤器。
正如我之前提到的,过滤器结构必须是过滤器子类型结构的第一个成员,以便类型双关起作用。如果我们想“继承”这样的两种不同类型,它们都需要处于这样的位置:矛盾。
幸运的是,类型双关可以被推广,这样就不需要第一个成员约束。这通常是通过 container_of()
宏完成的。这是符合 C99 的定义。
#include
#define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member)))
给定一个指向结构体成员的指针, container_of()
宏允许我们返回到包含的结构体。假设正则表达式结构的定义不同,因此 regex_t
成员排在第一位。
struct filter_regex {
regex_t regex;
struct filter filter;
};
构造函数保持不变。方法中的强制转换更改为宏。
static bool
method_match_regex(struct filter *f, const char *string)
{
struct filter_regex *regex = container_of(f, struct filter_regex, filter);
return regexec(®ex->regex, string, 0, NULL, 0) == 0;
}
static void
method_free_regex(struct filter *f)
{
struct filter_regex *regex = container_of(f, struct filter_regex, filter);
regfree(®ex->regex);
free(f);
}
它是一个常量、编译时计算的偏移量,因此应该不会对实际性能产生影响。过滤器现在可以自由参与其他侵入式数据结构,例如链接列表等。它类似于多重继承。
假设我们要向过滤器 API 添加第三种方法 clone()
,以制作过滤器的独立副本,需要单独释放该副本。它就像 C++ 中的复制赋值运算符。每种过滤器都需要为其定义适当的“方法”。只要最后添加这样的新方法,就不会破坏 API,但无论如何都会破坏 ABI。
struct filter {
bool (*match)(struct filter *, const char *);
void (*free)(struct filter *);
struct filter *(*clone)(struct filter *);
};
过滤器对象开始变大。它有三个指针——在现代系统上是 24 个字节——并且这些指针在同一类型的所有实例之间都是相同的。这是很多冗余。为了节省内存空间,这些指针可以在称为虚拟方法表(通常称为 vtable)的公共表中的实例之间共享。
这是过滤器 API 的 vtable 版本。无论接口中有多少方法,现在的开销都只是一个指针。
struct filter {
struct filter_vtable *vtable;
};
struct filter_vtable {
bool (*match)(struct filter *, const char *);
void (*free)(struct filter *);
struct filter *(*clone)(struct filter *);
};
每种类型都会创建自己的 vtable 并在构造函数中链接到它。这是为新的 vtable API 和克隆方法重写的正则表达式过滤器。这是面向对象 C 语言大结局的所有技巧!
struct filter *filter_regex_create(const char *pattern);
struct filter_regex {
regex_t regex;
const char *pattern;
struct filter filter;
};
static bool
method_match_regex(struct filter *f, const char *string)
{
struct filter_regex *regex = container_of(f, struct filter_regex, filter);
return regexec(®ex->regex, string, 0, NULL, 0) == 0;
}
static void
method_free_regex(struct filter *f)
{
struct filter_regex *regex = container_of(f, struct filter_regex, filter);
regfree(®ex->regex);
free(f);
}
static struct filter *
method_clone_regex(struct filter *f)
{
struct filter_regex *regex = container_of(f, struct filter_regex, filter);
return filter_regex_create(regex->pattern);
}
/* vtable */
struct filter_vtable filter_regex_vtable = {
method_match_regex, method_free_regex, method_clone_regex
};
/* constructor */
struct filter *filter_regex_create(const char *pattern)
{
struct filter_regex *regex = malloc(sizeof(*regex));
regex->pattern = pattern;
regcomp(®ex->regex, pattern, REG_EXTENDED);
regex->filter.vtable = &filter_regex_vtable;
return ®ex->filter;
}
这几乎正是 C++ 幕后发生的事情。当方法/函数被声明为 virtual
并因此根据其最左边参数的运行时类型进行分派时,它会列在实现它的类的 vtable 中。否则它只是一个正常的功能。这就是为什么在 C++ 中需要提前声明函数 virtual
的原因。
总之,在普通的旧 C 语言中获得面向对象编程的核心优势相对容易。它不需要大量使用宏,这些系统的用户也不需要知道它的底层是一个对象系统,除非他们想为自己扩展它。
如果您有兴趣戳一下,这里是整个示例程序:
原文地址: C Object Oriented Programming
相关参考: