这是在C API中实现抽象的正确方法吗?

这是在C API中实现抽象的正确方法吗?,c,abstraction,C,Abstraction,我想用C语言创建一个API。我的目标是实现抽象来访问和修改API中定义的结构变量 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(struc

我想用C语言创建一个API。我的目标是实现抽象来访问和修改API中定义的结构变量

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
}
关于我的代码,我有以下问题:

有没有一种直接的非黑客方式来隐藏我的API的实现

这是为客户端创建抽象以使用我的API的正确方法吗?如果没有,我如何改进它以使其更好


这是在C应用程序中进行抽象和封装的正确方法。 使用C语言中的不完整类型隐藏结构细节。对于枚举,可以定义结构、联合和枚举,而不列出其成员或值。这样做会导致类型不完整。不能声明不完整类型的变量,但可以使用指向这些类型的指针

c语言中的康斯特内斯 在evrey函数中,尤其是在api中公开的那些函数,它们最好不改变指针或指针指向的结构数据,并且应该是常量指针。这将以某种方式确保:-您仍然可以在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.cli中的所有辅助函数都应该是静态的。请将函数范围仅限于api.c

尽量减少api.h中包含的内容


关于问题1,我认为没有办法隐藏实现细节

这是在C应用程序中进行抽象和封装的正确方法。 使用C语言中的不完整类型隐藏结构细节。对于枚举,可以定义结构、联合和枚举,而不列出其成员或值。这样做会导致类型不完整。不能声明不完整类型的变量,但可以使用指向这些类型的指针

c语言中的康斯特内斯 在evrey函数中,尤其是在api中公开的那些函数,它们最好不改变指针或指针指向的结构数据,并且应该是常量指针。这将以某种方式确保:-您仍然可以在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.cli中的所有辅助函数都应该是静态的。请将函数范围仅限于api.c

尽量减少api.h中包含的内容


关于问题1,我认为没有办法隐藏实现细节

访问器不是一个好的术语。这个术语在面向对象编程中用来表示一种方法

结构类型struct s_accessor实际上是一个称为句柄的东西。它包含一个指向真实对象的指针。句柄是双间接指针:应用程序传递指向句柄的指针,句柄包含指向对象的指针

一句古老的格言说,计算机科学中的任何问题都可以通过添加另一层间接寻址来解决,其中句柄就是一个很好的例子。句柄允许对象从一个地址移动到另一个地址或被替换。然而,对于应用程序来说,句柄地址代表对象,因此当重新定位或替换实现对象时,就应用程序而言,它仍然是相同的对象

使用手柄,我们可以执行以下操作:

有一个可以生长的向量对象

具有可以明显更改其类的OOP对象

重新定位可变长度对象(如缓冲区和字符串)以压缩其内存占用

所有这些都不会改变对象的内存地址和身份。由于句柄在发生这些更改时保持不变,因此应用程序不必查找对象指针的每个副本并用新副本替换它;手柄在一个地方有效地处理了这个问题

尽管如此,句柄在C API中往往是不寻常的,尤其是较低级别的。给定一个不使用句柄的API,您可以围绕它快速生成句柄。即使您认为对象的用户将从句柄中受益,也可以将API分成两部分:一部分内部API只处理s,另一部分外部API处理struct s_句柄

如果您使用的是线程,那么句柄需要仔细的并发编程。也就是说,即使从应用程序的角度来看,您可以更改句柄引用的对象,这很方便,它需要同步。假设我们有一个由句柄引用的向量对象。应用程序代码正在使用它,所以我们不能为了调整它的大小而突然将向量替换为指向另一个向量的指针。另一个线程是 只是在使用原始指针的中间。访问向量或通过句柄将值存储到向量中的操作必须与替换操作同步。即使所有这些都做得很好,也会增加很多开销,因此应用程序人员可能会注意到一些性能问题,并要求在API中使用转义图案填充,比如某些函数固定句柄,以便在高效操作直接处理其内的对象时,对象无法移动

出于这个原因,我倾向于远离设计句柄API,而将这类事情作为应用程序的问题。对于一个多线程应用程序来说,仅仅正确地使用一个精心设计的s please API可能比编写一个完全线程安全、健壮、高效的结构s_句柄层更容易

有没有一种直接的非黑客方式来隐藏我的API的实现? 基本上,在C中隐藏API实现的规则1是不允许客户端应用程序声明一些内存并由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);
在这个声明中,我们没有向客户机透露任何信息,只是我们的对象是需要int对齐的内存缓冲区,并且至少有42 int宽

