Por que precisamos incluir o .h enquanto tudo funciona quando se inclui apenas o arquivo .cpp?


18

Por que precisamos incluir os arquivos .he .cppenquanto podemos fazê-lo funcionar apenas incluindo o .cpparquivo?

Por exemplo: criando uma file.hdeclaração contendo, em seguida, criando uma file.cppdefinição contendo e incluindo ambas em main.cpp.

Como alternativa: criar uma file.cppdeclaração / definições contendo (sem protótipos) incluindo-a main.cpp.

Ambos trabalham para mim. Não vejo a diferença. Talvez algumas dicas sobre o processo de compilação e vinculação possam ajudar.


Compartilhar sua pesquisa ajuda a todos . Conte-nos o que você tentou e por que ele não atendeu às suas necessidades. Isso demonstra que você reservou um tempo para tentar ajudar a si mesmo, evita reiterar respostas óbvias e, acima de tudo, ajuda a obter uma resposta mais específica e relevante. Veja também Como perguntar
gnat

Eu recomendo fortemente olhar para as questões relacionadas aqui ao lado. programmers.stackexchange.com/questions/56215/… e programmers.stackexchange.com/questions/115368/… são boas leituras

Por que isso ocorre nos programadores e não no SO?
Lightness Races com Monica

@LightnessRacesInOrbit porque é uma questão de estilo de codificação e não uma questão de "como fazer".
precisa saber é o seguinte

@CashCow: Parece que você entendeu mal o SO, que não é um site de "como fazer". Também parece que você entendeu mal esta questão, que não é uma questão de estilo de codificação.
Lightness Races com Monica

Respostas:


29

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.


Eu não entendo o argumento que você está fazendo aqui. Se você acabou de incluir Foo.cppna Main.cppvocê não precisa incluir o .harquivo, você tem menos um arquivo, você ainda ganhar no código de divisão em arquivos separados para facilitar a leitura, e seu comando de compilação é mais simples. Embora compreenda a importância de cabeçalho de arquivos Eu não acho que é isso
Benjamin Gruenbaum

2
Para um programa trivial, basta colocar tudo em um arquivo. Nem inclua nenhum cabeçalho de projeto (apenas cabeçalhos de biblioteca). A inclusão de arquivos de origem é um mau hábito que causará problemas para todos os programas, exceto os menores, quando você tiver várias unidades de compilação e compilá-las separadamente.

2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Frase mais importante sobre a questão.
Marczellm

@ Snowman Right, que é o que eu acho que você deveria se concentrar - várias unidades de compilação. A colagem de código com / sem arquivos de cabeçalho para dividir o código em partes menores é útil e ortogonal ao objetivo dos arquivos de cabeçalho. Se tudo o que queremos fazer é dividi-lo em arquivos de cabeçalho, os arquivos não nos compram nada - uma vez que precisamos de vínculo dinâmico, vínculo condicional e mais compilações avançadas, de repente eles são muito úteis.
Benjamin Gruenbaum

Existem várias maneiras de organizar a origem de um compilador / vinculador C / C ++. O solicitante não tinha certeza do processo, então expliquei a melhor prática de como organizar o código e como o processo funciona. Você pode dividir os detalhes sobre a possibilidade de organizar o código de maneira diferente, mas o fato é que expliquei como 99% dos projetos lá fora o fazem, o que é a melhor prática por um motivo. Funciona bem e mantém sua sanidade.

9

Leia mais sobre o papel do pré-processador C e C ++ , que é conceitualmente a primeira "fase" do compilador C ou C ++ (historicamente, era um programa separado /lib/cpp; agora, por motivos de desempenho, ele é integrado ao compilador propriamente dito cc1 ou cc1plus). Leia em particular a documentação do cpppré-processador GNU . Portanto, na prática, o compilador pré-processa primeiro sua unidade de compilação (ou unidade de tradução ) e depois trabalha no formulário pré-processado.

Você provavelmente sempre precisará incluir o arquivo de cabeçalho, file.hse ele contiver (conforme exigido por convenções e hábitos ):

  • definições de macro
  • definições tipos (por exemplo typedef, struct, classetc, ...)
  • definições de static inlinefunções
  • declarações de funções externas.

Observe que é uma questão de convenções (e conveniência) colocá-las em um arquivo de cabeçalho.

Obviamente, sua implementação file.cppprecisa de todas as opções acima, então, #include "file.h" primeiro você precisa .

Esta é uma convenção (mas muito comum). Você pode evitar os arquivos de cabeçalho e copiar e colar seu conteúdo nos arquivos de implementação (ou seja, unidades de tradução). Mas você não deseja isso (exceto, talvez, se seu código C ou C ++ for gerado automaticamente; você poderá fazer o programa gerador copiar e colar, imitando o papel do pré-processador).

