不透明的C结构:它们应该如何声明?

spl*_*cer 46 c struct coding-style typedef opaque-pointers

我已经看到以下两种在C API中声明不透明类型的样式.使用一种风格而不是另一种风格有明显的优势吗?

选项1

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

// foo.c
struct foo {
    int x;
    int y;
};
Run Code Online (Sandbox Code Playgroud)

选项2

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

// foo.c
struct _foo {
    int x;
    int y;
};
Run Code Online (Sandbox Code Playgroud)

R..*_*R.. 72

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

我见过第三种方式:

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

// foo.c
struct foo {
    int x;
    int y;
};
Run Code Online (Sandbox Code Playgroud)

如果你真的不能忍受输入struct关键字,typedef struct foo foo;(注意:摆脱无用且有问题的下划线)是可以接受的.但无论你做什么,都不要用来typedef为指针类型定义名称.它隐藏的信息非常重要的一块,只要你把它们传递给函数,并使得处理不同合格的(例如,它可以被修改的对象这种类型的引用变量const的指针一个重大的痛苦的-qualified)版本.

  • +1 for不要隐藏typedef后面的指针!(或者更糟糕的是阵列.) (26认同)
  • 类型是否是指针是**不是实现细节**.它是您可能使用该类型的任何操作的语义的基础.这是一个"永远"我完全支持的人. (19认同)
  • @R:类型是否是指针**绝对是一个实现细节**.是的,作为一个指针给它一定的语义,但**那些语义并不是指针**的特殊之处.如果我从我的库中公开一个句柄类型,并告诉你它持久地识别一个小工具,你不应该,不应该,并且**一定不在乎它是指针**还是私有全局数组的索引(或者我的图书馆内的链接列表,允许增长,或魔术.唯一重要的是它被正确记录为持久对象的标识符. (12认同)
  • "从不"在这里相当强大:不透明类型的全部意义是隐藏api用户的实现,改变后者的独立性,并通过限制用户的直接修改来提供安全措施; 在这种情况下,我看到别名指针类型或隐藏限定符没有错(即,如果它们是实现细节) (6认同)
  • @Eric:顶级`const`被从实际参数中删除,因此"const指向魔法"和"const魔法"都不会以任何方式限制库.而且它是"指向const魔法的指针"还是"指向非const魔法的指针"是一个实现细节...它至少对调用者的代码并不重要,因为他不应该触及魔法,甚至不应该取消引用指针,这是触摸魔法的必要的第一步. (4认同)
  • @Christoph:将例如`const`封装到typedef中的一个问题是,如果你想要表达一些API函数修改对象,而其他API函数没有.你可以有`typedef struct foo*foo; typedef struct const foo*const_foo;`,但这很严峻! (2认同)
  • 对于不可变字符串(或任何已分配的对象),具有内置`const`限定符的类型**无效**因为对象的实现不能"释放"`const`限定的指针(`free`需要非`const`-qualified`void*`,有充分理由).这不是技术性问题,而是违反`const`语义的问题.当然你可以在`immutable_string_free`函数中抛出`const`,但现在我们正在进入脏兮兮的领域.**任何**opaque对象分配函数都应该总是返回`footype*`,而free的函数应该是`footype*`. (2认同)

Gab*_*les 6

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

我习惯于使用Option 1,除非您将引用命名为 with_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
}
Run Code Online (Sandbox Code Playgroud)

这是在 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:
}
Run Code Online (Sandbox Code Playgroud)

简化示例用法:

#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;
}
Run Code Online (Sandbox Code Playgroud)

除此之外的唯一改进是:

  1. 实现完整的错误处理并返回错误而不是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;
    
    Run Code Online (Sandbox Code Playgroud)

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

  2. 添加一个调用my_module_config_t.h 文件的配置结构,并将其传递给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;
     }
    
    Run Code Online (Sandbox Code Playgroud)

    和用法:

     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
    
    Run Code Online (Sandbox Code Playgroud)

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

  1. 在推出自己的结构时提供辅助功能

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

  1. 支持使用gotoC 进行错误处理的论据:https : //github.com/ElectricRCAircraftGuy/eRCaGuy_dotfiles/blob/master/Research_General/goto_for_error_handling_in_C/readme.md
  2. *****优秀文章展示了goto在 C 中的错误处理中使用的优点:“在 C 中使用 goto 进行错误处理” - https://eli.thegreenplace.net/2009/04/27/using-goto-for- c 中的错误处理
  3. 在 C 中有效使用 goto 进行错误管理?
  4. C 代码中的错误处理

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

  • 这个例子几乎是完美的,直到我看到......goto。真的吗? (2认同)
  • 对真的。我曾经也非常反对 goto,直到我开始专业地使用它。现在我已经编写了大量的 C 代码,这些代码执行了冗长而复杂的错误检查,我得出的结论是,这是处理错误检查的最佳方法,期间,并且没有等效的替代方案可以使代码安全、可读且简单像 goto 那样写。如果你和我在一起,我们就可以坐在一起,我会花 1 个小时以上的时间和你一起讨论很多例子,在这些例子中,以这种方式(并且只有这种方式)使用 goto 的优点真正体现出来,我认为你会成为一个皈依者并且也使用它。 (2认同)
  • 在我看来,C++ 开发人员讨厌 C 风格和 C 开发人员讨厌 C++ 这两种观点都是错误的。我最喜欢的编写“C”的方法是使用 **C++** 编译器,因为使用 C++ 编译器可以编写比使用 C 编译器更漂亮的代码,看起来像 C(但实际上是 C++)。关于“goto”:社区是分裂的。“goto”在学校里被错误地教授。**说它是邪恶的并且永远不应该使用是......好吧......邪恶的,并且永远不应该说。:)** 如果使用得当,它有它的一席之地。请参阅我的文章和我的答案底部的链接中的其他理由。 (2认同)