Исходный файл c. Перевод Включая один исходный файл C в другой? Что может быть в заголовочном файле

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

Отчасти это дело вкуса, поэтому, кому интересно как это делаю я, добро пожаловать под кат.

Несмотря на то, что «вся правда» о h-файлах содержится в соответствующем разделе описания препроцессора gcc, позволю себе некоторые пояснения и иллюстрации.

Итак, если дословно, заголовочный файл (h-файл) - файл содержащий Си декларации и макро определения, предназначенные для использования в нескольких исходных файлах (с-файлах). Проиллюстрируем это.

Легко заметить, что функции 1 и 2, а так же макрос 2, упомянуты в обоих файлах. И, поскольку, включение заголовочных файлов приводит к таким же результатам, как и копирование содержимого в каждый си-файл, мы можем сделать следующее:

Таким образом мы просто выделили общую часть из двух файлов и поместили ее в заголовочный файл.
Но является ли заголовочный файл интерфейсом в данном случае?

  • Если нам нужно использовать функциональность, которую реализуют функции 1 и 2 где то еще, то Да
  • Если макрос 2, предназначен только для использования в файлах Unit1.c и Unit2.c, то ему не место в интерфейсном файле
Более того, действительно ли нам необходимо иметь два си-файла для реализации интерфейса, определенного в заголовочном файле? Или достаточно одного?
Ответ на этот вопрос зависит от деталей реализации интерфейсных функций и от их места реализации. Например, если сделать диаграммы более подробными, можно представить вариант, когда интерфейсные функции реализованы в разных файлах:


Такой вариант реализации приводит к высокой связности кода, низкой тестируемости и к сложности повторного использования таких модулей.
Для того, что бы не иметь таких трудностей, я всегда рассматриваю си-файл и заголовочный файл как один модуль. В котором,
  • заголовочный файл содержит только те декларации функций, типов, макросов, которые являются частью интерфейса данного модуля.
  • Си-файл, в свою очередь, должен содержать реализацию всех функций, декларированных в h- файле, а также приватные типы, макросы и функции, которые нужны для реализации интерфейса.
Таким образом, если бы мне довелось реализовывать код, которому соответствует диаграмма приведенная выше, я бы постарался, добиться следующего (окончания _с и _h в именах файлов добавлены по причине невозможности использовать точку в инструменте , которым я пользовался для создания диаграмм):


Из диаграммы видно, что на самом деле мы имеем дело с двумя независимыми модулями, у каждого из которых имеется свой интерфейс в виде заголовочного файла. Это дает возможность использовать только тот интерфейс, который действительно необходим в данном конкретном случае.Более того, эти модули могут быть протестированы независимо друг от друга.
Читатель, наверное, заметил, что макрос 2 из заголовочного файла снова вернулся в виде копии в оба си-файла. Конечно, это не очень удобно поддерживать. Но и делать данный макрос частью интерфейса не правильно.
В таких случаях, я предпочитаю делать отдельный заголовочный файл содержащий типы и макросы, необходимые нескольким си-файлам.

Надеюсь, мне удалось обозначить те сущности, которые нуждаются в том, что бы быть помещенными в заголовочные файлы. А так же, показать разницу между интерфейсам и файлами, содержащими декларации и макросы, необходимые нескольким си-файлам.

Спасибо за внимание к материалу.

Исходные файлы

Текст программы на языке Си может быть разделен на несколько исходных файлов. Исходный файл представляет собой текстовый файл, который содержит либо всю программу, либо ее часть. При компиляции исходной программы каждый из составляющих ее исходных файлов должен быть скомпилирован отдельно, а затем связан с другими файлами компоновщиком. Отдельные исходные файлы можно объединять в один исходный файл, компилируемый как единое целое, посредством директивы препроцессора #include .

Исходный файл может содержать любую целостную комбинацию директив, указаний компилятору, объявлений и определений. Под целостностью подразумевается, что такие объекты, как определения функций, структуры данных либо набор связанных между собой директив условной компиляции, должны целиком располагаться в одном файле, т. е. не могут начинаться в одном файле, а продолжаться в другом.

Исходный файл не обязательно должен содержать выполняемые операторы. Иногда удобно размещать определения переменных в одном файле, а в других файлах использовать эти переменные путем их объявления. В этом случае определения переменных становятся легко доступными для поиска и модификации. Из тех же соображений именованные константы и макроопределения обычно собирают в отдельные файлы и включают их посредством директивы препроцессора #include в те исходные файлы, в которых они требуются.

Указания компилятору обычно действуют только для отдельных участков исходного файла. Специфические действия компилятора, задаваемые указаниями, определяются конкретной реализацией компилятора языка Си.

