不透明的 C 结构:声明它们的各种方式

Posted

技术标签:

【中文标题】不透明的 C 结构:声明它们的各种方式【英文标题】:Opaque C structs: various ways to declare them 【发布时间】:2011-04-27 06:55:31 【问题描述】:

我在 C API 中看到了以下两种声明不透明类型的风格。在 C 中声明不透明结构/指针的各种方法是什么?使用一种风格比另一种风格有什么明显的优势吗?

选项 1

// foo.h
typedef struct foo * fooRef;
void doStuff(fooRef f);

// foo.c
struct foo 
    int x;
    int y;
;

选项 2

// foo.h
typedef struct _foo foo;
void doStuff(foo *f);

// foo.c
struct _foo 
    int x;
    int y;
;

【问题讨论】:

另见Is it a good idea to typedef pointers? 另请注意,以下划线开头的名称在用户代码中不是一个好主意(相对于系统代码 - 实现)。 §7.1.3 标准的“保留标识符”: • 以下划线和大写字母或另一个下划线开头的所有标识符始终保留用于任何用途。 • 所有以下划线开头的标识符始终保留用作在普通和标记名称空间中具有文件范围的标识符。 Opaque type example (聚会有点晚了,我知道,但是)我只是提出了一个完整的例子,如Option 1.5,这里:***.com/a/54488289/4561887。 投票重新提出这个问题。请求各种方式来声明和使用指向结构的不透明指针不是基于意见的。相反,它只是简单地展示了该语言允许的各种方法和技术。 【参考方案1】:

我的投票是 mouviciel 发布然后删除的第三个选项:

我见过第三种方式:

// foo.h
struct foo;
void doStuff(struct foo *f);

// foo.c
struct foo 
    int x;
    int y;
;

如果你实在无法忍受输入struct 关键字,typedef struct foo foo;(注意:去掉无用和有问题的下划线)是可以接受的。但无论你做什么,永远不要使用typedef 来定义指针类型的名称。它隐藏了极其重要的信息,即这种类型的变量引用了一个可以在将它们传递给函数时进行修改的对象,并且它使处理不同限定(例如,const-qualified)的指针版本大痛。

【讨论】:

'Never' 在这里相当强大:不透明类型的全部意义在于向您的 api 用户隐藏实现,使前者独立于后者,并通过以下方式提供安全措施限制用户直接修改;在这种情况下,我认为别名指针类型或隐藏限定符没有任何问题(即,如果它们是实现细节) 类型是否为指针不是实现细节。它是您可能使用该类型的任何操作的语义的基础。这是我完全支持的“从不”。 具有内置 const 限定符的类型对于不可变字符串(或任何分配的对象)无效,因为您的对象实现不能 free a const -qualified 指针(free 采用非const-qualified void *,这是有充分理由的)。这不是技术问题,而是违反const 语义的问题。当然你可以在你的immutable_string_free 函数中抛弃const,但是现在我们进入了肮脏的黑客领域。 任何不透明对象分配函数应始终返回footype *,而要释放的函数应采用footype * @R: 类型是否为指针绝对是实现细节。是的,作为指针赋予了它特定的语义,但是这些语义并不是指针所特有的。如果我从我的库中公开一个句柄类型,并告诉您它持久地标识一个小工具,那么您不需要、不应该并且必须不在乎它是指针还是私有全局的索引我的库中的数组(或链表,以允许增长)或魔法。唯一重要的是它被正确记录为持久对象的标识符。 @Eric:***const 已从实际参数中删除,因此“指向魔法的常量指针”和“常量魔法”都不会以任何方式限制库。并且无论是“指向 const 魔法的指针”还是“指向非 const 魔法的指针”都是一个实现细节……至少对调用者的代码并不重要,因为他不应该接触魔法,甚至不应该取消引用指针,这是接触魔法的必要第一步。【参考方案2】:

选项 1.5(“基于对象”的 C 架构):

我习惯于使用 选项 1,除非你用 _h 命名你的引用以表示它是这个给定 C“类”的 C 样式“对象”的“句柄” ”。然后,您确保您的函数原型使用const,只要此对象“句柄”的内容只是一个输入,并且不能更改,并且不要在任何内容可以的地方使用const改变了。所以,做这种风格:

// -------------
// my_module.h
// -------------

// An opaque pointer (handle) to a C-style "object" of "class" type 
// "my_module" (struct my_module_s *, or my_module_h):
typedef struct my_module_s *my_module_h;

void doStuff1(my_module_h my_module);
void doStuff2(const my_module_h my_module);

// -------------
// my_module.c
// -------------

// Definition of the opaque struct "object" of C-style "class" "my_module".
struct my_module_s

    int int1;
    int int2;
    float f1;
    // etc. etc--add more "private" member variables as you see fit
