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

相关参考:

相关推荐
齐 飞30 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod1 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man2 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*2 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu2 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s2 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子2 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王2 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea