Скрытие элементов в структуре C

Я читал об ООП в C, но мне никогда не нравилось, что вы не можете иметь закрытые данные-члены, как в 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;
}

Отсюда вы можете просто преобразовать одну структуру в другую. Считается ли это плохой практикой? Или это делается часто?


person Marlon    schedule 20.04.2010    source источник
comment
Зачем делать это так сложно? Если инструмент не делает то, что вам нужно, используйте другой.   -  person Romain Hippeau    schedule 20.04.2010
comment
Я думаю, что это нарушило бы правила алиасинга объектов, по крайней мере, в C99. Я знаю, что это было бы в C++.   -  person James McNellis    schedule 20.04.2010
comment
Было бы ужасно закрыть это! Почему люди голосуют близко по такому правильному вопросу? Потому что забыли, как делать что-то на C?   -  person Heath Hunnicutt    schedule 20.04.2010
comment
Не пытайтесь скрывать вещи. Просто используйте имена для частных членов, которые абсолютно ясно дают понять, что никто не должен их трогать. И если кто-то их использует, отпишитесь. Или изменить имена, чтобы просветить их.   -  person gnasher729    schedule 03.04.2014


Ответы (14)


Лично мне больше нравится вот это:

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

В конце концов, это C, если люди хотят облажаться, им нужно разрешить это - не нужно ничего скрывать, кроме:

Если вам нужно поддерживать совместимость ABI/API, есть 2 подхода, которые более распространены из того, что я видел.

  • Не давайте своим клиентам доступ к структуре, дайте им непрозрачный дескриптор (void* с красивым именем), предоставьте функции инициализации/уничтожения и доступа для всего. Это гарантирует, что вы можете изменить структуру даже без перекомпиляции клиентов, если вы пишете библиотеку.

  • предоставьте непрозрачный дескриптор как часть вашей структуры, которую вы можете выделить по своему усмотрению. Этот подход используется даже в C++ для обеспечения совместимости с ABI.

e.g

 struct SomeStruct {
  int member;
  void* internals; //allocate this to your private struct
 };
