如何在C语言中隐藏结构体的某些字段?

19

我正在尝试实现一个person结构体,并且需要隐藏一些字段或使它们成为常量。一个用于创建私有字段的技巧。

头文件:

#pragma once

#define NAME_MAX_LEN 20

typedef struct _person {
    float wage;
    int groupid;
} Person;

const char const *getName (Person *p);
int getId (Person *p);

/// OTHER FUNCTIONS

来源

#include "person.h"


struct _person
{
    int id;

    float wage;
    int groupid;

    char name[NAME_MAX_LEN];
};

/// FUNCTIONS

GCC 报告错误:person.c:7:8: error: redefinition a 'struct _person' struct _person

我可以把这个写在头文件里,但之后我无法使用结构体的字段。

typedef struct _person Person;

11
C语言不允许你有选择地隐藏字段,在这里没有“private”的概念。 - user2357112
@user2357112 如何保护我的变量(idname)不被编辑? - Wootiae
1
在该网站的其他评论中,对于“不要以为我让你看到内部就可以随意更改”的问题,有一种不寻常(不是BOFH)的方法:https://dev59.com/dV0Z5IYBdhLWcg3wrB2H#2qygEYcBWogLw_1beX0q - dmckee --- ex-moderator kitten
5个回答

24

结构体不能有多个冲突的定义。因此,您不能创建一个隐藏某些字段的结构体。

但是,您可以在头文件中声明该结构体而不定义它。然后,调用方只能使用指向该结构体的指针并使用您实现中的函数来修改它。

例如,您可以如下定义您的头文件:

typedef struct _person Person;

Person *init(const char *name, int id, float wage, int groupid);

const char *getName (const Person *p);
int getId (const Person *p);
float getWage (const Person *p);
int getGroupid (const Person *p);

而你的实现将包含:

#include "person.h"

struct _person
{
    int id;

    float wage;
    int groupid;

    char name[NAME_MAX_LEN];
};

Person *init(const char *name, int id, float wage, int groupid)
{
    Person *p = malloc(sizeof *p);
    strcpy(p->name, name);
    p->id = id;
    p->wage= wage;
    p->groupid= groupid;
    return p;
}

...

我会添加const指针:int getId (const Person *p); 这样函数就可以使用常量指针调用(因为它们只是获取器)。 - Jean-François Fabre
1
@Jean-FrançoisFabre 好主意。已更新。另外,恭喜你获得钻石! - dbush
我可以展示“wage”和“groupid”吗?以便使用“p->wage”? - Wootiae
@Wootiae 不在调用代码中,因为它不知道 Person 包含什么。您的实现需要一个访问器函数来允许用户读取它。 - dbush
如何在其中使用 sizeof - Wootiae
@Wootiae 你无法从实现外部进行操作,因为该类型尚未完全定义。你也不应该这样做,因为实现应该执行任何需要完成的操作。 - dbush

19
C没有针对结构体成员的隐藏机制。但是,通过仅使用指向该类型的指针并不提供定义,您可以将整个类型设置为不透明。然后,用户必须使用您提供的函数以任何方式操作实例。有时候会这样做。
在某种程度上,您可能能够通过隐藏上下文来实现类似于您所描述的内容。例如,请考虑以下内容: header.h
typedef struct _person {
    float wage;
    int groupid;
} Person;

implementation.c

struct _person_real {
    Person person;  // must be first, and is a structure, not a pointer.
    int id;
    char name[NAME_MAX_LEN];
};

现在你可以这样做:

Person *create_person(char name[]) {
    struct _person_real *pr = malloc(sizeof(*pr));

    if (pr) {
        pr->person.wage = DEFAULT_WAGE;
        pr->person.groupid = DEFAULT_GROUPID;
        pr->id = generate_id();
        strncpy(pr->name, name, sizeof(pr->name));
        pr->name[sizeof(pr->name) - 1] = '\0';

        return &pr->person;  // <-- NOTE WELL
    } else {
        return NULL;
    }
}
结构体的第一个成员的指针总是也指向整个结构体,所以如果客户端将从该函数获得的指针传回给您,您也可以使用它来访问整个结构体。
struct _person_real *pr = (struct _person_real *) Person_pointer;

同时要从更广泛的上下文中考虑成员。

但是请注意,这种方案是有风险的。没有任何防止用户创建一个Person对象时缺少更大上下文的情况,然后将其指针传递给期望存在上下文对象的函数。还有其他问题。

总体而言,C API通常采用不透明结构方法或仅仔细记录客户端可以访问的数据操作,甚至只是记录如何工作,以便用户可以自行选择。尤其是后者,与整体C的方法和惯例非常吻合--C不会扶着你,并保护你免受伤害。它相信你知道自己在做什么,并且只做你打算做的事。


