在C结构体中隐藏成员

78

我一直在读关于在C中使用面向对象编程(OOP)的文章,但是我从来不喜欢它不能像C++那样拥有私有数据成员。但是后来我想到可以创建2个结构体,一个在头文件中定义,另一个在源文件中定义。

// =========================================
// in somestruct.h
typedef struct {
  int _public_member;
} SomeStruct;

// =========================================
// in somestruct.c

#include "somestruct.h"

typedef struct {
  int _public_member;
  int _private_member;
} SomeStructSource;

SomeStruct *SomeStruct_Create()
{
  SomeStructSource *p = (SomeStructSource *)malloc(sizeof(SomeStructSource));
  p->_private_member = 42;
  return (SomeStruct *)p;
}

你可以直接将一个结构体转换为另一个结构体。 这被认为是不好的实践吗?还是经常这样做?


1
为什么要把它搞得这么复杂 - 如果工具不能满足你的需求,就换一个工具。 - Romain Hippeau
我认为这会违反对象别名规则,至少在C99中是如此。在C++中,我知道也是这样的。 - James McNellis
39
关闭这个问题会很糟糕!为什么有人要对这样一个有效的问题投票关闭?是因为他们忘记了如何使用C语言吗? - Heath Hunnicutt
3
不要试图隐藏东西。只需使用名称来表示“私有”成员,使其绝对清晰,没有人可以触碰它们。如果有人使用它们,请告诉他们。或更改名称以教育他们。 - gnasher729
16个回答

65

sizeof(SomeStruct)!= sizeof(SomeStructSource)。这将会导致有人在某一天找到你并杀了你。


26
任何陪审团都会在此后释放他们。 - gnud
31
永远以编写代码的方式,就好像最终维护你的代码的人是一个知道你住在哪里的狂暴精神病患者。 (引自 Rick Osborne) - Dietrich Epp

49
个人而言,我更喜欢这种方式:
typedef struct {
  int _public_member;
  /*I know you wont listen, but don't ever touch this member.*/
  int _private_member;
} SomeStructSource;

毕竟是C语言,如果人们想把它搞砸,他们应该被允许这样做-没有必要隐藏东西,除非:

如果您需要保持ABI / API兼容性,则有两种方法更常见。

  • 不要向客户端提供结构体,而是提供一个不透明的句柄(带有漂亮名称的void *),为所有内容提供init / destroy和访问器函数。这可以确保在编写库时即使未重新编译客户端也可以更改结构。

  • 作为您结构体的一部分提供不透明的句柄,您可以根据需要分配。这种方法甚至在C ++中用于提供ABI兼容性。

例如:

 struct SomeStruct {
  int member;
  void* internals; //allocate this to your private struct
 };

