У вас круговая зависимость заголовка, что является плохим дизайном и почти всегда приводит к сбоям. Проблема осложняется слишком сложной стратегией именования, которая делает код действительно трудным для чтения.
Вот основная проблема, использующая значительно упрощенные имена:
Файл драйвера.h
#ifndef DRIVER_H
#define DRIVER_H
#include "state.h"
/* See answer text for an explanation of this declaration style. */
typedef struct Driver Driver;
struct Driver {
// ...
State state;
// ...
};
#endif
Файл state.h
#ifndef STATE_H
#define STATE_H
// Needed because the Driver type is used in a prototype
#include "driver.h"
typedef struct State State;
struct State {
// ...
};
// Attempt to fix circular dependency with a redundant declaration.
typedef struct Driver Driver;
int32_t stateTask(Driver* driver);
#endif
Итак, эти два заголовка включают друг друга. Защитники заголовка будут препятствовать их включению дважды, но они не будут гарантировать, что объявления прочитаны в правильном порядке. Что произойдет, зависит от того, в каком порядке вы #include
два заголовка:
Если вы сначала #include "driver.h"
, он установит защиту своего заголовка и сразу же #include "state.h"
. Он не был включен ранее, поэтому защита заголовка не установлена, и компилятор начинает ее обрабатывать. Он сразу же достигает значения #include driver.h"
, но защита заголовка теперь установлена, хотя заголовок еще не был обработан, поэтому избегать циклического включения. В конце концов достигает прототипа, который ссылается на тип Driver
, который еще не определен.
Полагаю, вы уже столкнулись с этой проблемой из-за избыточных объявлений struct
в state.h
. Вставив эти декларации, вы могли бы удалить #include "driver.h"
, но, возможно, у вас возникли другие потребности.
С другой стороны, если вы сначала #include "state.h"
, компилятор видит, что его защита заголовка не установлена, устанавливает защиту заголовка и продолжает работу с заголовком. Он сразу видит #include "driver.h"
; защита заголовка еще не установлена, поэтому она устанавливает защиту заголовка и продолжает работу с этим заголовком. Теперь, когда он попадает в #include "state.h"
в заголовке driver.h
, он ничего не делает, потому что защита заголовка уже установлена.
К сожалению, это #include
действительно необходимо. Компиляция завершается неудачно, когда делается попытка определить член типа State
, который еще не был определен. В этом случае вы на самом деле включаете целый объект State
, а не просто указатель, поэтому вы не можете с легкостью просто объявить вкладку struct
вперед.
Короче говоря, все будет работать нормально с одним заголовком, включая порядок, но не с другим. К сожалению, очень сложно предсказать порядок, в котором заголовки будут включены в сложный проект, потому что заголовки не всегда видны. Они могут появляться внутри других заголовков, которые включены. Это может привести к тому, что заголовки будут включены в «неправильный» порядок.
В общем случае не стоит писать заголовки, которые должны быть #include
d в определенном порядке. Это почти всегда заканчивается такой проблемой.
Когда у вас есть несколько взаимосвязанных типов, которые все используются одним и тем же небольшим компонентом, вам лучше поместить их все в один заголовок. Вам все равно нужно будет правильно отсортировать заказ, но, по крайней мере, все они находятся в одном месте. В этом единственном заголовочном файле может потребоваться предварительное объявление структур, чтобы позволить указатели на них из других структур. Помещение определений типов перед прототипами уменьшает необходимость предварительного объявления ссылок на прототипы.
Если у вас много типов, вы можете поместить все их объявления в один внутренний project/types.h
заголовок. Затем вы можете расположить прототипы в любой сложной файловой организации, которая вам нравится. Это довольно распространенная парадигма. Для внешних заголовков прототипа, то есть заголовков, которые объявляют функции, предназначенные для отображения в глобальном масштабе, вы можете уменьшить беспорядок, просто объявив структуры, используемые прототипами. Предполагая, что прототип использует только указатель на структуру, что, безусловно, является наиболее распространенным, нет необходимости делать определение структуры видимым.
Предупреждение: Следуют рекомендациям по стилю. Если вам не нравятся такие вещи, вы можете перестать читать здесь.
После того как ваши заголовки отсортированы, если они используются только для внутренних целей, вы можете упростить вещи для себя и своих читателей, удалив ненужные префиксы из внутренней структуры и имен перечислений. Typedefs, теги структуры и объединения и перечисления не имеют связи; они не могут проникнуть в другой отдельно скомпилированный модуль перевода. Поэтому нет необходимости делать их глобально уникальными, если они не предназначены для глобального использования.
Независимо от того, что вы можете увидеть в чужом коде, совершенно не нужно делать имена typedef отличными от тегов struct, даже если вы когда-нибудь намереваетесь скомпилировать с C ++. В C имена находятся в двух совершенно разных пространствах имен; тег struct распознается как таковой только в том случае, если ему предшествует токен struct
. Так
typedef struct MyStructure MyStructure;
абсолютно верно. Фактически, это действительно, даже если struct MyStructure
еще не выделено, что делает его удобным для типов структур, которые включают указатели на тот же тип.
Я обычно использую стиль, показанный во фрагментах кода выше, всегда ставя typedef
перед определением структуры. Я нахожу это более читабельным, чем указывать имя typedef в конце определения структуры, даже если в моем стиле имена всегда одинаковы. Кроме того, форвард typedef
можно просто скопировать в заголовки, которые в них нуждаются. C не жалуется, если вы typedef
указали одно и то же имя более одного раза, так что это абсолютно безопасно.