C语言如何面向对象编程? 面向对象编程是一种方法,并不局限于某一种编程语言

C语言如何面向对象编程? 面向对象编程是一种方法,并不局限于某一种编程语言

C 不具备面向对象的功能,因此大型 C 程序往往会从 C 的原语中发展出自己的程序。这包括大型 C 项目,如 Linux 内核、BSD 内核和 SQLite。

Starting Simple 从简单开始

假设您正在编写一个函数 pass_match() ,它接受输入流、输出流和模式。它的工作原理有点像 grep。它将与模式匹配的每一行输入传递到输出。模式字符串包含由 POSIX fnmatch() 处理的 shell glob 模式。接口如下所示。

c 复制代码
void pass_match(FILE *in, FILE *out, const char *pattern);

Glob 模式非常简单,不需要像正则表达式那样进行预编译。只用 字符串 就行了。

一段时间后,客户希望程序除了 shell 样式的 glob 模式之外还支持正则表达式。为了提高效率,正则表达式需要预编译,因此不会作为字符串传递给函数 。相反,它将是一个 POSIX regex_t 对象。一种快速而不整洁的方法可能是接受两者并匹配不为 NULL 的一个。

c 复制代码
void pass_match(FILE *in, FILE *out, const char *pattern, regex_t *re);

凡事就怕然而, 然而这很丑陋并且无法很好地扩展。当需要更多种类的过滤器时会发生什么?最好接受一个涵盖这两种情况的单个对象,甚至将来可能接受另一种过滤器。

A Generalized Filter 通用过滤器

在 C 中自定义函数行为的最常见方法之一是传递函数指针。例如, qsort() 的最后一个参数是一个比较器,用于确定对象如何排序。

对于 pass_match() ,此函数将传入一个字符串并返回一个布尔值,布尔值决定是否应将字符串传递到输出流。每行输入都会调用一次。

c 复制代码
void pass_match(FILE *in, FILE *out, bool (*match)(const char *));

但是,这具有与 qsort() 相同的问题之一:传递的函数指针缺乏上下文。它需要一个模式字符串或 regex_t 对象来操作在其他语言中,它们将作为闭包附加到函数上,但 C 没有闭包。它需要通过全局变量 传递进来,这很不好。

c 复制代码
static regex_t regex;  // BAD!!!

bool regex_match(const char *string)
{
    return regexec(&regex, string, 0, NULL, 0) == 0;
}

由于全局变量,实际上 pass_match() 既不是可重入的也不是线程安全的 。我们可以从 GNU 的 qsort_r() 中吸取教训,接受要传递给过滤器函数的上下文。这模拟了一个闭包

c 复制代码
void pass_match(FILE *in, FILE *out,
                bool (*match)(const char *, void *), void *context);

提供的上下文指针将作为第二个参数传递给过滤器函数,并且不再需要全局变量。这对于大多数用途来说可能已经足够了,而且尽可能简单。 pass_match() 的接口将涵盖任何类型的过滤器。

但是,将函数和上下文打包为一个对象不是很好吗?

More Abstraction 更多抽象

将上下文放在一个结构上并从中创建一个接口怎么样?这是一个带有tag的union,其行为与其中之一相同。

c 复制代码
enum filter_type { GLOB, REGEX };

struct filter {
    enum filter_type type;
    union {
        const char *pattern;
        regex_t regex;
    } context;
};

有一个函数可以与该结构进行交互: filter_match() 。它检查 type 成员并使用正确的上下文调用正确的函数。

c 复制代码
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() 实现和接口的最终更改。

c 复制代码
void pass_match(FILE *input, FILE *output, struct filter *filter);

它仍然不关心过滤器如何工作,因此它足以覆盖未来的所有情况。它只是在给定的指针上调用 filter_match() 。然而, switch 和taged union对扩展并不友好。确实,这对扩展来说 是彻头彻尾的敌人。我们终于有了一定程度的多态性,但它很粗糙。这就像将管道胶带构建到设计中一样。添加新行为意味着添加另一个 switch 案例。这是一种倒退。我们可以做得更好。

Methods 方法

使用 switch 我们不再利用函数指针。那么在结构体上放置一个函数指针怎么样?

c 复制代码
struct filter {
    bool (*match)(struct filter *, const char *);
};

过滤器本身作为第一个参数传递,提供上下文 。在面向对象语言中,这是隐式的 this 参数。为了避免要求调用者担心这个细节,我们将其隐藏在新的 switch 无版本 filter_match() 中。

c 复制代码
bool filter_match(struct filter *filter, const char *string)
{
    return filter->match(filter, string);
}

请注意,我们仍然缺少实际的上下文、模式字符串或正则表达式对象。这些将是嵌入过滤器结构的不同结构

c 复制代码
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方法。