事实上,物体较小;我们刚刚为未来的增长增加了一笔准备金。只要我们的对象不需要超过int[42]字节,我们就可以在不需要重新编译客户端的情况下使实际对象变大

为什么我们有ifndef是因为实现代码将执行以下操作:

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);
定义不透明\u IMPL//抑制标题中的假定义 包括不透明

//实际定义 结构不透明{ int随便什么; 字符*名称; };

这类事情违反了ISO C的规则,因为实际上客户端和实现使用了不同的结构不透明类型定义

允许客户机自己分配对象会产生一定的效率,因为在自动存储中分配对象(即将它们声明为局部变量)可以将它们放在堆栈中,与动态内存分配相比,开销非常小

对于不透明性,更常见的方法是根本不提供init操作,只提供分配新对象并销毁它的操作:

typedef struct opaque opaque_t; // incomplete struct

opaque_t *opaque_create(/* args .... */);
void opaque_destroy(opaque_t *o);
现在调用方什么都不知道,只知道一个不透明对象被表示为一个指针,在它的整个生命周期中都是同一个指针

对于应用程序或应用程序框架内部的API来说,完全不透明可能不值得。它对于具有外部客户机的API非常有用,例如不同团队或组织中的应用程序开发人员


问自己这样一个问题:这个API的客户机及其实现是否会单独发布和升级?如果答案是否定的,那么这就减少了对完全不透明的需求。

访问器不是一个好的术语。这个术语在面向对象编程中用来表示一种方法

结构类型struct s_accessor实际上是一个称为句柄的东西。它包含一个指向真实对象的指针。句柄是双间接指针:应用程序传递指向句柄的指针,句柄包含指向对象的指针

一句古老的格言说,计算机科学中的任何问题都可以通过添加另一层间接寻址来解决,其中句柄就是一个很好的例子。句柄允许对象从一个地址移动到另一个地址或被替换。然而,对于应用程序来说,句柄地址代表对象,因此当重新定位或替换实现对象时,就应用程序而言,它仍然是相同的对象

使用手柄,我们可以执行以下操作:

有一个可以生长的向量对象

具有可以明显更改其类的OOP对象

重新定位可变长度对象(如缓冲区和字符串)以压缩其内存占用

所有这些都不会改变对象的内存地址和身份。由于句柄在发生这些更改时保持不变,因此应用程序不必查找对象指针的每个副本并用新副本替换它;手柄在一个地方有效地处理了这个问题

尽管如此,句柄在C API中往往是不寻常的,尤其是较低级别的。给定一个不使用句柄的API,您可以围绕它快速生成句柄。即使您认为对象的用户将从句柄中受益,也可以将API分成两部分:一部分内部API只处理s,另一部分外部API处理struct s_句柄

如果您使用的是线程,那么句柄需要仔细的并发编程。也就是说,即使从应用程序的角度来看,您也可以更改句柄引用对象,这很方便,需要同步 离子。假设我们有一个由句柄引用的向量对象。应用程序代码正在使用它,所以我们不能为了调整它的大小而突然将向量替换为指向另一个向量的指针。另一个线程正处于使用原始指针的中间。访问向量或通过句柄将值存储到向量中的操作必须与替换操作同步。即使所有这些都做得很好,也会增加很多开销,因此应用程序人员可能会注意到一些性能问题,并要求在API中使用转义图案填充,比如某些函数固定句柄,以便在高效操作直接处理其内的对象时,对象无法移动

出于这个原因,我倾向于远离设计句柄API,而将这类事情作为应用程序的问题。对于一个多线程应用程序来说,仅仅正确地使用一个精心设计的s please API可能比编写一个完全线程安全、健壮、高效的结构s_句柄层更容易

有没有一种直接的非黑客方式来隐藏我的API的实现? 基本上,在C中隐藏API实现的规则1是不允许客户端应用程序声明一些内存并由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);
在这个声明中,我们没有向客户机透露任何信息,只是我们的对象是需要int对齐的内存缓冲区,并且至少有42 int宽

事实上,物体较小;我们刚刚为未来的增长增加了一笔准备金。只要我们的对象不需要超过int[42]字节,我们就可以在不需要重新编译客户端的情况下使实际对象变大

为什么我们有ifndef是因为实现代码将执行以下操作:

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);
定义不透明\u IMPL//抑制标题中的假定义 包括不透明

