72

我一直在阅读 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;
}

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

4

15 回答 15

54

sizeof(SomeStruct) != sizeof(SomeStructSource). 这导致有人找到你并在某天谋杀你。

于 2010-04-20T01:47:23.477 回答
44

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

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*),为所有内容提供初始化/销毁和访问器函数。如果您正在编写库,这确保您可以更改结构,甚至无需重新编译客户端。

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

例如

 struct SomeStruct {
  int member;
  void* internals; //allocate this to your private struct
 };
于 2010-04-20T01:57:35.777 回答
27

你几乎拥有它,但还远远不够。

在标题中:

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。这当然也可以被视为一个缺点。

于 2010-04-20T01:58:06.867 回答
20

我不建议使用公共结构模式。对于 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(在某些情况下这可能是不必要的开销),我建议您将结构隐藏在私有文件中。私人成员将是可访问的,但这取决于用户的股份。

/*********** 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 */
}
于 2013-03-11T14:17:34.927 回答
9

永远不要那样做。如果您的 API 支持将 SomeStruct 作为参数的任何东西(我期望它会这样做),那么他们可以在堆栈上分配一个并将其传递。您会在尝试访问私有成员时遇到重大错误,因为编译器为客户端类分配不包含空间。

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

于 2010-04-20T01:50:36.670 回答
7

有时确实会使用与您提出的方法类似的东西(例如,查看struct sockaddr*BSD 套接字 API 中的不同变体),但在不违反 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;
}
于 2010-04-20T02:11:03.957 回答
4

我会编写一个隐藏结构,并使用公共结构中的指针来引用它。例如,您的 .h 可能具有:

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

还有你的 .c:

typedef struct {
    int c, d;
} private_t;

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

于 2010-04-20T02:04:23.060 回答
3

使用以下解决方法:

#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
于 2015-04-05T20:40:51.447 回答
2

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

于 2010-04-20T01:51:43.720 回答
1

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

由 BSD Unix 定义的套接字 API 使用的稍微不同的方法是用于struct sockaddr.

于 2010-04-20T01:57:04.653 回答
1

我的解决方案是只提供内部结构的原型,然后在 .c 文件中声明定义。对于展示 C 接口和在后面使用 C++ 非常有用。

。H :

struct internal;

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

。C :

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

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

于 2013-10-10T07:12:38.620 回答
1

我发现bit-field如果您真的想隐藏某些东西,这可能是一个很好的解决方案。

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

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

struct person 的第一个成员是一个未命名的位域。64-bit pointer在这种情况下用于 a 。它是完全隐藏的,不能通过 struct variable name 访问

因为这个结构体中的前 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;
}

一个小的工作示例,在我的 intel mac 上进行了测试:

//
// 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;
}
于 2020-07-06T05:32:33.857 回答
0

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

编辑:我错过了它在 .c 文件中,但确实没有什么能阻止客户将其复制出来,甚至可能#include直接 .c 文件。

于 2010-04-20T01:43:52.057 回答
0

相关,但并不完全隐藏。

是有条件地弃用成员。

请注意,这适用于 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;
}
于 2014-04-02T21:07:15.610 回答
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;
}

我不建议将公共成员放在私人成员之后。初始化没有成员指示符的结构,例如 with{ 10, 20, 30 }仍然可以初始化私有成员。如果私有成员的数量发生变化,这也会默默地破坏所有没有成员指示符的初始化程序。最好总是使用成员指示符来避免这种情况。

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

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

于 2022-02-16T21:40:42.023 回答