person nos    schedule 20.04.2010
comment
Я нахожу действительно плохой дизайн, позволяющий клиенту получить доступ к любому члену структуры. Вся структура должна быть закрытой. Доступ к его членам должен осуществляться через геттеры и сеттеры. - person Felipe Lavratti; 19.04.2013
comment
@fanl делает это в C, имеет много последствий, например. чтобы скрыть структуру таким образом, становится довольно сложно разместить ее в стеке или встроить в качестве члена другой структуры. Простой выход из этого состоит в том, чтобы динамически выделить структуру и выставить только void* или дескриптор, и хотя в некоторых случаях это может быть нормально, во многих случаях последствия слишком велики, и это не позволит вам воспользоваться преимуществом. что C предоставляет вам. - person nos; 19.04.2013
comment
ИМХО, второй приведенный здесь пример должен быть ответом, просто не забудьте предоставить функцию деструктора. - person Luis; 03.10.2014
comment
В критических для производительности ситуациях избегайте скачка, который подразумевает void *, и просто размещайте личные данные в строке - будь проклята конфиденциальность (в этом случае префикс подчеркивания - это все, что вы можете сделать). - person Engineer; 11.08.2015
comment
Скрытие членов структуры может хорошо работать с большинством приложений в качестве формы инкапсуляции и разрыва зависимостей компиляции, однако во многих ограниченных (встроенных) системах использование malloc нецелесообразно, и поэтому структуры должны размещаться статически или на диске. куча. Полное сокрытие структуры делает это трудным (невозможным?), поэтому вы все равно выставите ее напоказ. И тогда вы должны задокументировать это, и тогда это может быть явным. - person davidA; 05.08.2017
comment
@meowsqueak Тот факт, что код никто не видит, не означает, что его не нужно документировать, иначе разработка проприетарного программного обеспечения была бы одной из лучших работ, которые вы когда-либо могли получить в своей карьере программиста. Лучше всего в том смысле, что вам никогда не придется писать документацию... За исключением двух месяцев спустя, когда все будет в огне. Таким образом, вам все равно нужно документировать все, но это не значит, что вы должны объяснять другим, кто не должен касаться этого, что это такое. - person Purple Ice; 24.10.2018
comment
@PurpleIce Я полностью согласен - я имел в виду (по памяти), что вы должны документировать это как часть API. - person davidA; 26.10.2018
comment
Ваше первое предложение на самом деле отличное. В нашей компании мы делаем struct S { int x; // PRIVATE_FIELD };, а затем у нас есть собственный анализатор кода C, который проверяет комментарии и, если он видит комментарий PRIVATE_FIELD, запрещает пользователям вводить S.x для этого поля. - person mercury0114; 09.10.2020

sizeof(SomeStruct) != sizeof(SomeStructSource). Это приведет к тому, что кто-то когда-нибудь найдет вас и убьет.

person hobbs    schedule 20.04.2010
comment
И любое жюри отпустило бы их потом. - person gnud; 20.04.2010
comment
Всегда кодируйте так, как будто человек, который в конечном итоге поддерживает ваш код, — жестокий психопат, который знает, где вы живете. (приписывается Рику Осборну) - person Dietrich Epp; 02.11.2011

У вас почти получилось, но вы не зашли достаточно далеко.

В заголовке:

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

В .с:

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 в стеке. Это, конечно, тоже можно рассматривать как недостаток.

person Logan Capaldo    schedule 20.04.2010
comment
Некоторые считают использование typedef для скрытия указателей плохой идеей, особенно потому, что более очевидно, что SomeStruct * нужно как-то освобождать, чем SomeThing, которая выглядит как обычная переменная стека. Действительно, вы все еще можете объявить struct SomeStruct;, и пока вы его не определите, люди будут вынуждены использовать указатели SomeStruct *, не имея возможности разыменовывать свои элементы, таким образом, имея тот же эффект, но не скрывая указатель. - person Chris Lutz; 20.04.2010

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

/*********** 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 */
}
person Felipe Lavratti    schedule 11.03.2013
comment
Одна из трудностей с этим подходом заключается в том, что он требует использования malloc/free даже в ситуациях, когда структура может быть просто создана как переменная стека, а затем исчезнет при выходе из метода. Использование malloc/free для вещей, которые должны иметь семантику стека, может привести к фрагментации памяти, если между созданием/уничтожением другой код должен создавать постоянные объекты. Такую проблему можно смягчить, если предоставить метод использования переданного блока памяти для хранения объекта и определить тип int[] подходящего размера для этой цели. - person supercat; 12.03.2013
comment
@supercat Правда, если вы не хотите использовать malloc/free во встроенной системе, сделайте структуру частной для программиста, а не для кода. Я отредактировал свой ответ, чтобы справиться с этим. - person Felipe Lavratti; 12.03.2013
comment
Это разумный подход. Подход, который еще сильнее принудил бы к правильному использованию, состоял бы в том, чтобы определить typedef int module_store_t[20];, а затем иметь module_t *Module_CreateIn(module_store_t *p). Код может создать автоматическую переменную типа module_store_t, а затем использовать Module_CreateIn для получения из этого указателя вновь инициализированного модуля, время жизни которого будет соответствовать времени жизни автоматической переменной. - person supercat; 12.03.2013
comment
Второй подход не помогает в инкапсуляции структур. К сожалению, он по-прежнему позволяет напрямую ссылаться на частные члены структуры! Пожалуйста, попробуйте это в своем коде. - person Adi; 19.04.2013
comment
Ади, ты прав. К сожалению, не существует шаблона проектирования, который мог бы заставить компилятор генерировать ошибки, если пользователь пытается получить доступ к элементам данных структуры, когда вы хотите разместить данные структуры в стеке, а не в куче. Это короткая общая проблема, и мы должны решать ее со здравым смыслом. Структура была определена внутри заголовка с именем private, поэтому с этого момента пользователь не должен обращаться к именам, объявленным внутри этих файлов. Это может привести к нарушению инкапсуляции, поэтому я всегда придерживаюсь выбора malloc (пример 1 ответа). - person Felipe Lavratti; 19.04.2013
comment
Как бы мне ни нравился C за его простоту, он так раздражает, когда дело доходит до применения шаблонов проектирования. C и шаблоны проектирования просто несовместимы. И это действительно расстраивает, что после 40 лет существования C не существует единой техники, которая позволила бы вам использовать лучшие правила кодирования в C. Если мы проигнорируем проблему распределения стека, техника ADT действительно может быть применима, но только если будет соответствующая реализация malloc, которая не вызовет проблем с фрагментацией. Я действительно удивлен, что нет реализаций стандартной библиотеки C ‹продолжение следует› - person Adi; 19.04.2013
comment
которые ориентированы на эту проблему. В первую очередь я говорю в контексте встроенных систем, где в настоящее время в основном используется C и где фрагментация памяти представляет собой большую проблему. Просто скажу, что меня не устраивают решения, в которых вместо malloc используются статически размещенные объекты. Да, можно сказать, что меня очень раздражают такие C-фичи :/ - person Adi; 19.04.2013
comment
Согласен Ади. Однако статически размещенные объекты могут быть полезными, поскольку максимальное количество экземпляров данной структуры, существующих одновременно, может быть определено со значительной точностью. Что я обычно делаю во встроенной системе, так это использую malloc, если я получаю ошибку распределения в какой-то момент разработки продукта, мне придется перезаписать распределение для каждого модуля статическим выбором. В остальную прошивку не будут внесены какие-либо последствия. - person Felipe Lavratti; 19.04.2013

