如何在c中隐藏结构实现并避免变量同时具有不完整的类型?

Posted

技术标签:

【中文标题】如何在c中隐藏结构实现并避免变量同时具有不完整的类型?【英文标题】:How to hide the struct implementation and avoid variable has incomplete type at the same time in c? 【发布时间】:2022-01-08 15:54:41 【问题描述】:

在 InputBuffer.c 中定义 InputBuffer

typedef struct InputBuffer_t 
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
 InputBuffer;

在 InputBuffer.h 中隐藏 InputBuffer 的实现

#ifndef INPUTBUFFER_H
#define INPUTBUFFER_H

typedef struct InputBuffer_t InputBuffer;

#endif

然后在testBuffer.c中使用InputBuffer

#include "InputBuffer.h"

void testBuffer() 
   InputBuffer b = sizeof(InputBuffer);

但是,编译 testBuffer 会导致“变量的类型不完整 'struct InputBuffer'”,因为完整的 InputBuffer 实现不在 InputBuffer.h 中。

所以不知道有没有办法隐藏一个struct类型的实现,同时避免不完整的类型错误。

【问题讨论】:

此代码不应引起编译器警告。您不会取消引用指针,也不会访问任何成员。 @Tony 该代码不会产生任何错误或警告。如果您看到错误,您需要发布您尝试编译的实际代码。见minimal reproducible example。 @Cheatah 问题已修改 即使不隐藏结构定义,您添加的行也是无效的。 @GabrielStaples 当 cmets 用于获取澄清并且获得澄清时,最好删除 cmets。一旦澄清他们只是noice... 【参考方案1】:

C 中不透明结构和数据隐藏的架构考虑和方法

解决您问题中的代码:

sizeof(InputBuffer)

您不能获取隐藏结构(通常称为“不透明结构”)的大小! testBuffer.c 不知道结构的大小,因为它是隐藏的!它无权访问实现。

我也不知道你想在这里做什么:

#include "InputBuffer.h"

void testBuffer() 
   InputBuffer b = sizeof(InputBuffer);  // <=== What is this?

您不能随意为结构分配数字。


补充说明:

你的 typedef 很尴尬。

InputBuffer.c 中,执行:

typedef struct InputBuffer_s 
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
 InputBuffer_t;

然后,在 InputBuffer.htestBuffer.c 中,执行以下选项之一:

    选项 1:对您的不透明(隐藏)结构进行前向声明 typedef

    InputBuffer.h 中,执行:

    #ifndef INPUTBUFFER_H
    #define INPUTBUFFER_H
    
    // Forward declaration of the struct defined in InputBuffer.c, since this
    // header does not have access to that definition. You can therefore call this
    // an "opaque struct". It is a type of data hiding since this header now knows 
    // that `InputBuffer_t` **exists**, but doesn't know what is in it. 
    typedef struct InputBuffer_s InputBuffer_t;
    
    #endif
    

    testBuffer.c 中:

    #include "InputBuffer.h"
    
    void testBuffer(InputBuffer_t *inputBuffer) 
    
    
    

    选项 2:对不透明(隐藏)结构的 指针 进行前向声明 typdef。这个 typedef 现在是一个以 pointer 形式指向结构的“句柄”

    有些人不推荐这个选项,尽管我以前在一些高质量、安全关键、实时的 C 代码库中专业地使用过它。

    例如,

    @Lundin 强烈建议反对使用这种技术in their comment below this answer,其中指出:

    我强烈反对将指针隐藏在 typedef 后面的建议,即使它们是不透明的。将指针隐藏在 typedef 后面通常是非常糟糕的,但我们也从经验中知道,将不透明的指针隐藏在 typedef 后面会导致糟糕的 API。特别是带有 HANDLE、HWND 和其他奇怪类型的 Windows API,它们会导致程序员通过引用 HANDLE* 依次传递这些类型,从而不必要地创建多个间接级别,因此总体上代码速度较慢且可读性较差。

    这是一个很好的观点。因此,我建议您考虑typedef将指向结构的指针放入“句柄”中,如下所示

      整个代码库中_h 命名的“句柄”的唯一类型是指针,因此所有_h 命名的句柄都清楚地称为指针。 您确保开发人员知道代码库中所有_h 命名的“句柄”都是指针,因此他们不会不必要地对它们进行引用(在 C++ 中)或指针(在 C 或 C++ 中)。李>

    考虑到以上 2 点,我使用了这种“处理”技术并且对它很好,尽管我可以理解反对它的论点。你可以在这里看到我在我的回答中使用它:Opaque C structs: various ways to declare them

    InputBuffer.h 中,执行:

    #ifndef INPUTBUFFER_H
    #define INPUTBUFFER_H
    
    // InputBuffer_h is a "handle", or pointer to an opaque struct; 
    // AKA: InputBuffer_h is an "opaque pointer", meaning it is a pointer
    // to a struct whose implementation is hidden. This is true data-hiding
    // in C.
    typedef struct InputBuffer_s *InputBuffer_h;
    
    #endif
    

    testBuffer.c 中:

    #include "InputBuffer.h"
    
    void testBuffer(InputBuffer_h inputBuffer) 
    
    
    

