int val;
Является одновременно и определением и объявлением.
В данном случае происходит реальное выделение памяти под переменную.
Объявление это
extern int val;
Оно говорит компилятору, что где-то есть переменная val типа int. В этом случае компилятор уже знает какие операции допустимо использовать с этой переменной, но память под переменную не выделяет. Реальный адрес переменной подставит линковщик, когда будет собирать исполняемый файл из нескольких единиц трансляции.
Если в одной единице трансляции вы используете объявление переменной, то где-то (в другой единице трансляции или в этой же) вы должны обязательно сделать определение. Иначе линковщик не найдя определения выдаст undefined reference.
Если же определять переменную в каждой единице трансляции, то линковщик выругается на redefinition symbol, т.е. несколько символов с одним именем (переопределение существующего символа).
Если вам все таки нужны символы с одним именем в разных единицах трансляции, то вы должны объявлять их static. В этом случае будет использоваться локальная для данной единицы трансляции переменная с данным именем.
С функциями все аналогично.