c 复制代码
static bool
method_match_regex(struct filter *filter, const char *string)
{
    struct filter_regex *regex = (struct filter_regex *) filter;
    return regexec(&regex->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 因为它们是完全私有的。程序的其他部分只能通过结构上的函数指针来访问它们。这意味着我们需要一些构造函数来设置这些函数指针。 (为简单起见,我不进行错误检查。)

c 复制代码
struct filter *filter_regex_create(const char *pattern)
{
    struct filter_regex *regex = malloc(sizeof(*regex));
    regcomp(&regex->regex, pattern, REG_EXTENDED);
    regex->filter.match = method_match_regex;
    return &regex->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() 方法。

c 复制代码
struct filter {
    bool (*match)(struct filter *, const char *);
    void (*free)(struct filter *);
};

void filter_free(struct filter *filter)
{
    return filter->free(filter);
}

以及每种方法的方法。这些也将在构造函数中分配。

c 复制代码
static void
method_free_regex(struct filter *f)
{
    struct filter_regex *regex = (struct filter_regex *) f;
    regfree(&regex->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 过滤器。仅当其两个子过滤器都匹配时才匹配。它支持短路,因此将更快或最具辨别力的过滤器放在构造函数中的第一个(用户的责任)。

c 复制代码
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() 方法释放了它的子过滤器。这意味着用户不需要保留创建的每个过滤器,只需保留合成中的"顶部"过滤器即可。

为了使合成过滤器更易于使用,这里有两个"恒定"过滤器。它们是静态分配、共享的,并且永远不会真正释放。

c 复制代码
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() 一起使用。

这是一个简单的程序,它将多个全局过滤器组合成一个过滤器,每个过滤器对应一个程序参数。

c 复制代码
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 的定义。

c 复制代码
#include <stddef.h>

#define container_of(ptr, type, member) \    ((type *)((char *)(ptr) - offsetof(type, member)))

给定一个指向结构体成员的指针, container_of() 宏允许我们返回到包含的结构体。假设正则表达式结构的定义不同,因此 regex_t 成员排在第一位。

c 复制代码
struct filter_regex {
    regex_t regex;
    struct filter filter;
};

构造函数保持不变。方法中的强制转换更改为宏。

c 复制代码
static bool
method_match_regex(struct filter *f, const char *string)
{
    struct filter_regex *regex = container_of(f, struct filter_regex, filter);
    return regexec(&regex->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(&regex->regex);
    free(f);

}

它是一个常量、编译时计算的偏移量,因此应该不会对实际性能产生影响。过滤器现在可以自由参与其他侵入式数据结构,例如链接列表等。它类似于多重继承。

Vtables 虚表

假设我们要向过滤器 API 添加第三种方法 clone() ,以制作过滤器的独立副本,需要单独释放该副本。它就像 C++ 中的复制赋值运算符。每种过滤器都需要为其定义适当的"方法"。只要最后添加这样的新方法,就不会破坏 API,但无论如何都会破坏 ABI。

c 复制代码
struct filter {
    bool (*match)(struct filter *, const char *);
    void (*free)(struct filter *);
    struct filter *(*clone)(struct filter *);
};

过滤器对象开始变大。它有三个指针 ------在现代系统上是 24 个字节------并且这些指针在同一类型的所有实例之间都是相同的。这是很多冗余为了节省内存空间 ,这些指针可以在称为虚拟方法表(通常称为 vtable)的公共表中的实例之间共享

这是过滤器 API 的 vtable 版本。无论接口中有多少方法,现在的开销都只是一个指针

c 复制代码
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 语言大结局的所有技巧!

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(&regex->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(&regex->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(&regex->regex, pattern, REG_EXTENDED);
    regex->filter.vtable = &filter_regex_vtable;
    return &regex->filter;
}

这几乎正​​是 C++ 幕后发生的事情。当方法/函数被声明为 virtual 并因此根据其最左边参数的运行时类型进行分派时,它会列在实现它的类的 vtable 中 。否则它只是一个正常的功能。这就是为什么在 C++ 中需要提前声明函数 virtual 的原因。

总之,在普通的旧 C 语言中获得面向对象编程的核心优势相对容易。它不需要大量使用宏,这些系统的用户也不需要知道它的底层是一个对象系统,除非他们想为自己扩展它。

如果您有兴趣戳一下,这里是整个示例程序:

原文地址: C Object Oriented Programming

相关参考:

相关推荐
小突突突1 小时前
Spring框架中的单例bean是线程安全的吗?
java·后端·spring
iso少年1 小时前
Go 语言并发编程核心与用法
开发语言·后端·golang
掘金码甲哥1 小时前
云原生算力平台的架构解读
后端
码事漫谈1 小时前
智谱AI从清华实验室到“全球大模型第一股”的六年征程
后端
码事漫谈1 小时前
现代软件开发中常用架构的系统梳理与实践指南
后端
Mr.Entropy2 小时前
JdbcTemplate 性能好,但 Hibernate 生产力高。 如何选择?
java·后端·hibernate
YDS8292 小时前
SpringCloud —— MQ的可靠性保障和延迟消息
后端·spring·spring cloud·rabbitmq
无限大62 小时前
为什么"区块链"不只是比特币?——从加密货币到分布式应用
后端
洛神么么哒2 小时前
freeswitch-初级-01-日志分割
后端
蝎子莱莱爱打怪2 小时前
我的2025年年终总结
java·后端·面试