Как скрыть некоторые поля структуры в C?

Я пытаюсь реализовать структуру лица, и мне нужно скрыть некоторые поля или сделать их постоянными. Трюк для создания приватных полей.

Заголовок:

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

Ответы

Ответ 1

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 без более широкого контекста и передать указатель на него функции, которая ожидает присутствия объекта контекста. Есть и другие проблемы.

В целом, API C обычно используют подход непрозрачной структуры или просто тщательно документируют, что клиентам разрешено делать с данными, к которым у них есть доступ, или даже просто документируют, как все работает, чтобы пользователи могли сами выбирать. Они, особенно последние, хорошо согласуются с общими подходами и идиомами С - С не держит вас за руку и не защищает от причинения вреда. Вам доверяют знать, что вы делаете, и делать только то, что вы намерены делать.

Ответ 2

Структура не может иметь несколько противоречивых определений. Таким образом, вы не можете создать структуру, которая скрывает некоторые поля.

Однако вы можете объявить, что структура существует в заголовке, не определяя его. Тогда вызывающая сторона ограничивается использованием только указателя на структуру и использованием функций в вашей реализации для ее изменения.

Например, вы можете определить свой заголовок следующим образом:

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

...

Ответ 3

Вы можете использовать стиль mixin; например напишите в шапке:

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

Смотрите, например, https://en.wikipedia.org/wiki/Offsetof для подробной информации о container_of().

Ответ 4

Приложение к ответу Джона Боллинджера:

Хотя, IMHO, непрозрачные типы указателей с функциями доступа (init/get/set/destroy) являются наиболее безопасным подходом, существует еще один вариант, который позволяет пользователям размещать объекты в стеке.

Можно выделить один "типизированный" кусок памяти как часть struct и использовать эту память явно (побитово/побитово за байтом) вместо использования дополнительных типов.

то есть:

// 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__ члена следует избегать. Разработчики, у которых нет доступа к файлу реализации, даже не будут знать, что там.

Сказав это, лучшим подходом является непрозрачный тип, с которым вы, возможно, столкнулись при использовании API pthread_t (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 с указателем. Это только смущает разработчиков.

    Лучше сохранять указатели явными, чтобы все разработчики знали, что используемый ими тип выделяется динамически.

    РЕДАКТИРОВАТЬ: Кроме того, избегая "определения типа" типа указателя, 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.

Ответ 5

То, что написал Джон Боллинджер, - это аккуратный способ использования структуры и работы памяти, но также и простой способ получить ошибку сегмента (представьте себе выделение массива Person и последующую передачу последнего элемента в "метод", который обращается к id или это имя), или испортите ваши данные (в массиве Person следующий Person перезаписывает "частные" переменные предыдущего Person). Вы должны помнить, что вы должны создать массив указателей на Person вместо массива Person (звучит довольно очевидно, пока вы не решите что-то оптимизировать и не подумаете, что можете распределять и инициализировать структуру более эффективно, чем функция initializer).

Не поймите меня неправильно, это отличный способ решить проблему, но вы должны быть осторожны при его использовании. То, что я бы предложил (хотя и использовал на 4/8 байт больше памяти на Person), это создать структуру 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.