这是在 C API 中实现抽象的正确方法吗?
Is this the right way to implement abstractions in a C API?
我想在 C 中创建一个 API。我的目标是实现抽象以访问和改变 API 中定义的 struct
变量。
API的头文件:
#ifndef API_H
#define API_H
struct s_accessor {
struct s* s_ptr;
};
void api_init_func(struct s_accessor *foo);
void api_mutate_func(struct s_accessor *foo, int x);
void api_print_func(struct s_accessor *foo);
#endif
API'实现文件:
#include <stdio.h>
#include "api.h"
struct s {
int internal;
int other_stuff;
};
void api_init_func(struct s_accessor* foo) {
foo->s_ptr = NULL;
}
void api_print_func(struct s_accessor *foo)
{
printf("Value of member 'internal' = %d\n", foo->s_ptr->internal);
}
void api_mutate_func(struct s_accessor *foo, int x)
{
struct s bar;
foo->s_ptr = &bar;
foo->s_ptr->internal = x;
}
使用API的客户端程序:
#include <stdio.h>
#include "api.h"
int main()
{
struct s_accessor foo;
api_init_func(&foo); // set s_ptr to NULL
api_mutate_func(&foo, 123); // change value of member 'internal' of an instance of struct s
api_print_func(&foo); // print member of struct s
}
我对我的代码有以下疑问:
是否有直接(非 hackish)方式来隐藏我的 API 的实现?
这是为客户端创建抽象以使用我的 API 的正确方法吗?如果没有,我该如何改进它使其变得更好?
这是在 C 应用程序中进行抽象和封装的正确方法。
使用 C 语言中的不完整类型来隐藏结构细节。您可以定义结构、联合和枚举,而无需列出它们的成员(或值,在枚举的情况下)。这样做会导致类型不完整。您不能声明不完整类型的变量,但可以使用指向这些类型的指针
c 语言中的常量
在每个函数中,特别是那些你在 api 中公开的函数,它们不会改变指针或指针指向的结构数据,更好,应该是 const pointer
。这将确保(不知何故 :-) 您仍然可以在 c) 中将其更改为 api 用户您没有更改结构数据。您还可以通过双常量指针来保护数据和地址,请参见下文:
#ifndef API_H
#define API_H
typedef struct s_accessor s_accessor, *p_s_accessor;
void api_init_func(p_s_accessor p_foo);
void api_mutate_func(p_s_accessor p_foo, int x);
void api_print_func(const p_s_accessor const p_foo);
#endif
在api.c你可以完成结构类型:
struct s {
int internal;
int other_stuff;
};
所有辅助函数在 api.c 中应该是静态的(将函数范围限制为 api.c!
尽量减少 api.h 中的包含。
关于问题 1,我认为您没有办法隐藏实施细节!
“访问器”不是一个好术语。这个术语在面向object的编程中用来表示一种方法。
结构类型 struct s_accessor
实际上是一种叫做 handle 的东西。它包含一个指向真实 object 的指针。句柄是一个双重间接指针:应用程序将指针传递给句柄,句柄包含指向 objects.
的指针
一句古老的谚语说“计算机科学中的任何问题都可以通过添加另一层间接来解决”,句柄就是一个典型的例子。句柄允许 objects 从一个地址移动到另一个地址或被替换。然而,对于应用程序而言,句柄地址代表 object,因此当实现 object 被重新定位或替换时,对于应用程序而言,它仍然是相同的 object .
有了句柄,我们可以做如下事情:
有一个向量object可以增长
有 OOP objects 显然可以改变他们的 class
重新定位 variable-length object 缓冲区和字符串等以压缩它们的内存占用空间
所有都没有 object 更改其内存地址并因此更改身份。因为当这些变化发生时句柄保持不变,所以应用程序不必查找 object 指针的每个副本并用一个新指针替换它;手柄有效地在一个地方解决了这个问题。
尽管如此,句柄在 C API 中并不常见,尤其是 lower-level 中。给定一个不使用句柄的 API,您可以围绕它创建句柄。即使您认为 object 的用户将从句柄中获益,将 API 分成两部分可能会更好:一个只处理 s
的内部用户,以及外部一个 struct s_handle
.
如果您正在使用线程,那么句柄需要仔细的并发编程。所以也就是说,即使从应用程序的角度来看,你可以改变handle-referenced object,这很方便,它需要同步。假设我们有一个由句柄引用的向量 object。应用程序代码正在使用它,所以我们不能突然用指向不同向量的指针替换向量(以调整它的大小)。另一个线程正在处理原始指针。通过句柄访问向量或将值存储到向量中的操作必须与替换操作同步。即使所有这些都做对了,它也会增加很多开销,因此应用程序人员可能会注意到一些性能问题并要求在 API 中寻找逃生舱口,比如某些函数函数要“固定”向下一个手柄,这样 object 就不能移动,而有效的操作直接与其中的 s
object 一起工作。
出于这个原因,我倾向于远离设计句柄 API,并使这类事情成为应用程序的问题。对于 multi-threaded 应用程序来说,仅使用 well-designed“请只 s
”API 可能比完全编写 thread-safe 更容易,健壮、高效的 struct s_handle
层。
- Is there a direct (non-hackish) way to hide the implementation of my API?
基本上,在 C 中隐藏 API 实现的“规则 #1”不允许 init
操作,客户端应用程序声明一些内存和您的 API初始化它。也就是说,可能是这样的:
typedef struct opaque opaque_t;
#ifndef OPAQUE_IMPL
struct opaque {
int dummy[42]; // big enough for all future extension
} opaque_t;
#endif
void opaque_init(opaque_t *o);
在此声明中,我们没有向客户透露任何内容,只是我们的 object 是需要 int
对齐的内存缓冲区,并且至少为 42 int
宽
实际上,object更小;我们刚刚为未来的增长增加了储备金。只要我们的 object 不需要超过 int [42]
字节,我们就可以使我们的实际 object 更大而不必 re-compile 客户端。
为什么我们有 #ifndef
是因为实现代码会做这样的事情:
#define OPAQUE_IMPL // 抑制 header 中的假定义
#include “opaque.h”
//实际定义
结构不透明 {
随便什么;
字符 *名称;
};
这种事情不符合 ISO C 的“法律”,因为客户端和实现实际上使用了 struct opaque
类型的不同定义。
允许客户端自己分配 objects 会产生一定的效率,因为在自动存储中分配 objects(即将它们声明为局部变量)可以将它们放入堆栈中,开销很小与动态内存分配相比。
更常见的不透明方法是根本不提供init
操作,只提供分配新object并销毁它的操作:
typedef struct opaque opaque_t; // incomplete struct
opaque_t *opaque_create(/* args .... */);
void opaque_destroy(opaque_t *o);
现在调用者什么都不知道,除了“不透明”object 表示为一个指针,在其整个生命周期中都是同一个指针。
总o对于应用程序或应用程序框架内部的 API,水性可能不值得。它对拥有外部客户的 API 很有用,例如不同团队或组织中的应用程序开发人员。
问问自己这个问题:这个 API 的客户端及其实现是否会单独发布和升级?如果答案是否定的,那么就减少了对完全不透明的需求。
我想在 C 中创建一个 API。我的目标是实现抽象以访问和改变 API 中定义的 struct
变量。
API的头文件:
#ifndef API_H
#define API_H
struct s_accessor {
struct s* s_ptr;
};
void api_init_func(struct s_accessor *foo);
void api_mutate_func(struct s_accessor *foo, int x);
void api_print_func(struct s_accessor *foo);
#endif
API'实现文件:
#include <stdio.h>
#include "api.h"
struct s {
int internal;
int other_stuff;
};
void api_init_func(struct s_accessor* foo) {
foo->s_ptr = NULL;
}
void api_print_func(struct s_accessor *foo)
{
printf("Value of member 'internal' = %d\n", foo->s_ptr->internal);
}
void api_mutate_func(struct s_accessor *foo, int x)
{
struct s bar;
foo->s_ptr = &bar;
foo->s_ptr->internal = x;
}
使用API的客户端程序:
#include <stdio.h>
#include "api.h"
int main()
{
struct s_accessor foo;
api_init_func(&foo); // set s_ptr to NULL
api_mutate_func(&foo, 123); // change value of member 'internal' of an instance of struct s
api_print_func(&foo); // print member of struct s
}
我对我的代码有以下疑问:
是否有直接(非 hackish)方式来隐藏我的 API 的实现?
这是为客户端创建抽象以使用我的 API 的正确方法吗?如果没有,我该如何改进它使其变得更好?
这是在 C 应用程序中进行抽象和封装的正确方法。 使用 C 语言中的不完整类型来隐藏结构细节。您可以定义结构、联合和枚举,而无需列出它们的成员(或值,在枚举的情况下)。这样做会导致类型不完整。您不能声明不完整类型的变量,但可以使用指向这些类型的指针
c 语言中的常量
在每个函数中,特别是那些你在 api 中公开的函数,它们不会改变指针或指针指向的结构数据,更好,应该是 const pointer
。这将确保(不知何故 :-) 您仍然可以在 c) 中将其更改为 api 用户您没有更改结构数据。您还可以通过双常量指针来保护数据和地址,请参见下文:
#ifndef API_H
#define API_H
typedef struct s_accessor s_accessor, *p_s_accessor;
void api_init_func(p_s_accessor p_foo);
void api_mutate_func(p_s_accessor p_foo, int x);
void api_print_func(const p_s_accessor const p_foo);
#endif
在api.c你可以完成结构类型:
struct s {
int internal;
int other_stuff;
};
所有辅助函数在 api.c 中应该是静态的(将函数范围限制为 api.c!
尽量减少 api.h 中的包含。
关于问题 1,我认为您没有办法隐藏实施细节!
“访问器”不是一个好术语。这个术语在面向object的编程中用来表示一种方法。
结构类型 struct s_accessor
实际上是一种叫做 handle 的东西。它包含一个指向真实 object 的指针。句柄是一个双重间接指针:应用程序将指针传递给句柄,句柄包含指向 objects.
一句古老的谚语说“计算机科学中的任何问题都可以通过添加另一层间接来解决”,句柄就是一个典型的例子。句柄允许 objects 从一个地址移动到另一个地址或被替换。然而,对于应用程序而言,句柄地址代表 object,因此当实现 object 被重新定位或替换时,对于应用程序而言,它仍然是相同的 object .
有了句柄,我们可以做如下事情:
有一个向量object可以增长
有 OOP objects 显然可以改变他们的 class
重新定位 variable-length object 缓冲区和字符串等以压缩它们的内存占用空间
所有都没有 object 更改其内存地址并因此更改身份。因为当这些变化发生时句柄保持不变,所以应用程序不必查找 object 指针的每个副本并用一个新指针替换它;手柄有效地在一个地方解决了这个问题。
尽管如此,句柄在 C API 中并不常见,尤其是 lower-level 中。给定一个不使用句柄的 API,您可以围绕它创建句柄。即使您认为 object 的用户将从句柄中获益,将 API 分成两部分可能会更好:一个只处理 s
的内部用户,以及外部一个 struct s_handle
.
如果您正在使用线程,那么句柄需要仔细的并发编程。所以也就是说,即使从应用程序的角度来看,你可以改变handle-referenced object,这很方便,它需要同步。假设我们有一个由句柄引用的向量 object。应用程序代码正在使用它,所以我们不能突然用指向不同向量的指针替换向量(以调整它的大小)。另一个线程正在处理原始指针。通过句柄访问向量或将值存储到向量中的操作必须与替换操作同步。即使所有这些都做对了,它也会增加很多开销,因此应用程序人员可能会注意到一些性能问题并要求在 API 中寻找逃生舱口,比如某些函数函数要“固定”向下一个手柄,这样 object 就不能移动,而有效的操作直接与其中的 s
object 一起工作。
出于这个原因,我倾向于远离设计句柄 API,并使这类事情成为应用程序的问题。对于 multi-threaded 应用程序来说,仅使用 well-designed“请只 s
”API 可能比完全编写 thread-safe 更容易,健壮、高效的 struct s_handle
层。
- Is there a direct (non-hackish) way to hide the implementation of my API?
基本上,在 C 中隐藏 API 实现的“规则 #1”不允许 init
操作,客户端应用程序声明一些内存和您的 API初始化它。也就是说,可能是这样的:
typedef struct opaque opaque_t;
#ifndef OPAQUE_IMPL
struct opaque {
int dummy[42]; // big enough for all future extension
} opaque_t;
#endif
void opaque_init(opaque_t *o);
在此声明中,我们没有向客户透露任何内容,只是我们的 object 是需要 int
对齐的内存缓冲区,并且至少为 42 int
宽
实际上,object更小;我们刚刚为未来的增长增加了储备金。只要我们的 object 不需要超过 int [42]
字节,我们就可以使我们的实际 object 更大而不必 re-compile 客户端。
为什么我们有 #ifndef
是因为实现代码会做这样的事情:
#define OPAQUE_IMPL // 抑制 header 中的假定义 #include “opaque.h”
//实际定义 结构不透明 { 随便什么; 字符 *名称; };
这种事情不符合 ISO C 的“法律”,因为客户端和实现实际上使用了 struct opaque
类型的不同定义。
允许客户端自己分配 objects 会产生一定的效率,因为在自动存储中分配 objects(即将它们声明为局部变量)可以将它们放入堆栈中,开销很小与动态内存分配相比。
更常见的不透明方法是根本不提供init
操作,只提供分配新object并销毁它的操作:
typedef struct opaque opaque_t; // incomplete struct
opaque_t *opaque_create(/* args .... */);
void opaque_destroy(opaque_t *o);
现在调用者什么都不知道,除了“不透明”object 表示为一个指针,在其整个生命周期中都是同一个指针。
总o对于应用程序或应用程序框架内部的 API,水性可能不值得。它对拥有外部客户的 API 很有用,例如不同团队或组织中的应用程序开发人员。
问问自己这个问题:这个 API 的客户端及其实现是否会单独发布和升级?如果答案是否定的,那么就减少了对完全不透明的需求。