Хедеры служат, чтобы два c-файла — они называются «единицы трансляции» — компилировались по отдельности. Это основное назначение той разновидности include-файлов, которые называются хедерами.
Можно в h-файлы посадить и тела функций, только убедиться, что каждое тело ровно в одной единице трансляции — есть такой подход под названием «одна единица трансляции», и он существует у больших редко перекомпилируемых библиотек, чтобы перекомпиляция была покороче.
Подключение main.c←math_functions.h служит, чтобы сказать компилятору: а где-то в другом месте есть эти функции.
Подключение math_functions.c←math_functions.h — частично для подключения прочего общего вроде типов, частично для проверки на ошибки. Дело в том, что Си традиционно не козявит (does not mangle) имена функций, и если в хедере sin(int), а в реализации sin(double) — будут трудноуловимые ошибки.
Да, деление на единицы компиляции решает и другую задачу — декомпизицию программы на меньшие элементы, и есть противоречие одного с другим (особенно в языке Си++, где хедеры огромны)
В хедере находится только то, что не производит кода. Если говорить про Си++, то…
• inline-объекты (код производят вызовы объекта)
• определения типов, функций и прочего; extern-определения переменных (код производят тела функций и окончательные определения переменных)
• шаблоны, если те нужны более чем в одной единице компиляции (код производит специализация)
Но полные специализации шаблонов производят код и находятся в cpp-файле!