Embora você possa incluir .cpparquivos como você mencionou, é uma má idéia.
Como você mencionou, as declarações pertencem aos arquivos de cabeçalho. Isso não causa problemas quando incluído em várias unidades de compilação, porque não inclui implementações. Incluir a definição de uma função ou membro da classe várias vezes normalmente causa um problema (mas nem sempre) porque o vinculador fica confuso e gera um erro.
O que deve acontecer é que cada .cpparquivo inclui definições para um subconjunto do programa, como uma classe, grupo de funções logicamente organizado, variáveis estáticas globais (use com moderação, se houver), etc.
Cada unidade de compilação ( .cpparquivo) inclui as declarações necessárias para compilar as definições que ela contém. Ele monitora as funções e as classes mencionadas, mas não contém, para que o vinculador possa resolvê-las mais tarde, quando combinar o código do objeto em um executável ou biblioteca.
Exemplo
Foo.h -> contém declaração (interface) para a classe Foo.
Foo.cpp -> contém definição (implementação) para a classe Foo.
Main.cpp-> contém o método principal, ponto de entrada do programa. Esse código instancia um Foo e o usa.
Ambos Foo.cppe Main.cppprecisam incluir Foo.h. Foo.cppprecisa porque está definindo o código que suporta a interface da classe, portanto, precisa saber o que é essa interface. Main.cppprecisa dele porque está criando um Foo e invocando seu comportamento; portanto, ele precisa saber qual é esse comportamento, o tamanho de um Foo na memória e como encontrar suas funções, etc., mas ainda não precisa da implementação real.
O compilador irá gerar a Foo.opartir do Foo.cppqual contém todo o código da classe Foo no formato compilado. Também gera o Main.oque inclui o método principal e as referências não resolvidas da classe Foo.
Agora vem o vinculador, que combina os dois arquivos de objeto Foo.oe Main.oem um arquivo executável. Ele vê as referências não resolvidas de Foo, Main.omas vê que Foo.ocontém os símbolos necessários; portanto, "liga os pontos", por assim dizer. Uma chamada de função Main.oagora está conectada ao local real do código compilado, portanto, em tempo de execução, o programa pode ir para o local correto.
Se você incluísse o Foo.cpparquivo Main.cpp, haveria duas definições da classe Foo. O vinculador veria isso e diria "Não sei qual escolher, então isso é um erro". A etapa de compilação seria bem-sucedida, mas a vinculação não. (A menos que você apenas não compile, Foo.cppmas por que está em um .cpparquivo separado ?)
Finalmente, a ideia de diferentes tipos de arquivo é irrelevante para um compilador C / C ++. Ele compila "arquivos de texto" que esperançosamente contêm código válido para o idioma desejado. Às vezes, é possível saber o idioma com base na extensão do arquivo. Por exemplo, compile um .carquivo sem opções de compilador e ele assumirá C, enquanto uma extensão .ccou .cppdiria para ele assumir C ++. No entanto, posso dizer facilmente a um compilador para compilar um arquivo .hou mesmo .docxcomo C ++, e ele emitirá um .oarquivo object ( ) se contiver código C ++ válido em formato de texto sem formatação. Essas extensões são mais para o benefício do programador. Se eu vir Foo.he Foo.cpp, presumo imediatamente que o primeiro contém a declaração da classe e o segundo contém a definição.