在 C 结构中隐藏成员

我一直在阅读关于 C 语言中的 OOP 的文章,但是我从来不喜欢你不能像在 C + + 中那样拥有私有数据成员。但我突然想到,你可以创建两个结构。一个在头文件中定义,另一个在源文件中定义。

// =========================================
// 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;
}

从这里你可以将一个结构转换为另一个结构。 这被认为是不好的做法吗? 还是经常这样做?

45336 次浏览

不是很私有,因为调用代码可以回溯到 (SomeStructSource *)。另外,当您想要添加另一个公共成员时会发生什么?你必须打破二进制兼容性。

编辑: 我错过了它是在一个。文件,但是没有什么可以阻止客户端复制它,甚至可能是 #include0。C 文件。

这个 威尔导致有人找到你并在某一天杀了你。

千万别这么做。如果您的 API 支持任何将 Somstruct 作为参数的东西(我希望它会这样做) ,那么他们可以在堆栈上分配一个参数并将其传递进来。试图访问私有成员时会出现重大错误,因为编译器为客户端类分配的成员不包含该成员的空间。

在 struct 中隐藏成员的经典方法是使其为 void * 。它基本上是一个只有实现文件才知道的句柄/cookie。几乎每个 C 库都对私有数据这样做。

有更好的方法可以做到这一点,比如在公共结构中使用指向私有结构的 void *指针。你这样做是在愚弄编译器。

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

BSDUnix 定义的套接字 API 使用的略有不同的方法是 struct sockaddr使用的样式。

就我个人而言,我更喜欢这样:

typedef struct {
int _public_member;
/*I know you wont listen, but don't ever touch this member.*/
int _private_member;
} SomeStructSource;

毕竟,如果人们想搞砸,他们应该被允许——没有必要隐瞒什么,除了:

如果您需要的是保持 ABI/API 兼容,那么从我所见到的情况来看,有两种方法更为常见。

  • 不要让你的客户端访问 struct,给他们一个不透明的句柄(一个带有漂亮名字的 void *) ,为所有东西提供 init/delete 和 accessor 函数。这确保你可以改变 如果您正在编写一个库,那么这个结构甚至不需要重新编译客户端。

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

例如:

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

你差点就成功了,但还不够。

标题:

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 ...

关键是,现在消费者已经了解了 Somstruct 的内部构造,您可以随意更改它,添加和删除成员,甚至不需要消费者重新编译。它们也不能“意外地”直接删除成员,或者在堆栈上分配某个结构体。当然,这也可以被视为一种劣势。

我将编写一个隐藏结构,并使用公共结构中的指针引用它。例如,你的。我本可以:

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

还有你的 C:

typedef struct {
int c, d;
} private_t;

它显然不能防止指针算法,并且为分配/释放增加了一点开销,但是我想这已经超出了问题的范围。

有时确实会使用与您提出的方法类似的东西(例如,参见 BSD 套接字 API 中 struct sockaddr*的不同变体) ,但是在不违反 C99严格的别名规则的情况下使用它几乎是不可能的。

然而,你可以这样做:

返回文章页面

struct SomeStructPrivate; /* Opaque type */


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

返回文章页面

#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;
}

我不建议使用公共结构模式。对于 C 语言中的 OOP 来说,正确的设计模式是提供访问每个数据的函数,而不允许公共访问数据。类数据应该在源中声明,以便成为私有数据,并以转发方式引用,其中 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 (在某些情况下可能会产生不必要的开销) ,我建议您将 struct 隐藏在一个私有文件中。私人成员将可以访问,但这取决于用户的利益。

/*********** 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 */
}

我的解决方案是只提供内部结构的原型,然后在。文件。非常有用的显示 C 接口和使用 C + + 背后。

。 h:

struct internal;


struct foo {
int public_field;
struct internal *_internal;
};

C:

struct internal {
int private_field; // could be a C++ class
};

注意: 在这种情况下,变量必须是一个指针,因为编译器无法知道内部结构的大小。

相关的,但不是完全隐藏。

是有条件地废弃成员。

请注意,这适用于 GCC/Clang,但是 MSVC 和其他编译器也不赞成, 所以有可能提出一个更便携的版本。

如果使用相当严格的警告进行生成,或者将警告作为错误进行生成,那么这至少可以避免意外使用。

// =========================================
// in somestruct.h


#ifdef _IS_SOMESTRUCT_C
#  if defined(__GNUC__)
#    define HIDE_MEMBER __attribute__((deprecated))
#  else
#    define HIDE_MEMBER  /* no hiding! */
#  endif
#else
#  define HIDE_MEMBER
#endif


typedef struct {
int _public_member;
int _private_member  HIDE_MEMBER;
} SomeStruct;


#undef HIDE_MEMBER




// =========================================
// in somestruct.c
#define _IS_SOMESTRUCT_C
#include "somestruct.h"


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

使用以下解决办法:

#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

我发现 bit-field可能是一个很好的解决方案,如果你真的想 藏起来的东西。

struct person {
unsigned long :64;
char          *name;
int           age;
};


struct wallet {
char *currency;
double balance;
};

Struct person 的第一个成员是一个未命名的位字段。在这种情况下用于 64-bit pointer。这是完全隐藏和 不能通过 struct 变量名访问

