Как скрыть некоторые поля структуры в C? - PullRequest
18 голосов
/ 22 марта 2019

Я пытаюсь реализовать struct 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;

Ответы [ 5 ]

24 голосов
/ 22 марта 2019

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

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

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

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

...
16 голосов
/ 23 марта 2019

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

3 голосов
/ 23 марта 2019

Вы можете использовать стиль 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().

2 голосов
/ 10 июля 2019

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

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

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

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

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

1 голос
/ 23 марта 2019

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

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

...