Обычный способ добиться этого - поместить исходный код каждого модуля в отдельный каталог. Каждый каталог может содержать все исходные и заголовочные файлы для модуля.
Публичный заголовок для каждого модуля может быть помещен в отдельный общий каталог заголовков. Возможно, я бы использовал символическую ссылку из общего каталога в соответствующий каталог модулей для каждого заголовка.
Правила компиляции просто утверждают, что ни один модуль не может включать заголовки из других модулей, кроме заголовков в общем каталоге. Это приводит к тому, что ни один модуль не может включать заголовки из другого модуля, за исключением общедоступного заголовка (что обеспечивает соблюдение частных барьеров).
Автоматическое предотвращение циклических зависимостей не является тривиальным. Проблема в том, что вы можете установить наличие циклической зависимости, только просматривая одновременно несколько исходных файлов, а компилятор просматривает только по одному за раз.
Рассмотрим пару модулей, ModuleA и ModuleB, и программу Program1, в которой используются оба модуля.
base/include
ModuleA.h
ModuleB.h
base/ModuleA
ModuleA.h
ModuleA1.c
ModuleA2.c
base/ModuleB
ModuleB.h
ModuleB1.c
ModuleB2.c
base/Program1
Program1.c
При компиляции Program1.c вполне законно включить и ModuleA.h, и ModuleB.h, если он использует службы обоих модулей. Таким образом, ModuleA.h не может жаловаться, если ModuleB.h включен в тот же модуль перевода (TU), и также не может ModuleB.h жаловаться, если ModuleA.h включен в тот же TU.
Предположим, что для ModuleA является законным использование возможностей ModuleB. Поэтому при компиляции ModuleA1.c или ModuleA2.c не должно быть проблем с включением обоих ModuleA.h и ModuleB.h.
Однако, чтобы предотвратить циклические зависимости, вы должны иметь возможность запретить коду в ModuleB1.c и ModuleB2.c использовать ModuleA.h.
Насколько я могу видеть, единственный способ сделать это - это какой-то метод, который требует частного заголовка для ModuleB, который говорит: «ModuleA уже включен», хотя это не так, и это включается до того, как ModuleA.h когда-либо включены.
Каркас ModuleA.h будет стандартным форматом (и ModuleB.h будет аналогичным):
#ifndef MODULEA_H_INCLUDED
#define MODULEA_H_INCLUDED
...contents of ModuleA.h...
#endif
Теперь, если код в ModuleB1.c содержит:
#define MODULEA_H_INCLUDED
#include "ModuleB.h"
...if ModuleA.h is also included, it will declare nothing...
...so anything that depends on its contents will fail to compile...
Это далеко не автоматически.
Вы можете выполнить анализ включенных файлов и потребовать, чтобы существовал топологический вид зависимостей без петель. Раньше в системах UNIX существовала программа tsort
(и сопутствующая программа lorder
), которые вместе предоставляли необходимые услуги для создания статической (.a
) библиотеки, содержащей объектные файлы в порядке, не требует повторного сканирования архива. Программа ranlib
и, в конечном итоге, ar
и ld
взяли на себя обязанности по управлению повторным сканированием одной библиотеки, что делает lorder
особенно избыточным. Но tsort
имеет более общее использование; он доступен в некоторых системах (например, MacOS X; RHEL 5 Linux).
Итак, используя отслеживание зависимостей от GCC плюс tsort
, вы сможете проверить, существуют ли циклы между модулями. Но с этим нужно обращаться осторожно.
Может быть какая-то IDE или другой набор инструментов, который обрабатывает этот материал автоматически. Но обычно программисты могут быть достаточно дисциплинированными, чтобы избежать проблем - при условии, что требования и межмодульные зависимости тщательно задокументированы.