Непрозрачные структуры C: как они должны быть объявлены?
Я видел оба из следующих двух стилей объявления непрозрачных типов в C API. Есть ли явное преимущество использования одного стиля над другим?
Опция 1
// foo.h
typedef struct foo * fooRef;
void doStuff(fooRef f);
// foo.c
struct foo {
int x;
int y;
};
Вариант 2
// foo.h
typedef struct _foo foo;
void doStuff(foo *f);
// foo.c
struct _foo {
int x;
int y;
};
4 ответа
Мой голос за третий вариант, который mouviciel опубликовал и удалил:
Я видел третий путь:
// foo.h struct foo; void doStuff(struct foo *f); // foo.c struct foo { int x; int y; };
Если вы действительно не можете набрать struct
ключевое слово, typedef struct foo foo;
(примечание: избавиться от бесполезного и проблемного подчеркивания) допустимо. Но что бы вы ни делали, никогда не используйте typedef
определить имена для типов указателей. Он скрывает чрезвычайно важную информацию о том, что переменные этого типа ссылаются на объект, который может быть изменен всякий раз, когда вы передаете их функциям, и позволяет работать с различными квалификациями (например, const
Квалифицированные версии указателя большая боль.
Вариант 1.5
Я привык использовать Вариант 1, кроме тех случаев, когда вы называете свою ссылку _h
чтобы показать, что это "дескриптор" к "объекту" в стиле C этого данного C "класса". Затем вы гарантируете использование прототипов вашей функции. const
где бы содержимое этого объекта "дескриптор" был только входом, и не может быть изменено, и не использовать const
везде, где содержание может быть изменено.
Вот полный пример:
//======================================================================================================================
// my_module.h
//======================================================================================================================
// An opaque pointer (handle) to a C-style "object" of "class" type "my_module" (struct my_module_s *, or my_module_h):
typedef struct my_module_s *my_module_h;
// Create a new "object" of "class" "my_module":
// A function that takes a *pointer to* an "object" handle, `malloc`s memory for a new copy of the opaque
// `struct my_module_s`, then points the user's input handle (via its passed-in pointer) to this newly-created
// "object" of "class" "my_module".
void my_module_open(my_module_h * my_module_h_p);
// A function that takes this "object" (via its handle) as an input only and cannot modify it
void my_module_do_stuff1(const my_module_h my_module);
// A function that can modify the private content of this "object" (via its handle) (but still cannot modify the
// handle itself)
void my_module_do_stuff2(my_module_h my_module);
// Destroy the passed-in "object" of "class" type "my_module":
// A function that can close this object by stopping all operations, as required, and `free`ing its memory.
// `struct my_module_s`, then points the user's input handle (via its passed-in pointer) to this newly-created "object".
void my_module_close(my_module_h my_module);
//======================================================================================================================
// my_module.c
//======================================================================================================================
// Definition of the opaque struct "object" of C-style "class" "my_module".
// - NB: Since this is an opaque struct (declared in the header but not defined until the source file), it has the
// following 2 important properties:
// 1) It permits data hiding, wherein you end up with the equivalent of a C++ "class" with only *private* member
// variables.
// 2) Objects of this "class" can only be dynamically allocated. No static allocation is possible since any module
// including the header file does not know the contents of *nor the size of* (this is the critical part) this "class"
// (ie: C struct).
struct my_module_s
{
int my_private_int1;
int my_private_int2;
float my_private_float;
// etc. etc--add more "private" member variables as you see fit
}
void my_module_open(my_module_h * my_module_h_p)
{
// Ensure the passed-in pointer is not NULL (since it is a core dump/segmentation fault to try to dereference
// a NULL pointer)
if (!my_module_h_p)
{
// Print some error or store some error code here, and return it at the end of the function instead of
// returning void.
goto done;
}
// Now allocate the actual memory for a new my_module C object from the heap, thereby dynamically creating this
// C-style "object".
my_module_h my_module; // Create a local object handle (pointer to a struct)
my_module = malloc(sizeof(*my_module)); // Dynamically allocate memory for the full contents of the struct "object"
if (!my_module)
{
// Malloc failed due to out-of-memory. Print some error or store some error code here, and return it
// at the end of the function instead of returning void.
goto done;
}
// Initialize all memory to zero (OR just use `calloc()` instead of `malloc()` above!)
memset(my_module, 0, sizeof(*my_module));
// Now pass out this object to the user, and exit.
*my_module_h_p = my_module;
done:
}
void my_module_do_stuff1(const my_module_h my_module)
{
// Ensure my_module is not a NULL pointer.
if (!my_module)
{
goto done;
}
// Do stuff where you use my_module private "member" variables.
// Ex: use `my_module->my_private_int1` here, or `my_module->my_private_float`, etc.
done:
}
void my_module_do_stuff2(my_module_h my_module)
{
// Ensure my_module is not a NULL pointer.
if (!my_module)
{
goto done;
}
// Do stuff where you use AND UPDATE my_module private "member" variables.
// Ex:
my_module->my_private_int1 = 7;
my_module->my_private_float = 3.14159;
// Etc.
done:
}
void my_module_close(my_module_h my_module)
{
// Ensure my_module is not a NULL pointer.
if (!my_module)
{
goto done;
}
free(my_module);
done:
}
Единственными улучшениями после этого было бы:
- Реализовать полную обработку ошибок и вернуть ошибку вместо
void
, Добавьте структуру конфигурации с именем
my_module_config_t
в файл.h и передайте егоopen
функция для обновления внутренних переменных при создании нового объекта. Пример://-------------------- // my_module.h //-------------------- // my_module configuration struct typedef struct my_module_config_s { int my_config_param_int; int my_config_param_float; } my_module_config_t; void my_module_open(my_module_h * my_module_h_p, const my_module_config_t *config); //-------------------- // my_module.c //-------------------- void my_module_open(my_module_h * my_module_h_p, const my_module_config_t *config) { // Ensure the passed-in pointer is not NULL (since it is a core dump/segmentation fault to try to dereference // a NULL pointer) if (!my_module_h_p) { // Print some error or store some error code here, and return it at the end of the function instead of // returning void. goto done; } // Now allocate the actual memory for a new my_module C object from the heap, thereby dynamically creating this // C-style "object". my_module_h my_module; // Create a local object handle (pointer to a struct) my_module = malloc(sizeof(*my_module)); // Dynamically allocate memory for the full contents of the struct "object" if (!my_module) { // Malloc failed due to out-of-memory. Print some error or store some error code here, and return it // at the end of the function instead of returning void. goto done; } // Initialize all memory to zero (OR just use `calloc()` instead of `malloc()` above!) memset(my_module, 0, sizeof(*my_module)); // Now initialize the object with values per the config struct passed in. my_module->my_private_int1 = config->my_config_param_int; my_module->my_private_int2 = config->my_config_param_int*3/2; my_module->my_private_float = config->my_config_param_float; // etc etc // Now pass out this object to the user, and exit. *my_module_h_p = my_module; done: }
bar(const fooRef)
объявляет неизменный адрес в качестве аргумента. bar(const foo *)
объявляет адрес неизменного foo в качестве аргумента.
По этой причине я предпочитаю вариант 2. Т.е. представленный тип интерфейса - это тот, где cv-ness может быть указан на каждом уровне косвенности. Конечно, можно обойти библиотеку варианта 1 и просто использовать foo
, открывая себя всевозможным ужасам, когда автор библиотеки меняет реализацию. (Т.е. автор библиотеки варианта 1 воспринимает только fooRef
является частью инвариантного интерфейса и foo
может прийти, уйти, измениться, что угодно. Автор библиотеки варианта 2 понимает, что foo
является частью инвариантного интерфейса.)
Меня больше удивляет, что никто не предложил комбинированные конструкции typedef/struct.typedef struct { ... } foo;
Вариант 3. Предоставьте людям выбор
/* foo.h */
typedef struct PersonInstance PersonInstance;
typedef struct PersonInstance * PersonHandle;
typedef const struct PersonInstance * ConstPersonHandle;
void saveStuff (PersonHandle person);
int readStuff (ConstPersonHandle person);
...
/* foo.c */
struct PersonInstance {
int a;
int b;
...
};
...