只需记录所有操作的工作原理,以便用户可以自行选择。问题在于,您会被锁定在特定结构的实现中 - 这可能只会带来不好的结果。如果您在实现中遗漏了某些内容,或者您的实现排除了一些在设计时没有考虑到的新功能,那么您可能只能在愿意破坏用户代码的情况下进行更改。 - Andrew Henle
4
最好使用完全不透明指针,这样用户就不会尝试分配他们自己的 Person - Kevin
@AndrewHenle,但是C和POSIX在多个地方采用了这种方法。它们指定了许多结构类型,为其指定了一定的最小成员集以及这些成员的含义。显然,维护这些标准的委员会认为“这只会是一件坏事”是错误的。它确实有缺点,但这些缺点可以通过用户能够声明相关结构类型的对象、直接访问它们而不是通过函数调用以及避免需要动态内存管理或静态数据来使用它们等非常大的优势来抵消。 - John Bollinger
1
@JohnBollinger 这些在 POSIX 和 C 中的例子基本上记录了现有接口,这些接口经过 多年 的广泛使用已经被深入研究。创建一个全新的接口并假设您已经涵盖了它未来可能发展的方向是完全不同的。考虑到标准结构(如 FILE)的迁移趋势,朝着不透明化方向发展(Linux 和 Solaris 在几十年中已经从开放的 FILE 结构向不透明的 FILE 结构转变),我坚持认为开放式的结构存在问题。 - Andrew Henle
这甚至没有涉及到在某些系统上,通过更改编译器选项,struct布局实际上可以发生变化的可能性。很难拥有一个非不透明结构,因为潜在的不同优化选项可能会导致不同的布局。 - Andrew Henle

4
您可以使用混合样式,例如在标头中编写:

struct person {
    float wage;
    int groupid;
};

struct person *person_new(void);
char const *getName (struct person const *p);
int getId (struct person const *p);

在源代码中

struct person_impl {
    struct person   p;
    char            name[NAME_MAX_LEN];
    int             id;
}

struct person *person_new(void)
{
    struct person_impl *p;

    p = malloc(sizeof *p);
    ...
    return &p->p;
}

chra const *getName(struct person const *p_)
{
    struct person_impl *p =
           container_of(p_, struct person_impl, p);

    return p->name;
}

详细了解 container_of(),请参阅例如https://zh.wikipedia.org/wiki/Offsetof


但是,如果调用者对返回的 struct person * 进行除使用 -> 获取字段之外的任何操作,则隐藏的 nameid 将丢失,并且 getName 将返回垃圾值。C 不会意识到这是无效的。 - pbfy0
@pbfy0 我这里没有看到问题。在 C 语言中,你可以随处这样做。比如,使用另一个答案中的不透明指针,你可以通过 memset(persion, 23, 42) 来覆盖内部内容。 - ensc
Person a = *person_new();对于不是特别熟练的C用户来说,这段代码似乎并不令人感到不合理。将随机偏移量调用memset()函数对结构体进行初始化看起来更容易引起意外行为。 - pbfy0

2

对John Bollinger答案的补充:

虽然我个人认为使用访问函数(init/get/set/destroy)和不透明指针类型是最安全的方法,但还有另一种选择,允许用户将对象放在堆栈上。

可以分配一个“无类型”内存块作为struct的一部分,并显式地使用该内存(按位/按字节),而不是使用其他类型。

例如:

Original Answer翻译成"最初的回答"

// public
typedef struct {
    float wage;
    int groupid;
    /* explanation: 1 for ID and NAME_MAX_LEN + 1 bytes for name... */
    unsigned long private__[1 + ((NAME_MAX_LEN + 1 + (sizeof(long) - 1)) / sizeof(long))];
} person_s;

// in .c file (private)
#define PERSON_ID(p) ((p)->private__[0])
#define PERSON_NAME(p) ((char*)((p)->private__ + 1))

这是一个非常强的指示,表明应该避免访问private__成员中的数据。没有访问实现文件的开发人员甚至不知道里面有什么。
话虽如此,最好的方法是使用不透明类型,就像在使用pthread_t API(POSIX)时遇到的那样。
"最初的回答"
typedef struct person_s person_s;
person_s * person_new(const char * name, size_t len);
const char * person_name(const person_s * person);
float person_wage_get(const person_s * person);
void person_wage_set(person_s * person, float wage);
// ...
void person_free(person_s * person);