但是,无论您在上面选择哪个选项,您都无法对 inputBuffer 参数执行任何操作,因为您无法取消引用它,也无法访问“testBuffer.c”中的任何成员,因为它的实现是隐藏并在您未包含的不同源文件 (InputBuffer.c) 中定义!

好方法 1 [这确实是比上面更好的方法]:将结构定义放在需要完整定义的同一个源文件中

所以不知道有没有办法隐藏一个struct类型的实现,同时避免不完整的类型错误。

所以,你应该在InputBuffer.h中声明你需要访问实现的函数原型,然后在InputBuffer.c中编写函数定义,这样它们就有了访问不透明结构的实现细节,因为结构是在 InputBuffer.c 中定义的。

看起来像这样,例如:

InputBuffer.h 中,执行:

#ifndef INPUTBUFFER_H
#define INPUTBUFFER_H

// Forward declaration of the struct defined in InputBuffer.c, since this
// header does not have access to that definition. You can therefore call this
// an "opaque struct". It is a type of data hiding since this header now knows 
// that `InputBuffer_t` **exists**, but doesn't know what is in it. 
typedef struct InputBuffer_s InputBuffer_t;

// put any public function prototypes (declarations) you may need here

#endif

InputBuffer.c中:

#include "InputBuffer.h"

// Full struct definition; no need to typedef this here since it's already 
// typedef'ed in InputBuffer.h, which is included above.
struct InputBuffer_s 
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
;

void testBuffer(InputBuffer_t *inputBuffer) 
    // Now you have full access to the size of the `InputBuffer_t`, and its 
    // members, since the full definition of this struct is above.

vvvvvvvvv这是我写的关于我如何使用不透明指针/结构来使用和编写“基于对象”的 C 架构的更彻底的答案:Opaque C structs: various ways to declare them ^^^^^^^^^

好方法 2 [上述方法的替代方法]:将您的结构定义放在 _private.h 头文件中,您将只将其包含在需要结构完整定义的其他源文件中

请注意,使用单源文件不透明指针/结构体系结构的替代方法(不透明指针/不透明结构体系结构经常需要使用动态内存分配malloc(),正如我在上面链接到的其他答案中所展示的那样)是简单地包含在以_private.h 为后缀的标头中定义的“隐藏”实现,例如myheader_private.h这意味着这些“私有”标头应该只包含在需要查看“隐藏”结构的完整定义的源文件中,而绝不应该由 API 的用户直接包含。 这是一种稍微不那么强大的数据隐藏形式,但具有让您对结构定义的完全访问权限对多个源文件的优势。

例子:

InputBuffer_private.h(“私有”头文件)中,执行:

// THIS "PRIVATE" HEADER SHOULD ONLY BE INCLUDED BY SOURCE FILES WHICH NEED FULL
// ACCESS TO THE STRUCT DEFINITION BELOW. It should NOT generally be include by
// regular users of your API, since your architectural goal is probably to have
// some level of data hiding to hide the contents of this struct from your
// regular API users. 

#ifndef INPUTBUFFER_PRIVATE_H
#define INPUTBUFFER_PRIVATE_H

// Full struct definition. No need to typedef it here since it will be 
// typedefed in the "public" header file below.
struct InputBuffer_s 
  char* buffer;
  size_t buffer_length;
  ssize_t input_length;
;

#endif

InputBuffer.h(“公共”头文件)中,执行:

#ifndef INPUTBUFFER_H
#define INPUTBUFFER_H

// Do your choice of Option 1 or 2 above, to expose the **existence** of the 
// opaque struct to the user of the API:
typedef struct InputBuffer_s InputBuffer_t;  // Option 1
// OR:
typedef struct InputBuffer_s *InputBuffer_h; // Option 2

#endif

InputBuffer.c中:

#include "InputBuffer.h"

#include "InputBuffer_private.h" // <==== NOTICE THIS ADDITION!


void testBuffer(InputBuffer_t *inputBuffer) 
    // Now you have full access to the size of the `InputBuffer_t`, and its 
    // members, since the full definition of this struct is **INCLUDED** above.

您还可以根据需要将完整的结构定义提供给其他源文件:

例如:在 SomeOtherSource.c

#include "SomeOtherSource.h"

