Нет магии. Когда программа на С компилируется, есть два основных шага к ней.
Во-первых, каждая отдельная единица компиляции находится в изоляции. (Единица компиляции - это один файл .c плюс все, что в него входит).
На данном этапе он ничего не знает о том, что содержится в других файлах .c, что означает, что он не может создать полную программу. Что он может сделать, так это сгенерировать код с несколькими точками «заполнить пробелы». Если из foo.c вы вызываете функцию, которая объявлена в bar.h и определена в bar.c, то компилятор может только видеть, что функция существует. Он объявлен в bar.h, поэтому мы должны предположить, что полное определение существует где-то . Но поскольку это определение находится внутри другого модуля компиляции, мы пока не можем его увидеть. Таким образом, компилятор генерирует код для вызова функции, с небольшим примечанием о том, что «введите адрес этой функции, как только она станет известна».
После того, как каждый модуль компиляции будет скомпилирован таким образом, у вас останется куча объектных файлов (обычно .o, если скомпилирована GCC, и .obj, если вы используете MSVC), содержащих этот вид «заполнения пробелов». "код.
Теперь компоновщик берет все эти объектные файлы и пытается объединить их вместе, что позволяет ему заполнить пробелы. Теперь можно найти функцию, для которой мы сгенерировали вызов, поэтому мы можем вставить ее адрес в вызов.
Так что ничего особенного не происходит, если файл .c имеет то же имя, что и .h. Это просто соглашение, чтобы людям было проще выяснить, что находится внутри каждого файла.
Компилятору все равно. Он просто берет каждый файл .c плюс все, что он включает, и компилирует его в объектный файл. Затем компоновщик объединяет все эти объектные файлы в один исполняемый файл.