При использовании include представьте, что все файлы объединены в один файл. Это в основном то, что видит компилятор, потому что все было включено препроцессором до стадии компиляции.
Итак, ваш main.cc начинает выглядеть так.
int a;
extern int a;
Компилятор считает, что это нормально, потому что extern без инициализатора - это просто объявление, поэтому для него не выделяется память. «int a», однако, является определением, поэтому main.o включает в себя инструкции по выделению памяти для.
Как только вы связываете его, компоновщик замечает, что a.o и b.o уже определили также "a". «a», потому что это то место, где оно было первоначально определено, и «b», потому что b включает «a», которое имело определение.
Чтобы это исправить, просто уберите #include "b.cc" в main.cc. На самом деле, вынимайте b.cc целиком, потому что в этом нет никакого смысла.
Если вы действительно хотите сделать это правильно, создайте отдельный заголовок для «a» с именем a.h с помощью extern int a. Тогда main.cc и b.cc могут свободно включать a.h без переопределения a.