#include "InputBuffer_private.h" // to expose the details of the opaque struct

// Now you can have full access to the size of the `InputBuffer_t`, and access
// to all of its members, as needed, in any function below.

// Your functions here

最后注意事项:如果您在另一个“公共”头文件中包含任何 _private.h 头文件,您只是丢失了数据隐藏

如果您不希望真正的数据隐藏,包括任何_private.h 头文件在另一个“公共”(旨在被 API 用户包含)头文件中,将向 API 用户公开完整的结构定义,并且所有真正的数据隐藏都丢失了!

这是一种有效的架构方法,您可以根据需要选择采用这种方法。优点是您现在可以为所有结构使用静态内存分配,而不是像不透明指针(又名:不透明结构)那样需要动态内存分配。

现在,您有 2 个选择:

    保留标题名称的_private.h 部分。这是一种“软数据隐藏”方法,它告诉公共 API 的用户该标头打算是私有的,他们不应该直接访问其中的内容虽然他们在技术上可以。这是一种完全有效的方法,而且,这个方法和下面的选项现在都允许您为所有这些结构使用完整的静态内存分配,这很棒。
      这本质上是 Python 的工作方式:您只需在任何想要“私有”的函数名称前加上 _,即使 Python 不支持真正的数据隐藏,并且任何导入模块的人都可以访问所有“私有”成员如果他们真的愿意。
    如果您不想再隐藏任何数据,请删除标题名称的 _private.h 部分。结构定义现在既完全公开 并且 打算完全公开。现在任何人都可以在任何地方包含这个标题,这很好。包含标头的任何人都可以完全使用结构定义,并且您希望公共 API 的用户也能够做到这一点。这也很好,具体取决于您要采用的架构方法。选择权在你。

不要_private.h 后缀留在头文件的末尾,并在其中包含结构定义,并允许公共 API 的用户直接包含您的 _private.h 标头。这违反了 API 的设计意图。相反,要么删除 _private.h 后缀并允许公共 API 的用户直接包含它,要么根据上述方法之一保留该后缀并仅包含 _private.h 文件(在私有源文件中以实现真正的数据隐藏, 或在公共头文件中用于伪数据隐藏,就像 Python 中存在的那样)。

另见

    再次,请参阅我在此处的另一个答案,以获取一个“句柄”(指向结构的典型指针)样式技术的完整示例。该技术还展示了根据需要使用动态内存分配(通过malloc())创建不透明结构的完整方法:Opaque C structs: various ways to declare them

【讨论】:

@user3386109,是的,你是对的。我将我的措辞从“错误”更新为“尴尬”。我怀疑 OP 没有透露他们的完整代码,并且他们试图在某个地方访问隐藏(不透明)结构的实现细节,从而导致错误。 @user3386109....就在这里!他们刚刚用他们的代码有什么问题更新了他们的问题。 @user3386109,同意。我同意你的观点,当你说他们的代码没有问题时,你是对的,当你说它现在无效时,你是对的。我同意这两种情况。他们添加了一行 now 使其无效。最初,他们的代码很笨拙,但很好,并且可以按照他们最初在原始问题中编写的方式进行编译。 我强烈反对将指针隐藏在 typedef 后面的建议,即使它们是不透明的。将指针隐藏在 typedef 后面通常是非常糟糕的,但我们也从经验中知道,将不透明的指针隐藏在 typedef 后面会导致糟糕的 API。特别是带有 HANDLE、HWND 和其他奇怪类型的 Windows API,这些类型会导致程序员通过引用 HANDLE* 依次传递这些类型,从而不必要地创建多个间接级别,因此整体代码速度较慢且可读性较差。 @Lundin,点了。我大量更新了我的答案,以包括您引用的评论和在将指向结构的指针定义为“句柄”时要考虑的一些项目符号。【参考方案2】:

通过前向声明进行私有封装的缺点是调用者获得了一个不完整的类型,事实就是如此。调用者必须使用指针类型。

如果您出于某种原因需要在封装之外公开结构的大小,则必须为此目的设计一个 getter 函数。示例:

InputBuffer* b = InputBufferCreate();
size_t size = InputBufferGetSize(b);

更多信息在这里:How to do private encapsulation in C?

【讨论】:

以上是关于如何在c中隐藏结构实现并避免变量同时具有不完整的类型?的主要内容,如果未能解决你的问题,请参考以下文章

C语言如何使用其他文件定义的结构体?(C++报错:无法转换到不完整的类需在头文件中定义结构体??)

C 定义的结构数组的元素在编译为 C++ 时具有不完整的类型

如何读取具有动态名称的文件,同时避免在 R 中进行硬编码?

结构体怎么定义

C语言 不允许使用不完整的类型

如何避免名称混淆?