В нижеследующем примере исходная программа состоит из двух исходных файлов. Функции main и max представлены в отдельных файлах. Функция main использует функцию max в процессе своего выполнения.

/* исходный файл 1 - функция main */

extern int max (int, int); /* объявление функции */

main () /* определение функции */

int w = ONE, x = TWO, у = THREE;

/* исходный файл 2 - функция max */

int max (a, b) /* определение функции */

В первом исходном файле функция max объявлена, но не определена. Такое объявление функции называется предварительным; оно позволяет компилятору контролировать обращение к функции до того, как она определена. Определение функции main содержит вызовы функции max .

Строки, начинающиеся с символа #, являются директивами препроцессора. Директивы указывают препроцессору на необходимость замены в первом исходном файле идентификаторов ONE, TWO, THREE на соответствующие значения. Область действия директив не распространяется на второй исходный файл.



Вы можете правильно включить файлы.C или.CPP в другие исходные файлы. В зависимости от вашей среды IDE вы обычно можете предотвращать двойную привязку, просматривая свойства исходных файлов, которые хотите включить, обычно щелкая правой кнопкой мыши по ним и щелкая по свойствам, и снимите флажок / проверить компиляцию / ссылку / исключить из сборки или любой другой вариант. может быть. Или вы не можете включить файл в сам проект, поэтому среда IDE даже не знает, что она существует, и не попытается ее скомпилировать. И с make-файлами вы просто просто не ставили файл в него для компиляции и компоновки.

EDIT: Извините, я дал ответ вместо ответа на другие ответы:(

В зависимости от вашей среды сборки (вы не укажете) вы можете обнаружить, что она работает именно так, как вы хотите.

Тем не менее, существует множество сред (как IDE, так и много обработанных вручную Makefile), которые ожидают компиляции * .c - если это произойдет, вы, вероятно, столкнетесь с ошибками компоновщика из-за дублирования символов.

Как правило, эту практику следует избегать.

Если вы обязательно должны # включить источник (и обычно этого следует избегать), используйте другой файл для файла.

Я думал, что поделюсь ситуацией, когда моя команда решила включить файлы.c. Наш архитектор в основном состоит из модулей, которые развязаны через систему сообщений. Эти обработчики сообщений являются общедоступными и вызывают множество локальных статических рабочих функций для выполнения своей работы. Проблема возникла при попытке получить покрытие для наших единичных тестовых случаев, так как единственный способ реализовать этот частный код реализации был косвенно через общий интерфейс сообщений. С некоторыми функциями работника в коленях в стеке это оказалось кошмаром для обеспечения надлежащего покрытия.

Включение файлов.c дало нам возможность добраться до винтика в машине, нам было интересно тестировать.

Включение файла C в другой файл является законным, но не целесообразным, если вы точно не знаете, почему вы это делаете и чего пытаетесь достичь.
Я почти уверен, что если вы опубликуете здесь причину, по которой ваш вопрос будет сообщаться сообществу, вы найдете еще один подходящий способ достижения вашей цели (обратите внимание на «почти», так как возможно, что это решение, учитывая контекст).

Кстати, я пропустил вторую часть вопроса. Если файл C включен в другой файл и в то же время включен в проект, вы, вероятно, столкнетесь с проблемой дублирования символов, почему связывание объектов, то есть одна и та же функция будет определена дважды (если только они не статичны).

Язык C не запрещает такой тип #include, но результирующая единица перевода еще должна быть действительной C.

Я не знаю, какую программу вы используете с.prj-файлом. Если вы используете что-то вроде «make» или Visual Studio или что-то еще, просто убедитесь, что вы устанавливаете его список файлов, которые должны быть скомпилированы без того, который не может быть скомпилирован независимо.

вы должны добавить заголовок, подобный этому

#include

примечание: оба файла должны размещаться в одном месте

Вы можете использовать компилятор gcc в linux для связывания двух файлов c одним выходом. Предположим, у вас есть два c-файла, один из которых - «main.c», а другой - «support.c». Таким образом, команда для соединения этих двух

Gcc main.c support.c -o main.out

Эти два файла будут связаны с одним выходом main.out. Для запуска вывода команда будет

./main.out

Если вы используете функцию main.c, которая объявлена ​​в файле support.c, тогда вы должны объявить ее в основном также с использованием класса extern storage.

Расширение файла не имеет значения для большинства компиляторов C, поэтому оно будет работать.

Однако, в зависимости от настроек вашего файла или проекта, включенный файл c может генерировать отдельный файл объекта. При связывании это может привести к двойным определенным символам.