Есть файлы math_functions.c и main.c. Я же могу просто в main.c использовать функции из файла math_functions.c? Но нет нужно ещё объявить этот самый math_functions.h в котором нужно определить функции, вот этого я не понимаю зачем ?
1. В отличие от других ЯП, Си плотно привязан к обработке текста. И это хорошо. Встроенный текстовой препроцессинг это его фишка, которой в других ЯП часто не хватает. Так вот, #include это не using из других языков (когда мы говорим компилятору, что хотим пользоваться таким-то модулем). Это копирование сырого текста из файла в заданное место.
2. Каждый файл .c/.cpp компилируется по отдельности. И компилятор работает с текстом этого файла, а не рассматривает его как модуль какого-то проекта. Если в main.c написать вызов функции из math_functions.c, компилятор эту функцию тупо не найдёт (её же в main.c нет). Значит, компилятору надо сказать перед вызовом каждой функции, что ГДЕ-ТО есть функция с такой сигнатурой (с таким именем и параметрами). Для этого перед первым вызовом функции должно идти её объявление без реализации, заканчивающееся вместо тела символом ;. Раз перед первым вызовом — самое удобное место это начало файла.
3. Чтобы не копипастить эти объявления без реализации руками, их выносят в файл math_functions.h, который при помощи #include подставляется в начало КАЖДОГО файла, где планируется их использовать, и таким образом эти объявления появляются в каждом файле .c/.cpp в виде текста.
4. При линковке проекта (когда откомпилированные по отдельности объектные файлы собираются в единый бинарник) линкер привязывает все вызовы мат.функций из main.c к их реализации из math_functions.c, используя имена как идентификаторы.