O ponto é que o pré-processador está executando operações apenas de texto . Você pode (em princípio) evitá-lo inteiramente copiando e colando ou substituindo-o por outro "pré-processador" ou gerador de código C (como gpp ou m4 ).

Uma questão adicional é que os padrões recentes de C (ou C ++) definem vários cabeçalhos padrão. A maioria das implementações realmente implementa esses cabeçalhos padrão como arquivos (específicos da implementação) , mas acredito que seria possível para uma implementação em conformidade implementar inclusões padrão (como #include <stdio.h>para C ou #include <vector>C ++) com alguns truques de mágica (por exemplo, usando algum banco de dados ou alguns informações dentro do compilador).

Se estiver usando compiladores GCC (por exemplo, gccou g++), você pode usar o -Hsinalizador para se informar sobre todas as inclusões e os -C -Esinalizadores para obter o formulário pré-processado. Obviamente, existem muitos outros sinalizadores do compilador que afetam o pré-processamento (por exemplo, -I /some/dir/para adicionar /some/dir/arquivos de pesquisa incluídos e -Dpara predefinir alguma macro do pré-processador, etc, etc ...).

NB Versões futuras do C ++ (talvez C ++ 20 , talvez até mais tarde) podem ter módulos C ++ .


1
Se os links apodrecerem, essa ainda seria uma boa resposta?
Dan Pichelman

4
Editei minha resposta para melhorá-la e escolho cuidadosamente os links - para a wikipedia ou para a documentação do GNU - que acredito que permanecerão relevantes por um longo tempo.
Basile Starynkevitch

3

Devido ao modelo de compilação de várias unidades do C ++, você precisa de um código que apareça no seu programa apenas uma vez (definições) e de um código que apareça em cada unidade de tradução do seu programa (declarações).

Daí nasce o idioma do cabeçalho C ++. É convenção por uma razão.

Você pode despejar todo o programa em uma única unidade de tradução, mas isso apresenta problemas com a reutilização de código, teste de unidade e manipulação de dependência entre módulos. Também é apenas uma grande bagunça.


0

A resposta selecionada de Por que precisamos escrever um arquivo de cabeçalho? é uma explicação razoável, mas eu queria adicionar detalhes adicionais.

Parece que o racional para arquivos de cabeçalho tende a se perder ao ensinar e discutir C / C ++.

O arquivo de cabeçalhos fornece uma solução para dois problemas de desenvolvimento de aplicativos:

  1. Separação de interface e implementação
  2. Tempos de compilação / link aprimorados para programas grandes

O C / C ++ pode variar de pequenos programas a grandes programas de arquivos de vários milhões de linhas e milhares de arquivos. O desenvolvimento de aplicativos pode variar de equipes de um desenvolvedor a centenas de desenvolvedores.

Você pode usar vários chapéus como desenvolvedor. Em particular, você pode ser o usuário de uma interface para funções e classes ou pode ser o escritor de uma interface de funções e classes.

Ao usar uma função, você precisa conhecer a interface da função, quais parâmetros usar, quais as funções retornadas e você precisa saber o que a função faz. Isso é facilmente documentado no arquivo de cabeçalho sem nunca olhar para a implementação. Quando você leu a implementação de printf? Compre, usamos todos os dias.

Quando você é desenvolvedor de uma interface, o chapéu muda na outra direção. O arquivo de cabeçalho fornece a declaração da interface pública. O arquivo de cabeçalho define o que outra implementação precisa para usar essa interface. As informações internas e privadas dessa nova interface não são (e não devem) ser declaradas no arquivo de cabeçalho. O arquivo de cabeçalho público deve ser tudo o que alguém precisa para usar o módulo.

Para o desenvolvimento em larga escala, a compilação e a vinculação podem levar muito tempo. De muitos minutos a muitas horas (até muitos dias!). Dividir o software em interfaces (cabeçalhos) e implementações (fontes) fornece um método apenas para compilar arquivos que precisam ser compilados, em vez de reconstruir tudo.

Além disso, os arquivos de cabeçalho permitem que um desenvolvedor forneça uma biblioteca (já compilada) e também um arquivo de cabeçalho. Outros usuários da biblioteca podem nem sempre ver a implementação adequada, mas ainda podem usar a biblioteca com o arquivo de cabeçalho. Você faz isso todos os dias com a biblioteca padrão C / C ++.

Mesmo se você estiver desenvolvendo um aplicativo pequeno, o uso de técnicas de desenvolvimento de software em larga escala é um bom hábito. Mas também precisamos lembrar por que usamos esses hábitos.


0

Você também pode estar se perguntando por que não colocar todo o seu código em um arquivo.

A resposta mais simples é a manutenção do código.

Há momentos em que é razoável criar uma classe:

  • tudo alinhado dentro de um cabeçalho
  • tudo dentro de uma unidade de compilação e sem cabeçalho.

O momento em que é razoável alinhar totalmente em um cabeçalho é quando a classe é realmente uma estrutura de dados com alguns getters e setters básicos e talvez um construtor que aceita valores para inicializar seus membros.

(Os modelos, que precisam ser todos incorporados, são um problema um pouco diferente).

O outro momento para criar uma classe dentro de um cabeçalho é quando você pode usá-la em vários projetos e, especificamente, deseja evitar a vinculação em bibliotecas.

Um momento em que você pode incluir uma classe inteira em uma unidade de compilação e não expor seu cabeçalho é:

  • Uma classe "impl" usada apenas pela classe que implementa. É um detalhe de implementação dessa classe e externamente não é usado.

  • Uma implementação de uma classe base abstrata criada por algum tipo de método de fábrica que retorna um ponteiro / referência / ponteiro inteligente) para a classe base. O método de fábrica seria exposto pela própria classe não seria. (Além disso, se a classe tiver uma instância que se registra em uma tabela por meio de uma instância estática, ela nem precisa ser exposta através de uma fábrica).

  • Uma classe do tipo "functor".