;

这是一个在 C 中使用不透明指针创建对象的完整示例。以下架构可能被称为“基于对象的 C”:

//==============================================================================================
// my_module.h
//==============================================================================================

// An opaque pointer (handle) to a C-style "object" of "class" type "my_module" (struct
// my_module_s *, or my_module_h):
typedef struct my_module_s *my_module_h;

// Create a new "object" of "class" "my_module": A function that takes a *pointer to* an
// "object" handle, `malloc`s memory for a new copy of the opaque  `struct my_module_s`, then
// points the user's input handle (via its passed-in pointer) to this newly-created  "object" of
// "class" "my_module".
void my_module_open(my_module_h * my_module_h_p);

// A function that takes this "object" (via its handle) as an input only and cannot modify it
void my_module_do_stuff1(const my_module_h my_module);

// A function that can modify the private content of this "object" (via its handle) (but still
// cannot modify the  handle itself)
void my_module_do_stuff2(my_module_h my_module);

// Destroy the passed-in "object" of "class" type "my_module": A function that can close this
// object by stopping all operations, as required, and `free`ing its memory.
void my_module_close(my_module_h my_module);

//==============================================================================================
// my_module.c
//==============================================================================================

// Definition of the opaque struct "object" of C-style "class" "my_module".
// - NB: Since this is an opaque struct (declared in the header but not defined until the source
// file), it has the  following 2 important properties:
// 1) It permits data hiding, wherein you end up with the equivalent of a C++ "class" with only
// *private* member  variables.
// 2) Objects of this "class" can only be dynamically allocated. No static allocation is
// possible since any module including the header file does not know the contents of *nor the
// size of* (this is the critical part) this "class" (ie: C struct).
struct my_module_s

    int my_private_int1;
    int my_private_int2;
    float my_private_float;
    // etc. etc--add more "private" member variables as you see fit
;

void my_module_open(my_module_h * my_module_h_p)

    // Ensure the passed-in pointer is not NULL (since it is a core dump/segmentation fault to
    // try to dereference  a NULL pointer)
    if (!my_module_h_p)
    
        // Print some error or store some error code here, and return it at the end of the
        // function instead of returning void.
        goto done;
    

    // Now allocate the actual memory for a new my_module C object from the heap, thereby
    // dynamically creating this C-style "object".
    my_module_h my_module; // Create a local object handle (pointer to a struct)
    // Dynamically allocate memory for the full contents of the struct "object"
    my_module = malloc(sizeof(*my_module)); 
    if (!my_module) 
    
        // Malloc failed due to out-of-memory. Print some error or store some error code here,
        // and return it at the end of the function instead of returning void.   
        goto done;
    

    // Initialize all memory to zero (OR just use `calloc()` instead of `malloc()` above!)
    memset(my_module, 0, sizeof(*my_module));

    // Now pass out this object to the user, and exit.
    *my_module_h_p = my_module;

done:


void my_module_do_stuff1(const my_module_h my_module)

    // Ensure my_module is not a NULL pointer.
    if (!my_module)
    
        goto done;
    

    // Do stuff where you use my_module private "member" variables.
    // Ex: use `my_module->my_private_int1` here, or `my_module->my_private_float`, etc. 

done:


void my_module_do_stuff2(my_module_h my_module)

    // Ensure my_module is not a NULL pointer.
    if (!my_module)
    
        goto done;
    

    // Do stuff where you use AND UPDATE my_module private "member" variables.
    // Ex:
    my_module->my_private_int1 = 7;
    my_module->my_private_float = 3.14159;
    // Etc.

done:


void my_module_close(my_module_h my_module)

    // Ensure my_module is not a NULL pointer.
    if (!my_module)
    
        goto done;
    

    free(my_module);

done:

简化示例用法:

#include "my_module.h"

#include <stdbool.h>
#include <stdio.h>

int main()

    printf("Hello World\n");

    bool exit_now = false;

    // setup/initialization
    my_module_h my_module = NULL;
    // For safety-critical and real-time embedded systems, it is **critical** that you ONLY call
    // the `_open()` functions during **initialization**, but NOT during normal run-time,
    // so that once the system is initialized and up-and-running, you can safely know that
    // no more dynamic-memory allocation, which is non-deterministic and can lead to crashes,
    // will occur.
    my_module_open(&my_module);
    // Ensure initialization was successful and `my_module` is no longer NULL.
    if (!my_module)
    
        // await connection of debugger, or automatic system power reset by watchdog
        log_errors_and_enter_infinite_loop(); 
    

    // run the program in this infinite main loop
    while (exit_now == false)
    
        my_module_do_stuff1(my_module);
        my_module_do_stuff2(my_module);
    

    // program clean-up; will only be reached in this case in the event of a major system 
    // problem, which triggers the infinite main loop above to `break` or exit via the 
    // `exit_now` variable
    my_module_close(my_module);

    // for microcontrollers or other low-level embedded systems, we can never return,
    // so enter infinite loop instead
    while (true) ; // await reset by watchdog

    return 0;

