Привет! У меня небольшое опыта, поэтому возможно я повторяюсь.<br/> <br/> <b><i>Вопрос:</i></b> <br/> Есть файлы <b>math_functions.c</b> и <b>main.c</b>. Могу ли я просто вызывать функции из <b>math_functions.c</b> в <b>main.c</b>, или обязательно нужно подключать объявляющий их заголовочный файл <b>math_functions.h</b>? Почему объявления (прототипы) нужны в заголовочном файле, если определения уже есть в <b>math_functions.c</b>? Для чего компилятору знать типы функций через заголовочный файл? Также понимаю, что в заголовочных файлах часто используют <b>#ifndef</b> для предотвращения множественного включения — разве такое ограничение нельзя сделать напрямую в <b>main.c</b>? Я понимаю, что это может быть простой вопрос, но хочу разобраться сейчас, поскольку начинаю проект с модулями и хочу правильно организовать код.
Короткий ответ — заголовочный файл нужен не «обязательно» в буквальном смысле, но почти всегда нужен по практическим причинам: он описывает интерфейс модуля (прототипы, типы, декларации), который компилятор и другие единицы трансляции должны видеть одинаковым образом. Ниже — почему так и как правильно организовать код. Почему прототипы нужны (и зачем помещать их в .h) - Компилятор проверяет вызовы функций. Прототип сообщает ему количество и типы аргументов и тип возвращаемого значения. Это позволяет найти ошибки при компиляции (неправильный тип, порядок аргументов и т.п.). - Без корректного прототипа можно получить неправильную генерацию кода: разные соглашения о вызове, преобразования аргументов (promotion), обработка возвращаемого значения (особенно для структур или 64‑бит целых на 32‑бит архитектурах) — всё это может привести к неопределённому поведению. - В старом C (до C99) существовала «неявная декларация функции» при вызове без прототипа; это небезопасно и в современных стандартах запрещено (C99+ компиляторы обычно выдадут ошибку или строгое предупреждение). Почему использовать .h, а не просто писать объявление в main.c - Технически вы можете объявить прототип прямо в main.c и всё скомпилируется, если только main.c и math_functions.c оба компонуются вместе. Но это дублирование: если прототип изменится, нужно править в нескольких местах — легко допустить рассинхронизацию. - Заголовок служит единственным источником правды: все файлы, использующие модуль, включают один и тот же .h и получают одну и ту же декларацию. - Заголовки упрощают повторное использование и модульность — другие .c файлы могут включать math_functions.h без знания внутренних деталей реализации. Про связывание (linker) - Компилятор проверяет вызовы в пределах одной единицы трансляции; разрешение фактических определений функций происходит на уровне линковки. Если вы вызвали функцию, у которой есть прототип, но определения нет — получите ошибку линковщика. - Если определения есть, но прототипы в разных единицах не совпадают, вы получите либо неопределённое поведение, либо трудноотлавливаемые ошибки. О include guards (#ifndef ...) - Директива «защиты от множественного включения» (#ifndef / #define / #endif или #pragma once) нужна в самом заголовочном файле, потому что один и тот же заголовок может быть включён косвенно через другие заголовки несколько раз в рамках одной единицы трансляции. Защита предотвращает повторное определение типов/макросов/прототипов при многократном #include. - Нельзя «сделать это в main.c», потому что проблема — не в main.c, а в самом заголовочном файле: он может включаться в разные .c и включаться несколько раз в одном .c через цепочку include. Поэтому защита должна быть в .h. Дополнительные моменты и рекомендации - Делайте так: math_functions.h — прототипы, typedef’ы, extern‑переменные, декларации структур (включая «opaque» неполные объявления для информационной сокрытия); math_functions.c — определения функций и include "math_functions.h" внутри .c, чтобы убедиться, что реализация соответствует прототипам. - Для внутренних (вспомогательных) функций делайте static в .c — тогда они не видны извне и их не нужно объявлять в .h. - Не класть определения обычных функций в .h (исключение: inline/статические inline или «шаблонообразные» функции), иначе при включении в несколько .c получите дублирование символов при линковке (или нужную специфику inline/extern). - Для глобальных переменных: в .h делаете extern int x; а в одном .c — int x = 0; - Можно использовать pragma diagnostics и строго компилировать с -Wall -Werror, тогда компилятор подскажет отсутствующие или некорректные прототипы. Пример правильной структуры - math_functions.h - include guard - прототипы функций, typedef’ы, extern переменные - math_functions.c - #include "math_functions.h" // проверяет соответствие объявлений и определений - определения функций - main.c - #include "math_functions.h" - вызовы функций Итого: заголовочные файлы — не просто «формальность», а средство обеспечить единую, проверяемую компилятором контрактную (интерфейсную) часть модуля между различными единицами трансляции. Писать объявления только в main.c можно в небольших однофайловых экспериментах, но для реальных проектов это плохая практика.
Для того, чтобы использовать в main.c функцию, определенную в math_functions.c, вам надо иметь ее объявление в main.c <br/> <br/> Вам надо сказать компилятору, что вот такая функция есть и у нее вот такие вот параметры. <br/> <br/> Можно просто в main.c написать то, что вы бы написали в math_functions.h. Но это быстро становится сложно, если у вас проект большой и функции используются в разных файлах. Надо будет эти объявления копировать в кучу мест. А если вам еще и поменять что-то надо потом, вы офигеете. Для этого и придумали заголовочные файлы - вы пишите объявление один раз и потом его везде используете. <br/> <br/> Вообще, сейчас не обязательно math_functions.h включать и в math_functions.c. Раньше надо было определять функцию даже если ее объявления нигде нет. В современных стандартах это не так. Но все равно хедеры включают в соответствующий файл, чтобы ловить ошибки. Если вы поменяете функцию в .c но не поменяете в хедере, компилятор заметит несоответствие объявления и определения и сообщит об ошибке.
Хедеры служат, чтобы два c-файла — они называются «единицы трансляции» — <b>компилировались по отдельности</b> . Это основное назначение той разновидности include-файлов, которые называются хедерами. <br/> <br/> Можно в h-файлы посадить и тела функций, только убедиться, что каждое тело ровно в одной единице трансляции — есть такой подход под названием «одна единица трансляции», и он существует у больших редко перекомпилируемых библиотек, чтобы перекомпиляция была покороче. <br/> <br/> Подключение main.c←math_functions.h служит, чтобы сказать компилятору: а где-то в другом месте есть эти функции. <br/> <br/> Подключение math_functions.c←math_functions.h — частично для подключения прочего общего вроде типов, частично для проверки на ошибки. Дело в том, что Си традиционно не <b>козявит</b> (does not mangle) имена функций, и если в хедере sin(int), а в реализации sin(double) — будут трудноуловимые ошибки. <br/> <br/> Да, деление на единицы компиляции решает и другую задачу — декомпизицию программы на меньшие элементы, и есть противоречие одного с другим (особенно в языке Си++, где хедеры огромны) <br/> <br/> В хедере находится только то, что <b>не производит кода</b> . Если говорить про Си++, то… <br/> • inline-объекты (код производят вызовы объекта) <br/> • определения типов, функций и прочего; extern-определения переменных (код производят тела функций и окончательные определения переменных) <br/> • шаблоны, если те нужны более чем в одной единице компиляции (код производит специализация) <br/> Но полные специализации шаблонов <b>производят</b> код и находятся в cpp-файле!
Ну есть же всё в гугле и в манах, где очень подробно всё расписано и даже с примерами: <a href="https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%B3%D0%BE%D0%BB%D0%BE%D0%B2%D0%BE%D1%87%D0%BD%D1%8B%D0%B9_%D1%84%D0%B0%D0%B9%D0%BB" rel="nofollow">https://ru.wikipedia.org/wiki/Заголовочный_файл</a>
Ничего не мешает вообще использовать только main.c. Но по мере роста программы и ее сложности ты опухнешь каждый раз рыться в исходнике.
<blockquote>Есть файлы math_functions.c и main.c. Я же могу просто в main.c использовать функции из файла math_functions.c? Но нет нужно ещё объявить этот самый math_functions.h в котором нужно определить функции, вот этого я не понимаю зачем ?</blockquote> <br/> <br/> 1. В отличие от других ЯП, Си плотно привязан к обработке текста. И это хорошо. Встроенный текстовой препроцессинг это его фишка, которой в других ЯП часто не хватает. Так вот, #include это не using из других языков (когда мы говорим компилятору, что хотим пользоваться таким-то модулем). Это копирование сырого текста из файла в заданное место. <br/> <br/> 2. Каждый файл .c/.cpp компилируется по отдельности. И компилятор работает с текстом этого файла, а не рассматривает его как модуль какого-то проекта. Если в main.c написать вызов функции из math_functions.c, компилятор эту функцию тупо не найдёт (её же в main.c нет). Значит, компилятору надо сказать перед вызовом каждой функции, что ГДЕ-ТО есть функция с такой сигнатурой (с таким именем и параметрами). Для этого перед первым вызовом функции должно идти её объявление без реализации, заканчивающееся вместо тела символом ;. Раз перед первым вызовом — самое удобное место это начало файла. <br/> <br/> 3. Чтобы не копипастить эти объявления без реализации руками, их выносят в файл math_functions.h, который при помощи #include подставляется в начало КАЖДОГО файла, где планируется их использовать, и таким образом эти объявления появляются в каждом файле .c/.cpp в виде текста. <br/> <br/> 4. При линковке проекта (когда откомпилированные по отдельности объектные файлы собираются в единый бинарник) линкер привязывает все вызовы мат.функций из main.c к их реализации из math_functions.c, используя имена как идентификаторы.