Никогда не делай этого. Если ваш API поддерживает что-либо, что принимает SomeStruct в качестве параметра (что, как я ожидаю, он и делает), тогда они могут выделить один в стеке и передать его. Вы получите серьезные ошибки, пытаясь получить доступ к частному члену, начиная с того, что компилятор выделенное для клиентского класса, не содержит для него места.

Классический способ скрыть члены в структуре — сделать ее пустой*. По сути, это дескриптор/куки, о котором знают только ваши файлы реализации. Почти каждая библиотека C делает это для частных данных.

person NG.    schedule 20.04.2010

Что-то похожее на предложенный вами метод действительно иногда используется (например, см. Различные варианты struct sockaddr* в API сокетов BSD), но его почти невозможно использовать, не нарушая строгие правила 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;
}
person caf    schedule 20.04.2010

Я бы написал скрытую структуру и ссылался на нее с помощью указателя в общедоступной структуре. Например, ваш .h может иметь:

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

И ваш .с:

typedef struct {
    int c, d;
} private_t;

Это, очевидно, не защищает от арифметики указателей и добавляет немного накладных расходов для выделения/освобождения, но я думаю, что это выходит за рамки вопроса.

person jweyrich    schedule 20.04.2010

Используйте следующий обходной путь:

#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
person Ilya Matveychikov    schedule 05.04.2015
comment
интересно... этот подход скрывает объявление приватной структуры, но позволяет вам получить доступ к приватной части, если вы получите экземпляр T-структуры. - person mkonvisar; 28.06.2017
comment
Это войдет в мой учебник udemy. Потрясающий - person Abhishek Sagar; 22.12.2017

Есть лучшие способы сделать это, например, использовать указатель void * на приватную структуру в публичной структуре. То, как вы это делаете, обманываете компилятор.

person Community    schedule 20.04.2010

Этот подход является действительным, полезным, стандартным C.

Немного другой подход, используемый API сокетов, который был определен BSD Unix, - это стиль, используемый для struct sockaddr.

person Heath Hunnicutt    schedule 20.04.2010

Мое решение состояло бы в том, чтобы предоставить только прототип внутренней структуры, а затем объявить определение в файле .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
};

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

person adc    schedule 10.10.2013

Я обнаружил, что bit-field может быть хорошим решением, если вы действительно хотите что-то скрыть.

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

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

Первым членом структуры person является безымянное битовое поле. используется для 64-bit pointer в этом случае. Он полностью скрыт и не может быть доступен по имени структурной переменной.

Поскольку первый 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;
}
person Rieon Ke    schedule 06.07.2020

Не очень конфиденциально, учитывая, что вызывающий код может возвращаться к (SomeStructSource *). Кроме того, что происходит, когда вы хотите добавить еще одного открытого участника? Вам придется сломать бинарную совместимость.

РЕДАКТИРОВАТЬ: я пропустил, что он был в файле .c, но на самом деле ничто не мешает клиенту скопировать его или, возможно, даже напрямую #includeing файл .c.

person Matthew Flaschen    schedule 20.04.2010
comment
Вот почему SomeStructSource определен в исходном файле. - person Marlon; 20.04.2010
comment
Только в том случае, если вы опубликуете SomeStructSource. Указатель объекта C++ аналогичен, можно использовать offsetof() и математику указателя, чтобы добраться до закрытых членов. - person Heath Hunnicutt; 20.04.2010

Родственники, хотя и не особо скрываемые.

Является условным прекращением поддержки членов.

Обратите внимание, что это работает для 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;
}
person ideasman42    schedule 02.04.2014