//实际定义 结构不透明{ int随便什么; 字符*名称; };

这类事情违反了ISO C的规则,因为实际上客户端和实现使用了不同的结构不透明类型定义

允许客户机自己分配对象会产生一定的效率,因为在自动存储中分配对象(即将它们声明为局部变量)可以将它们放在堆栈中,与动态内存分配相比,开销非常小

对于不透明性,更常见的方法是根本不提供init操作,只提供分配新对象并销毁它的操作:

typedef struct opaque opaque_t; // incomplete struct

opaque_t *opaque_create(/* args .... */);
void opaque_destroy(opaque_t *o);
现在调用方什么都不知道,只知道一个不透明对象被表示为一个指针,在它的整个生命周期中都是同一个指针

对于应用程序或应用程序框架内部的API来说,完全不透明可能不值得。它对于具有外部客户机的API非常有用,例如不同团队或组织中的应用程序开发人员



问自己这样一个问题:这个API的客户机及其实现是否会单独发布和升级?如果答案是否定的,那么这就减少了对完全不透明的需要。

如果你谈论抽象,你也需要谈论封装,当谈论封装时,结构应该只存在于c文件不完整类型中为什么要使用extern?在这种情况下,您根本不需要使用它。只需从方便的角度定义函数签名,而不使用extern。在使用这个东西时,有一个typedef跳过struct部分很好。@Adam谢谢你的建议。我已经从我的标题中删除了externfile@tadman谢谢你的建议!如果你谈论抽象,你也需要谈论封装,当谈论封装时,结构应该只存在于c文件中,为什么要使用extern?在这种情况下,您根本不需要使用它。只需从方便的角度定义函数签名,而不使用extern。在使用这个东西时,有一个typedef跳过struct部分很好。@Adam谢谢你的建议。我已经从我的标题中删除了externfile@tadman谢谢你的建议*p_s_存取器;帕夫,我想说的是。例如,api_print_func可采用常量s_acecssor*。由于我更像是一个linux内核编码风格的人,客观上我只使用struct s_accessor*,我认为这里不需要typedef。@KamilCuk-typedef在99%的情况下使代码更清晰易读。关于指针类型的typedef,我同意你的看法。但同时c也让我们能够做到这一点。@KamilCuk大多数Linux内核编码风格都是一堆废话,从8个空格的硬标签开始。@Adam谢谢你的回答!将根据您的需要对我的代码进行更改suggested@my高兴谢谢。*p_s_访问器;帕夫,我想说的是。例如,api_print_func可采用常量s_acecssor*。由于我更像是一个linux内核编码风格的人,所以我客观上
如果只使用struct s_accessor*,我认为这里不需要typedef。@KamilCuk-typedef在99%的情况下使代码更清晰易读。关于指针类型的typedef,我同意你的看法。但同时c也让我们能够做到这一点。@KamilCuk大多数Linux内核编码风格都是一堆废话,从8个空格的硬标签开始。@Adam谢谢你的回答!将根据您的需要对我的代码进行更改suggested@my高兴谢谢。非常感谢你的回答!从你的回答中学到了很多,尤其是关于把手的一般性问题。根据您的建议,我不会编写结构s_句柄层,因为我不想要不必要的层,因为在C中,我的代码的所有开销都是开放的。如果我计划在不使用struct s_handle层的情况下编写s-only API,您认为我应该添加哪些附加函数以使其成为设计良好的API?添加一个mutator函数就足够了吗?@RonakSharma尝试开发API以及一些用作单元测试的示例应用程序。API的使用将揭示需要哪些函数。有时,如果您过度预期了所需的内容,那么最终将得到未使用的API函数。这也是一个设计问题;把代码放在一边,制作一些关于应用程序如何与API交互的序列图。非常感谢您的回答!从你的回答中学到了很多,尤其是关于把手的一般性问题。根据您的建议,我不会编写结构s_句柄层,因为我不想要不必要的层,因为在C中,我的代码的所有开销都是开放的。如果我计划在不使用struct s_handle层的情况下编写s-only API,您认为我应该添加哪些附加函数以使其成为设计良好的API?添加一个mutator函数就足够了吗?@RonakSharma尝试开发API以及一些用作单元测试的示例应用程序。API的使用将揭示需要哪些函数。有时,如果您过度预期了所需的内容,那么最终将得到未使用的API函数。这也是一个设计问题;把代码放在一边,制作一些关于应用程序如何与API交互的序列图。