除此之外的唯一改进是:

    实现完整的错误处理并返回错误而不是void。例如:

     /// @brief my_module error codes
     typedef enum my_module_error_e
     
         /// No error
         MY_MODULE_ERROR_OK = 0,
    
         /// Invalid Arguments (ex: NULL pointer passed in where a valid pointer is required)
         MY_MODULE_ERROR_INVARG,
    
         /// Out of memory
         MY_MODULE_ERROR_NOMEM,
    
         /// etc. etc.
         MY_MODULE_ERROR_PROBLEM1,
      my_module_error_t;
    

    现在,不再在上面和下面的所有函数中返回 void 类型,而是返回 my_module_error_t 错误类型!

    在 .h 文件中添加一个名为 my_module_config_t 的配置结构,并将其传递给 open 函数以在创建新对象时更新内部变量。这有助于在调用 _open() 时将所有配置变量封装在单个结构中以保持整洁。

    例子:

     //--------------------
     // my_module.h
     //--------------------
    
     // my_module configuration struct
     typedef struct my_module_config_s
     
         int my_config_param_int;
         float my_config_param_float;
      my_module_config_t;
    
     my_module_error_t my_module_open(my_module_h * my_module_h_p, 
                                      const my_module_config_t *config);
    
     //--------------------
     // my_module.c
     //--------------------
    
     my_module_error_t my_module_open(my_module_h * my_module_h_p, 
                                      const my_module_config_t *config)
     
         my_module_error_t err = MY_MODULE_ERROR_OK;
    
         // Ensure the passed-in pointer is not NULL (since it is a core dump/segmentation fault
         // to try to dereference  a NULL pointer)
         if (!my_module_h_p)
         
             // Print some error or store some error code here, and return it at the end of the
             // function instead of returning void. Ex:
             err = MY_MODULE_ERROR_INVARG;
             goto done;
         
    
         // Now allocate the actual memory for a new my_module C object from the heap, thereby
         // dynamically creating this C-style "object".
         my_module_h my_module; // Create a local object handle (pointer to a struct)
         // Dynamically allocate memory for the full contents of the struct "object"
         my_module = malloc(sizeof(*my_module)); 
         if (!my_module) 
         
             // Malloc failed due to out-of-memory. Print some error or store some error code
             // here, and return it at the end of the function instead of returning void. Ex:
             err = MY_MODULE_ERROR_NOMEM;
             goto done;
         
    
         // Initialize all memory to zero (OR just use `calloc()` instead of `malloc()` above!)
         memset(my_module, 0, sizeof(*my_module));
    
         // Now initialize the object with values per the config struct passed in. Set these
         // private variables inside `my_module` to whatever they need to be. You get the idea...
         my_module->my_private_int1 = config->my_config_param_int;
         my_module->my_private_int2 = config->my_config_param_int*3/2;
         my_module->my_private_float = config->my_config_param_float;        
         // etc etc
    
         // Now pass out this object handle to the user, and exit.
         *my_module_h_p = my_module;
    
     done:
         return err;
     
    

    及用法:

     my_module_error_t err = MY_MODULE_ERROR_OK;
    
     my_module_h my_module = NULL;
     my_module_config_t my_module_config = 
     
         .my_config_param_int = 7,
         .my_config_param_float = 13.1278,
     ;
     err = my_module_open(&my_module, &my_module_config);
     if (err != MY_MODULE_ERROR_OK)
     
         switch (err)
         
         case MY_MODULE_ERROR_INVARG:
             printf("MY_MODULE_ERROR_INVARG\n");
             break;
         case MY_MODULE_ERROR_NOMEM:
             printf("MY_MODULE_ERROR_NOMEM\n");
             break;
         case MY_MODULE_ERROR_PROBLEM1:
             printf("MY_MODULE_ERROR_PROBLEM1\n");
             break;
         case MY_MODULE_ERROR_OK:
             // not reachable, but included so that when you compile with 
             // `-Wall -Wextra -Werror`, the compiler will fail to build if you forget to handle
             // any of the error codes in this switch statement.
             break;
         
    
         // Do whatever else you need to in the event of an error, here. Ex:
         // await connection of debugger, or automatic system power reset by watchdog
         while (true) ; 
     
    
     // ...continue other module initialization, and enter main loop
    

另见:

    [我的另一个答案引用了我上面的答案]Architectural considerations and approaches to opaque structs and data hiding in C

关于基于对象的 C 架构的补充阅读:

    Providing helper functions when rolling out own structures