Em outras palavras, onde você não deseja que ninguém inclua o cabeçalho.

Eu sei o que você pode estar pensando ... Se for apenas para manutenção, incluindo o arquivo cpp (ou um cabeçalho totalmente embutido), você poderá editar facilmente um arquivo para "encontrar" o código e apenas reconstruí-lo.

No entanto, "manutenibilidade" não é apenas ter o código arrumado. É uma questão de impactos de mudança. É sabido geralmente que, se você mantiver um cabeçalho inalterado e apenas alterar um (.cpp)arquivo de implementação , não será necessário reconstruir a outra fonte, pois não haverá efeitos colaterais.

Isso torna "mais seguro" fazer essas alterações sem se preocupar com o efeito indireto, e é isso que realmente significa "manutenção".


-1

O exemplo do Snowman precisa de uma pequena extensão para mostrar exatamente por que os arquivos .h são necessários.

Adicione outra barra de classe à peça, que também depende da classe Foo.

Foo.h -> contém declaração para a classe Foo

Foo.cpp -> contém a definição (impementação) da classe Foo

Main.cpp -> usa variável do tipo Foo.

Bar.cpp -> também usa variável do tipo Foo.

Agora, todos os arquivos cpp precisam incluir Foo.h Seria um erro incluir Foo.cpp em mais de um outro arquivo cpp. O vinculador falharia porque a classe Foo seria definida mais de uma vez.


1
Também não é isso. Isso pode ser resolvido exatamente da mesma maneira que é resolvido nos .harquivos de qualquer maneira - com guardas de inclusão e é exatamente o mesmo problema que você tem com os .harquivos de qualquer maneira. Não é para isso que .hservem os arquivos.
Benjamin Gruenbaum

Não tenho certeza de como você usaria os protetores de inclusão, pois eles funcionam apenas por arquivo (como o pré-processo). Nesse caso, como Main.cpp e Bar.cpp são arquivos diferentes, o Foo.cpp seria incluído apenas uma vez em ambos (protegido ou não, não faz diferença). Main.cpp e Bar.cpp seriam compilados. Mas o erro de link ainda ocorre devido à dupla definição da classe Foo. No entanto, também há herança que eu nem mencionei. Declaração para módulos externos (dll) etc.
SkaarjFace

Não, seria uma unidade de compilação, nenhuma vinculação ocorreria desde que você incluísse o .cpparquivo.
Benjamin Gruenbaum

Eu não disse que não será compilado. Mas não ligará main.o e bar.o no mesmo executável. Sem vincular qual o sentido da compilação.
SkaarjFace

Uhh ... está executando o código? Você ainda pode vinculá-lo, mas simplesmente não vincular os arquivos um ao outro - eles seriam a mesma unidade de compilação.
Benjamin Gruenbaum

-2

Se você escrever o código inteiro no mesmo arquivo, ele ficará feio.
Em segundo lugar, você não poderá compartilhar sua aula escrita com outras pessoas. De acordo com a engenharia de software, você deve escrever o código do cliente separadamente. O cliente não deve saber como seu programa está funcionando. Eles só precisam de saída Se você escrever o programa inteiro no mesmo arquivo, a segurança do programa será vazada.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.