Скрытие членов в структуре 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;
}
Отсюда вы можете просто привести одну структуру к другой. Это считается плохой практикой? Или это часто делается?
16 ответов
Лично мне больше понравится:
typedef struct {
int _public_member;
/*I know you wont listen, but don't ever touch this member.*/
int _private_member;
} SomeStructSource;
В конце концов, это C, если люди хотят облажаться, им нужно разрешить - не нужно ничего скрывать, кроме:
Если вам нужно поддерживать совместимость ABI/API, есть два подхода, которые более распространены из того, что я видел.
Не предоставляйте своим клиентам доступ к структуре, не давайте им непрозрачный дескриптор (пустота * с красивым именем), предоставляйте функции init/destroy и accessor для всего. Это гарантирует, что вы можете изменить структуру, даже не перекомпилировав клиентов, если вы пишете библиотеку.
предоставьте непрозрачный дескриптор как часть вашей структуры, которую вы можете разместить так, как вам нравится. Этот подход даже используется в C++ для обеспечения совместимости ABI.
например
struct SomeStruct {
int member;
void* internals; //allocate this to your private struct
};
sizeof(SomeStruct) != sizeof(SomeStructSource)
, Это заставит кого-то найти вас и убить когда-нибудь.
Вы почти получили это, но не зашли достаточно далеко.
В шапке:
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, и вы можете изменить их безнаказанно, добавляя и удаляя элементы по желанию, даже без необходимости перекомпиляции. Кроме того, они не могут "случайно" членов munge напрямую или размещать SomeStruct в стеке. Это, конечно, также можно рассматривать как недостаток.
Я не рекомендую использовать шаблон публичной структуры. Правильный шаблон проектирования для ООП в 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 */
}
Никогда не делай этого. Если ваш API поддерживает что-то, что принимает SomeStruct в качестве параметра (что я ожидаю, что это делает), то они могли бы выделить один в стек и передать его. Вы получите серьезные ошибки при попытке получить доступ к приватному члену, начиная с компилятора выделяет для клиентского класса не содержит места для него.
Классический способ скрыть элементы в структуре - сделать ее недействительной *. Это в основном дескриптор /cookie, о котором знают только ваши файлы реализации. Практически каждая библиотека C делает это для личных данных.
Нечто похожее на предложенный вами метод действительно иногда используется (например, см. Различные варианты 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;
}
Я написал бы скрытую структуру и ссылался бы на нее, используя указатель в публичной структуре. Например, ваш.h может иметь:
typedef struct {
int a, b;
void *private;
} public_t;
И ваш.c:
typedef struct {
int c, d;
} private_t;
Очевидно, что он не защищает от арифметики с указателями и добавляет немного накладных расходов для распределения / освобождения, но я думаю, что это выходит за рамки вопроса.
Используйте следующий обходной путь:
#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
Есть лучшие способы сделать это, например, с помощью void *
указатель на приватную структуру в публичной структуре. То, как вы делаете это, вы обманываете компилятор.
Этот подход является допустимым, полезным, стандартным C.
Несколько иной подход, используемый API сокетов, который был определен в BSD Unix, - это стиль, используемый для struct sockaddr
,
Вот очень организованный способ сделать это с помощью макросов. Вот как я видел его использование в некоторых крупных проектах. Я предполагаю следующее:
- Заголовочный файл со структурой
- Исходный файл с доступом к закрытым полям
- Исходный файл без доступа к закрытым полям (поля существуют, но переименованы).
Заголовочный файл:
// You can put this part in a header file
// and share it between multiple header files in your project
#ifndef ALLOW_PRIVATE_ACCESS
#define PRIVATE(T) private_##T
#else
#define PRIVATE(T) T
#endif
#define PUBLIC(T) T
typedef struct {
int PRIVATE(m1); // private member
int PUBLIC(m2); // public member
} mystruct;
mystruct *mystruct_create(void);
int mystruct_get_m1(mystruct *t);
Исходный файл с доступом к закрытым полям:
#include <stdlib.h>
#define ALLOW_PRIVATE_ACCESS
#include "mystruct.h"
mystruct *mystruct_create(void) {
mystruct *p = (mystruct *)malloc(sizeof(mystruct));
p->m1 = 42; // works (private)
p->m2 = 34; // works (public)
return (mystruct *)p;
}
int mystruct_get_m1(mystruct *t) {
return t->m1; // works (private)
}
Исходный файл без доступа к закрытым полям:
#include <stdio.h>
#include <stdlib.h>
#include "mystruct.h"
int main() {
mystruct *t = mystruct_create();
printf("t->m1 = %d\n", t->m1); // error (private)
printf("t->m1 = %d\n", mystruct_get_m1(t)); // works (using function)
printf("t->m2 = %d\n", t->m2); // works (public)
free(t);
return 0;
}
Я нашел это bit-field
может быть хорошим решением, если вы действительно хотите что-то скрыть.
struct person {
unsigned long :64;
char *name;
int age;
};
struct wallet {
char *currency;
double balance;
};
Первый член struct 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;
}
Связанные, хотя и не совсем скрывающиеся.
Это условно осудить членов.
Обратите внимание, что это работает для 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;
}
Моим решением было бы предоставить только прототип внутренней структуры, а затем объявить определение в файле.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
};
Примечание. В этом случае переменная должна быть указателем, потому что компилятор не может знать размер внутренней структуры.
Здесь может быть полезна анонимная структура.
#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
В любом файле, который должен иметь доступ к закрытым членам, определите
#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;
}
Я не рекомендую ставить публичные члены после частных. Инициализация структуры без указателей членов, например с
Вы должны спроектировать свои структуры, и особенно частные члены, с нулевой инициализацией, поскольку в C++ нет автоматических конструкторов. Пока элементы инициализированы до 0, они не останутся в недопустимом состоянии даже без функции инициализации. Запрет инициализации обозначения члена, инициализация просто
Единственный недостаток, который я обнаружил, заключается в том, что это мешает таким вещам, как отладчики и завершение кода, им обычно не нравится, когда один тип имеет один набор членов в одном файле и другой набор в другом файле.
Не очень конфиденциально, учитывая, что вызывающий код может привести к (SomeStructSource *)
, Кроме того, что происходит, когда вы хотите добавить другого публичного участника? Вам придется сломать бинарную совместимость.
РЕДАКТИРОВАТЬ: я пропустил, что это было в файле.c, но на самом деле ничто не мешает клиенту скопировать его, или, возможно, даже #include
в.c файл напрямую.