Здесь происходит несколько разных вещей.Сначала я расскажу, как работает базовая компиляция нескольких файлов.
Если у вас есть несколько файлов, важна разница между объявлением и определением функции.Это определение, вероятно, то, к чему вы привыкли при определении функций: вы записываете содержимое функции, например
int square(int i) {
return i*i;
}
Объявление, с другой стороны, позволяет объявить компилятору, что вы знаетеФункция существует, но вы не указываете компилятору, что это такое.Например, вы можете написать
int square(int i);
И компилятор будет ожидать, что функция "квадрат" определена в другом месте.
Теперь, если у вас есть два разных файла, которые вы хотите взаимодействовать (например, предположим, что функция "квадрат" определена в add.c, и вы хотите вызвать square (10) в main.c), вам нужно сделать и определение и объявление.Сначала вы определяете квадрат в add.c.Затем вы объявляете в начале файла main.c.Это даст компилятору знать, когда он компилирует main.c, что есть функция «квадрат», которая определена в другом месте.Теперь вам нужно скомпилировать как main.c, так и add.c в объектные файлы.Вы можете сделать это, позвонив
gcc -c main.c
gcc -c add.c
. Это создаст файлы main.o и add.o.Они содержат скомпилированные функции, но не совсем исполняемые.Здесь важно понять, что main.o в некотором смысле является «неполным».При компиляции main.o вы сказали, что функция «квадрат» существует, но функция «квадрат» не определена внутри main.o.Таким образом, main.o имеет своего рода «висячую ссылку» на функцию «квадрат».Он не скомпилируется в полную программу, если вы не объедините его с другим файлом .o (или .so или .a), который содержит определение «квадрат».Если вы просто попытаетесь связать main.o с программой, то есть
gcc -o executable main.o
Вы получите ошибку, потому что компилятор попытается разрешить зависаниессылка на функцию "квадрат", но не найду никакого определения для нее.Однако, если вы добавляете add.o при компоновке (компоновка - это процесс разрешения всех этих ссылок на неопределенные функции при конвертации файлов .o в исполняемые файлы или файлы .so), проблем не будет,то есть
gcc -o executable main.o add.o
Так вот, как функционально использовать функции в файлах C, но стилистически , то, что я только что показал вам, это "не правильный путь".Единственная причина, по которой я это сделал, заключается в том, что я думаю, что это поможет вам лучше понять, что происходит, а не полагаться на "#include magic"Теперь вы могли заметить, что до этого все становится немного грязно, если вам нужно переопределить каждую функцию, которую вы хотите использовать в начале main.c Вот почему часто программы на C используют вспомогательные файлы, называемые «заголовками», которые имеют расширение .h,Идея заголовка состоит в том, что он содержит просто объявлений функций, без их определений.Таким образом, чтобы скомпилировать программу с использованием функций, определенных в add.c, вам не нужно вручную объявлять каждую функцию, которую вы используете, и не нужно #include весь файл add.c в вашем коде.Вместо этого вы можете #include add.h, который просто содержит объявлений всех функций add.c.
Теперь обновим #include: #include просто копирует содержимое одного файла прямо в другой.Так, например, код
abc
#include "wtf.txt"
def
в точности эквивалентен
abc
hello world
def
при условии, что wtf.txt содержит текст "hello world".
Итак,если мы поместим все объявления add.c в add.h (то есть
int square(int i);
, а затем в начало main.c, мы напишем
#include "add.h"
Это функциональнотак же, как если бы мы только что вручную объявили функцию «квадрат» вверху main.c.
Итак, общая идея использования заголовков состоит в том, что у вас может быть специальный файл, который автоматически объявляет все необходимые вам функциипросто # Включая это.
Однако заголовки также имеют еще одно общее использование.Предположим, что main.c использует функции из 50 разных файлов.Верх файла main.c будет выглядеть следующим образом:
#include "add.h"
#include "divide.h"
#include "multiply.h"
#include "eat-pie.h"
...
Вместо этого люди часто перемещают все эти файлы #include в заголовочный файл main.h, а просто #include main.h из main.c.В этом случае заголовочный файл служит двум целям.Он объявляет функции из main.c для использования при включении другими файлами, и и включает все зависимости main.c при включении из main.c.Использование этого способа также позволяет цепочек зависимостей.Если вы #include add.h, вы не только получаете функции, определенные в add.c, но вы также неявно получаете любые функции, которые использует add.c, и любые функции , которые они используют, и так далее.
Также, более тонко, # включая заголовочный файл из его собственного .c файла, неявно проверяет наличие ошибок, которые вы делаете.Например, если вы случайно определили квадрат как
double square(int i);
в add.h, вы, как правило, не сможете понять, пока вы не связали этот main.o с поиском one определения квадрата,и add.o предоставляет другой, несовместимый один.Это приведет к тому, что вы получите ошибки при компоновке, поэтому вы не поймете ошибку до тех пор, пока не начнете в процессе сборки.Однако, если вы #include add.h из add.c, для компилятора ваш файл будет выглядеть как
#include "add.h"
int square(int i) {
return i*i;
}
, который после обработки оператора #include будет выглядеть как
double square(int i);
int square(int i) {
return i*i;
}
Что компилятор заметит при компиляции add.c и расскажет вам об этом.Фактически, включение собственного заголовка таким образом предотвращает ложную рекламу другим файлам типа функций, которые вы предоставляете.
Почему вы можете использовать функцию, даже не объявив ее
Как вы заметили, в некоторых случаях вы можете использовать функцию, не объявляя ее каждый раз или # включая любой файл, который ее объявляет.Это глупо, и все согласны с тем, что это глупо.Однако это унаследованная особенность языка программирования C (и компиляторов C), что если вы используете функцию, не объявив ее вначале, она просто предполагает, что это функция, возвращающая тип «int».Таким образом, использование функции неявно объявляет эту функцию как функцию, которая возвращает «int», если она еще не объявлена.Это очень странное поведение, если вы об этом думаете, и компилятор должен предупредить вас, если вы делаете это.
Заголовки
Еще одна распространенная практика - это использование"Заголовок охраны".Чтобы объяснить защиту заголовков, давайте рассмотрим возможную проблему.Допустим, у нас есть два файла: herp.c и derp.c, и они оба хотят использовать функции, содержащиеся друг в друге.Следуя приведенным выше рекомендациям, у вас может быть herp.h со строкой
#include "derp.h"
и derp.h со строкой
#include "herp.h"
Теперь, если подумать, #include "derp.h" будет преобразован в содержимое файла derp.h, который в свою очередь содержит строку #include "herp.h", которая будет преобразована в содержимое файла herp.h, и , которые содержит ... и так далее, так что компилятор будет продолжать расширять включения.Точно так же, если main.h #include и herp.h и derp.h, и и herp.h, и derp.h включают add.h, мы видим, что в main.h мы получаем two копии файла add.h, один из которых - в результате #inclusive herp.h, а другой - в результате включения derp.h.Итак, решение?«Защита заголовка», то есть фрагмент кода, который предотвращает включение заголовка дважды.Например, для add.h нормальный способ сделать это:
#ifndef ADD_H
#define ADD_H
int sqrt(int i);
...
#endif
Этот фрагмент кода, по сути, говорит препроцессору (части компилятора, который обрабатывает все операторы "#XXX"), чтобы проверить, определено ли уже "ADD_H".Если это не так (если n def), то сначала он определяет «ADD_H» (в этом контексте ADD_H не нужно определять как что-либо, это просто логическое значениекоторый либо определен, либо нет), а затем определяет остальное содержимое заголовка.Однако, если ADD_H уже определен, то #include этот файл не будет делать ничего , потому что за пределами блока #ifndef ничего нет.Таким образом, идея заключается в том, что только первый раз, когда он будет включен в любой данный файл, он фактически добавит любой текст в этот файл.После этого, #inclusive он не добавит никакого дополнительного текста в ваш файл.ADD_H - это произвольный символ, который вы выбираете, чтобы отслеживать, был ли добавлен add.h.Для каждого заголовка вы используете другой символ, чтобы отслеживать, был ли он включен или нет.Например, herp.h, вероятно, будет использовать HERP_H вместо ADD_H.Использование «функции защиты заголовка» устранит любую из перечисленных выше проблем, когда у вас есть дубликаты включенного файла или бесконечный цикл из # include.