因为这个结构中的第一个64位是未使用的,所以我们可以将它用作私有指针。我们可以通过这个成员的内存地址而不是变量名来访问它。

void init_person(struct person* p, struct wallet* w) {
*(unsigned long *)p = (unsigned long)w;
// now the first 64-bit of person is a pointer of wallet
}


struct wallet* get_wallet(struct person* p) {
return (struct wallet*)*(unsigned long *)p;
}

一个小例子,在我的电脑上测试过:

//
// Created by Rieon Ke on 2020/7/6.
//


#include <stdlib.h>
#include <string.h>
#include <assert.h>




#if __x86_64__ || __LP64__
#define PRIVATE_SET(obj, val) *(unsigned long *) obj = (unsigned long) val;
#define PRIVATE_GET(obj, type) (type)*(unsigned long *) obj;
#define PRIVATE_POINTER unsigned long:64
#else
#define PRIVATE_SET(obj, val) *(unsigned int *) obj = (unsigned int) val;
#define PRIVATE_GET(obj, type) (type)*(unsigned int *) obj;
#define PRIVATE_POINTER unsigned int:32
#endif


struct person {
PRIVATE_POINTER;
char *name;
int age;
};


struct wallet {
char *currency;
double balance;
};


int main() {


struct wallet w;
w.currency = strdup("$$");
w.balance = 99.9;


struct person p;
PRIVATE_SET(&p, &w) //set private member


p.name = strdup("JOHN");
p.age = 18;


struct wallet *pw = PRIVATE_GET(&p, struct wallet*) //get private member


assert(strcmp(pw->currency, "$$") == 0);
assert(pw->balance == 99.9);


free(w.currency);
free(p.name);


return 0;
}

这里可以使用匿名结构。

#ifndef MYSTRUCT_H
#define MYSTRUCT_H


typedef struct {
int i;
struct {
int j;
} MYSTRUCT_PRIVATE;


// NOTE: Avoid putting public members after private
int k;
} MyStruct;


void test_mystruct();


#endif

在任何应该有权访问私有成员的文件中,在包含此头之前将 MYSTRUCT_PRIVATE定义为空令牌。在这些文件中,私有成员位于匿名结构中,可以使用 m.j访问,但在其他所有地方,只能使用 m.MYSTRUCT_PRIVATE.j访问它们。

#define MYSTRUCT_PRIVATE
#include "mystruct.h"


void test_mystruct() {
// Can access .j without MYSTRUCT_PRIVATE in both
// initializer and dot operator.
MyStruct m = { .i = 10, .j = 20, .k = 30 };
m.j = 20;
}
#include <stdio.h>
#include "mystruct.h"


int main() {
// You can declare structs and, if you jump through
// a small hoop, access private members
MyStruct m = { .i = 10, .k = 30 };
m.MYSTRUCT_PRIVATE.j = 20;


// This will not work
//MyStruct m2 = { .i = 10, .j = 20, .k = 30 };


// But this WILL work, be careful
MyStruct m3 = { 10, 20, 30 };


test_mystruct();


return 0;
}

我不建议将公共成员置于私人成员之后。初始化没有成员指示符的结构(如使用 { 10, 20, 30 })仍然可以初始化私有成员。如果私有成员的数量发生变化,这也将无声地中断所有没有成员指示符的初始化器。为了避免这种情况,最好始终使用成员指示符。

您必须将结构,特别是私有成员设计为初始化为零,因为没有 C + + 中的自动构造函数。只要成员被初始化为0,那么即使没有初始化函数,它们也不会处于无效状态。除非进行成员指示符初始化,否则将初始化设计为简单的 { 0 }应该是安全的。

我所发现的唯一缺点是,这会干扰调试器和代码完成之类的事情,当一种类型在一个文件中有一组成员,而在另一个文件中有不同的成员时,它们通常不喜欢这样。

这里有一个非常有组织的方法来使用宏。这是我在一些大型项目中看到的使用方法。我假设如下:

  • 包含结构的头文件
  • 可访问私有字段的源文件
  • 不能访问私有字段的源文件(字段存在但被重命名)。

头文件:

// You can put this part in a header file
// and share it between multiple header files in your project
#ifndef ALLOW_PRIVATE_ACCESS
#define PRIVATE(T) private_##T
#else
#define PRIVATE(T) T
#endif
#define PUBLIC(T) T


typedef struct {
int PRIVATE(m1); // private member
int PUBLIC(m2); // public member
} mystruct;


mystruct *mystruct_create(void);
int mystruct_get_m1(mystruct *t);

可访问私有字段的源文件:

#include <stdlib.h>
#define ALLOW_PRIVATE_ACCESS
#include "mystruct.h"


mystruct *mystruct_create(void) {
mystruct *p = (mystruct *)malloc(sizeof(mystruct));
p->m1 = 42; // works (private)
p->m2 = 34; // works (public)
return (mystruct *)p;
}


int mystruct_get_m1(mystruct *t) {
return t->m1; // works (private)
}

不能访问私有字段的源文件:

#include <stdio.h>
#include <stdlib.h>
#include "mystruct.h"


int main() {
mystruct *t = mystruct_create();
printf("t->m1 = %d\n", t->m1); // error (private)
printf("t->m1 = %d\n", mystruct_get_m1(t)); // works (using function)
printf("t->m2 = %d\n", t->m2); // works (public)
free(t);
return 0;
}