在专业代码的错误处理中有效使用goto 的附加阅读和理由:

    支持在 C 中使用 goto 进行错误处理的论点:https://github.com/ElectricRCAircraftGuy/eRCaGuy_dotfiles/blob/master/Research_General/goto_for_error_handling_in_C/readme.md *****优秀文章展示了在 C 中的错误处理中使用 goto 的优点:“在 C 中使用 goto 进行错误处理”-https://eli.thegreenplace.net/2009/04/27/using-goto-for-error-handling-in-c Valid use of goto for error management in C? Error handling in C code

搜索词以使其更易于搜索:C 中的不透明指针、C 中的不透明结构、C 中的 typedef 枚举、C 中的错误处理、c 体系结构、基于对象的 c 体系结构、c 中初始化体系结构时的动态内存分配

【讨论】:

这个例子几乎是完美的,直到我看到.......goto。真的吗? 是的,真的。我曾经也非常反对 goto,直到我开始专业地使用它。既然我已经编写了大量的 C 代码来进行冗长而复杂的错误检查,我得出的结论是,这是处理错误检查的最佳方法,期间,并且没有等效的替代方案可以使代码变得安全、易读和简单像 goto 那样写。如果只有你和我在一起,我们可以坐在一起,我会花 1 小时 + 和你一起讨论许多例子,其中 goto 的优点以这种方式(并且只有这种方式)真正闪耀,我认为你会成为一个皈依者并使用它。 @FedericoBaù,这不是真的 (I understand that goto is something to stay far far away and everybody dislike it,),但这绝对是一个争论的领域。由于我已经在嵌入式 C 和应用程序级 C++ 方面进行过专业编程,因此我开始意识到专业开发人员(包括我自己)随着时间的推移变得非常固执己见。一些专业的软件开发团队宣称:“goto 是最好的 C 语言错误处理工具,你应该使用它。”此外,许多 C 开发人员对 C++ 怀有热情,而许多 C++ 开发人员对 C++ 中的 C 风格怀有热情。 这两种观点:C++ 开发人员讨厌 C 风格,C 开发人员讨厌 C++,在我看来都是错误的。我最喜欢的编写“C”的方法是使用 C++ 编译器,因为我可以使用 C++ 编译器编写看起来像 C(但实际上是 C++)的更漂亮的代码,而不是使用 C++ 编译器。 C 编译器。关于goto:社区分裂了。 goto 在学校被误教了。 说它是邪恶的,永远不应该被使用是......嗯......邪恶的,永远不应该说。 :) 如果使用得当,它就有它的位置。在我的答案底部的链接中查看我的文章和其他理由。 @Gabriel Staples,这一定是我表达评论的方式,但我实际上完全同意你所说的,我的意思是作为 C 的初学者并学习它,我接触到我在互联网上找到的东西是为了学习它,到目前为止,我对goto(因此我的短语)大多遇到了不好的看法。所以我碰到了你的答案,我实际上发现很有趣(因为再次,我主要看到的是“邪恶”)。我现在相信这是一个在变得更高级时最好留下的工具(所以不是我目前所处的位置)【参考方案3】:

bar(const fooRef) 声明一个不可变地址作为参数。 bar(const foo *) 声明一个不可变 foo 的地址作为参数。

出于这个原因,我倾向于选择选项 2。即,呈现的接口类型是可以在每个间接级别指定 cv-ness 的接口类型。当然,可以回避选项 1 库编写者,只使用 foo,当库编写者更改实现时,你会面临各种恐惧。 (即,选项 1 库编写者仅认为 fooRef 是不变量接口的一部分,并且 foo 可以来、去、更改等等。选项 2 库编写者认为 foo 是不变量的一部分界面。)

我更惊讶的是没有人建议组合 typedef/struct 构造。typedef struct ... foo;

【讨论】:

关于你的最后一句话,这些结构不允许不透明类型。如果您使用它们,您将在您的标头中公开结构的定义,以供调用应用程序滥用。 在这两个选项中,foo 部分界面的布局都不是。这就是以这种方式做事的全部意义所在。【参考方案4】:

选项 3:让人们选择

/*  foo.h  */

typedef struct PersonInstance PersonInstance;

typedef struct PersonInstance * PersonHandle;

typedef const struct PersonInstance * ConstPersonHandle;

void saveStuff (PersonHandle person);

int readStuff (ConstPersonHandle person);

...


/*  foo.c  */

struct PersonInstance 
    int a;
    int b;
    ...
;

...

【讨论】:

以上是关于不透明的 C 结构:声明它们的各种方式的主要内容,如果未能解决你的问题,请参考以下文章

C 结构信息隐藏(不透明指针)

具有 C 链接和 C++ 实现的不透明结构

SystemVerilog结构体

C数组声明和赋值?

结构的使用

JS中的各种排序方法