注意:

  1. 避免在指针中使用typedef,这只会让开发人员感到困惑。

    最好明确指出指针类型,以便所有开发人员都知道他们使用的类型是动态分配的。

    编辑:此外,通过避免“typedef”指针类型,API承诺未来/备用实现也将在其API中使用指针,使开发人员能够信任和依赖此行为(请参见评论)。

  2. 当使用不透明类型时,可以避免使用NAME_MAX_LEN,从而允许任意长度的名称(假设重命名需要一个新对象)。这是更喜欢不透明指针方法的一个额外的激励。

  3. 尽可能避免在标识符开头处放置_(例如_name)。以_开头的名称被认为具有特殊含义,有些已被保留。同样适用于以_t结尾的类型(由POSIX保留)。

    请注意,我使用_s将类型标记为结构体,而不是使用保留字_t

  4. C通常使用snake_case(至少在历史上是这样)。最著名的API和大部分C标准都是snake_case(除了从C++导入的东西)。

    此外,保持一致性更好。在某些情况下使用CamelCase(或smallCamelCase),而在其他情况下使用snake_case可能会让开发人员在试图记忆您的API时感到困惑。


当涉及到不透明类型时,我建议使用typedef指针。如果您担心释放分配的内存,请为此声明一个函数。 - klutt
@klutt - 在许多情况下,将指针typedef为不透明类型被证明是一个错误,其中pthread_t就是其中之一。typedef指针并不能保证未来/替代实现将使用基于指针的类型(如果在不同的实现中它可能是一个结构体,则保持API完整性而不是ABI)。这使得设计不太可移植。当需要动态分配和管理开发人员使用API时的类型(即存储和传递ptherad_t *)时,它可能会导致双重间接和添加分配。 - Myst
@klutt - 关于链接,facil.io文档代码在抽象线程库实现细节时遇到了类似的问题-因为pthread_t是一个typedef,facil.io不能保证所有系统都将pthread_t实现为指针(即使大多数系统都这样)。 - Myst
有趣。不能说我完全理解,但我会研究一下。顺便说一句,"尽可能避免在标识符开头放置_",什么情况下不可能这样做呢? - klutt
@klutt - 这种情况什么时候不可能呢?我很有礼貌 :) 但是有时我们并不会为项目指定名称或命名约定。 - Myst
显示剩余2条评论

1

John Bollinger所写的是一种巧妙利用结构体和内存工作方式的方法,但这也是一种容易导致段错误的方法(想象一下分配一个 Person 数组,然后稍后将最后一个元素传递给访问其ID或名称的“方法”),或者损坏数据(在 Person 数组中,下一个 Person 正在覆盖前一个 Person 的“私有”变量)。您必须记住,必须创建指向 Person 的指针数组,而不是 Person 数组(这听起来很明显,直到您决定优化某些东西,并认为您可以比初始化函数更有效地分配和初始化结构体)。

不要误解,这是一个解决问题的好方法,但在使用时必须小心。我建议(虽然每个Person会使用4/8字节更多的内存),创建一个结构体Person,其中包含一个指向另一个结构体的指针,该结构体仅在.c文件中定义并保存私有数据。这样做会更难出现错误(如果是一个更大的项目,相信我 - 您迟早会犯错)。
.h 文件:
#pragma once

#define NAME_MAX_LEN 20

typedef struct _person {
    float wage;
    int groupid;

    _personPriv *const priv;
} Person;

void personInit(Person *p, const char *name);
Person* personNew(const char *name);

const char const *getName (Person *p);
int getId (Person *p);

.c 文件:

typedef struct {
    int id;
    char name[NAME_MAX_LEN];
} _personPriv;

const char const *getName (Person *p) {
    return p->priv->name;
}

int getId (Person *p) {
    return p->priv->id;
}

_personPriv* _personPrivNew(const char *name) {
    _personPriv *ret = memcpy(
        malloc(sizeof(*ret->priv)),
        &(_personPriv) {
            .id = generateId();
        },
        sizeof(*ret->priv)
    );

    // if(strlen(name) >= NAME_MAX_LEN) {
    //     raise an error or something?
    //     return NULL;
    // }

    strncpy(ret->name, name, strlen(name));

    return ret;
}

void personInit(Person *p, const char *name) {
    if(p == NULL)
        return;

    p->priv = memcpy(
        malloc(sizeof(*p->priv)),
        &(_personPriv) {
            .id = generateId();
        },
        sizeof(*p->priv)
    );

    ret->priv = _personPrivNew(name);
    if(ret->priv == NULL) {
        // raise an error or something
    }
}

Person* personNew(const char *name) {
    Person *ret = malloc(sizeof(*ret));

    ret->priv = _personPrivNew(name);
    if(ret->priv == NULL) {
        free(ret);
        return NULL;
    }
    return ret;
}

顺便提一下:可以实现这个版本,使得私有块在结构体的“公共”部分之后/之前分配,以改善局部性。只需分配sizeof(Person) + sizeof(_personPriv)并初始化一个部分为Person,另一个部分为_personPriv


1
不应该创建以双下划线开头的标识符。这些在所有上下文中都是保留的。 - Christian Gibbons

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