2
我发现一个非常糟糕的设计,允许客户端访问结构体的任何成员。整个结构体应该是私有的。对其成员的访问应该通过访问器和设置器完成。 - Felipe Lavratti
9
用C语言这样做有很多影响,比如以这种方式隐藏一个结构体,在堆栈上分配它或将其作为另一个结构体的成员进行内联变得相当困难。摆脱这种困境的简单方法是动态分配结构体并仅公开void*或句柄,虽然在某些情况下这样做可能没问题,但在许多情况下,这种影响太大了,会阻碍您利用C语言提供的优势。 - nos
1
在我看来,这里给出的第二个示例应该是答案,只需记得提供析构函数。 - Luis
在性能关键的情况下,避免使用 void * 所暗示的跳转,而是直接内联分配私有数据 - 隐私权被忽略(在这种情况下,您只能添加下划线作为前缀)。 - Engineer
1
您的第一个建议实际上是一个很好的建议。我们公司的做法是struct S { int x; // PRIVATE_FIELD };,然后我们自己编写了C代码分析器来检查注释,如果看到"PRIVATE_FIELD"注释,就会防止用户为该字段键入S.x - mercury0114
显示剩余3条评论

31
你已经很接近了,但还差一点。
在头部中:
struct SomeStruct;
typedef struct SomeStruct *SomeThing;


SomeThing create_some_thing();
destroy_some_thing(SomeThing thing);
int get_public_member_some_thing(SomeThing thing);
void set_public_member_some_thing(SomeThing thing, int value);
在.c文件中:
struct SomeStruct {
  int public_member;
  int private_member;
};

SomeThing create_some_thing()
{
    SomeThing thing = malloc(sizeof(*thing));
    thing->public_member = 0;
    thing->private_member = 0;
    return thing;
}

... etc ...

重点是,现在消费者对SomeStruct的内部没有任何了解,您可以毫不顾虑地更改它,随意添加和删除成员,甚至无需消费者重新编译。他们也无法“意外”直接搞乱成员,或在堆栈上分配SomeStruct。当然,这也可以被视为一个缺点。


20
有些人认为使用typedef来隐藏指针不是一个好主意,特别是因为SomeStruct *需要被释放,而SomeThing看起来像一个普通的堆栈变量。实际上,你仍然可以声明struct SomeStruct;,只要你不定义它,人们就会被迫使用SomeStruct *指针而无法解引用其成员,从而达到相同的效果而不隐藏指针。 - Chris Lutz

20

我不建议使用公共结构体模式。在 C 的面向对象编程中,正确的设计模式是提供函数来访问每个数据,永远不允许公共访问数据。类数据应该在源代码中声明为私有,并以前向方式引用,在其中 CreateDestroy 分别完成数据的分配和释放。这样,公共/私有困境就不会再存在。

/*********** header.h ***********/
typedef struct sModuleData module_t' 
module_t *Module_Create();
void Module_Destroy(module_t *);
/* Only getters and Setters to access data */
void Module_SetSomething(module_t *);
void Module_GetSomething(module_t *);

/*********** source.c ***********/
struct sModuleData {
    /* private data */
};
module_t *Module_Create()
{
    module_t *inst = (module_t *)malloc(sizeof(struct sModuleData));
    /* ... */
    return inst;
}
void Module_Destroy(module_t *inst)
{
    /* ... */
    free(inst);
}

/* Other functions implementation */

另一方面,如果您不想使用Malloc/Free(这可能会在某些情况下造成不必要的开销),我建议您将结构体隐藏在一个私有文件中。私有成员将是可访问的,但用户需承担其风险。

/*********** privateTypes.h ***********/
/* All private, non forward, datatypes goes here */
struct sModuleData {
    /* private data */
};

/*********** header.h ***********/
#include "privateTypes.h"
typedef struct sModuleData module_t; 
void Module_Init(module_t *);
void Module_Deinit(module_t *);
/* Only getters and Setters to access data */
void Module_SetSomething(module_t *);
void Module_GetSomething(module_t *);

/*********** source.c ***********/
void Module_Init(module_t *inst)
{       
    /* perform initialization on the instance */        
}
void Module_Deinit(module_t *inst)
{
    /* perform deinitialization on the instance */  
}

/*********** main.c ***********/
int main()
{
    module_t mod_instance;
    module_Init(&mod_instance);
    /* and so on */
}

3
这种方法的一个困难之处在于,即使在应该可以简单地将结构创建为堆栈变量并在方法退出时消失的情况下,它也需要使用malloc / free。对于那些应该具有堆栈语义的东西使用malloc / free可能会导致内存碎片化,如果在创建/销毁之间的其他代码需要创建持久性对象。如果提供一种方法来使用传入的存储块来保存对象,并且为此目的typedef一个int[]适当的大小,则可以缓解这样的问题。 - supercat
1
这是一个合理的方法。更加强制正确使用的方法是定义一个 typedef int module_store_t[20];,然后有一个 module_t *Module_CreateIn(module_store_t *p)。代码可以创建一个类型为 module_store_t 的自动变量,然后使用 Module_CreateIn 从中派生出一个新初始化模块的指针,其生命周期将与自动变量相匹配。 - supercat
第二种方法并不能帮助实现结构体封装。不幸的是,它仍然允许直接引用私有结构体成员!请在您的代码中尝试一下。 - Adi
2
尽管我喜欢C语言的简单性,但在应用设计模式时它非常令人烦恼。C语言和设计模式根本不兼容。令人沮丧的是,在C语言存在了40年之后,没有一种技术可以让你在C语言中使用最佳实践编码规则。如果我们忽略堆栈分配问题,ADT技术确实可以使用,但只有在有适当的malloc实现且不会引起任何碎片问题的情况下才能使用。我真的很惊讶,竟然没有标准的C库实现<待续>。 - Adi
这些解决方案是围绕这个问题量身定制的。我主要是在嵌入式系统的背景下谈论,C语言现在被广泛使用,内存碎片化是一个大问题。只是说,我不满意那些使用静态分配对象而不是malloc的解决方案。是的,可以说我对这样的C语言“特性”非常恼火 :/ - Adi
显示剩余3条评论

9
永远不要这样做。如果您的API支持以SomeStruct作为参数(我期望它确实支持),那么客户端可以在堆栈上分配一个SomeStruct并将其传递进去。由于编译器为客户端类分配的SomeStruct不包含私有成员,因此尝试访问私有成员时会出现主要错误。

隐藏结构体中成员的经典方法是将其作为void*。它基本上是一个句柄/cookie,只有您的实现文件知道。几乎每个C库都会为私有数据执行此操作。

8

您提出的方法有时确实会被使用(例如,请参见BSD sockets API中不同变体的struct sockaddr*),但几乎不可能在不违反C99的严格别名规则的情况下使用。

然而,您可以安全地这样做:

somestruct.h

struct SomeStructPrivate; /* Opaque type */

typedef struct {
  int _public_member;
  struct SomeStructPrivate *private;
} SomeStruct;

somestruct.c:

#include "somestruct.h"

struct SomeStructPrivate {
    int _member;
};

SomeStruct *SomeStruct_Create()
{
    SomeStruct *p = malloc(sizeof *p);
    p->private = malloc(sizeof *p->private);
    p->private->_member = 0xWHATEVER;
    return p;
}

4

我会写一个隐藏的结构体,并在公共结构体中使用指针引用它。例如,你的 .h 文件可以包含以下内容:

typedef struct {
    int a, b;
    void *private;
} public_t;

还有你的.c文件:

typedef struct {
    int c, d;
} private_t;

显然,它不能保护指针算术,并且为分配/释放添加了一些开销,但我认为这超出了问题的范围。


2
请使用以下解决方法:
#include <stdio.h>

#define C_PRIVATE(T)        struct T##private {
#define C_PRIVATE_END       } private;

#define C_PRIV(x)           ((x).private)
#define C_PRIV_REF(x)       (&(x)->private)

struct T {
    int a;

C_PRIVATE(T)
    int x;
C_PRIVATE_END
};

int main()
{
    struct T  t;
    struct T *tref = &t;

    t.a = 1;
    C_PRIV(t).x = 2;

    printf("t.a = %d\nt.x = %d\n", t.a, C_PRIV(t).x);

    tref->a = 3;
    C_PRIV_REF(tref)->x = 4;

    printf("tref->a = %d\ntref->x = %d\n", tref->a, C_PRIV_REF(tref)->x);

    return 0;
}

结果如下:

t.a = 1
t.x = 2
tref->a = 3
tref->x = 4

有趣的是,这种方法隐藏了“private”结构的声明,但如果您获取T结构的实例,则允许访问私有部分。 - mkonvisar
这将会出现在我的Udemy教程中。太棒了! - Abhishek Sagar

2

有更好的方法来做这件事,比如在公共结构体中使用 void * 指针指向私有结构体。你现在的做法是在愚弄编译器。


2

这种方法是有效的、有用的、标准的 C 语言。

套接字 API 使用了稍微不同的方法,这种方法是由 BSD Unix 定义的,它是用于 struct sockaddr 的样式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接