Por que o C ++ precisa de um arquivo de cabeçalho separado?

138

Eu realmente nunca entendi porque o C ++ precisa de um arquivo de cabeçalho separado com as mesmas funções do arquivo .cpp. Isso torna muito difícil criar classes e refatorá-las, além de adicionar arquivos desnecessários ao projeto. E há o problema de incluir arquivos de cabeçalho, mas verificar explicitamente se já foi incluído.

O C ++ foi ratificado em 1998, então, por que é projetado dessa maneira? Quais são as vantagens de ter um arquivo de cabeçalho separado?


Questão a seguir:

Como o compilador encontra o arquivo .cpp com o código, quando tudo o que incluo é o arquivo .h? Ele pressupõe que o arquivo .cpp tenha o mesmo nome que o arquivo .h, ou ele realmente procura todos os arquivos na árvore de diretórios?

Marius
fonte
2
Se você deseja editar um único arquivo, faça o checkout do lzz (www.lazycplusplus.com).
Richard Corden
3
Duplicata exata: stackoverflow.com/questions/333889 . Perto duplicado: stackoverflow.com/questions/752793
Peter Mortensen

Respostas:

105

Você parece estar perguntando sobre a separação de definições de declarações, embora existam outros usos para arquivos de cabeçalho.

A resposta é que o C ++ não "precisa" disso. Se você marcar tudo em linha (de qualquer maneira, automático para as funções de membro definidas em uma definição de classe), não haverá necessidade de separação. Você pode apenas definir tudo nos arquivos de cabeçalho.

Os motivos pelos quais você pode querer separar são:

  1. Para melhorar os tempos de construção.
  2. Para vincular ao código sem ter a fonte das definições.
  3. Para evitar marcar tudo "em linha".

Se sua pergunta mais geral é "por que o C ++ não é idêntico ao Java?", Então eu tenho que perguntar: "por que você está escrevendo C ++ em vez de Java?" ;-p

Mais seriamente, porém, o motivo é que o compilador C ++ não pode simplesmente acessar outra unidade de tradução e descobrir como usar seus símbolos, da maneira que o javac pode e faz. O arquivo de cabeçalho é necessário para declarar ao compilador o que ele pode esperar estar disponível no momento do link.

O mesmo #includeacontece com uma substituição textual direta. Se você definir tudo nos arquivos de cabeçalho, o pré-processador acaba criando uma enorme cópia e colar de todos os arquivos de origem no seu projeto, e alimentando-o no compilador. O fato de o padrão C ++ ter sido ratificado em 1998 não tem nada a ver com isso, é o fato de que o ambiente de compilação para C ++ se baseia tão intimamente no de C.

Convertendo meus comentários para responder à sua pergunta de acompanhamento:

Como o compilador encontra o arquivo .cpp com o código nele

Não, pelo menos não no momento em que compila o código que usou o arquivo de cabeçalho. As funções às quais você está vinculando nem precisam ser gravadas ainda, não importa o compilador saber em que .cpparquivo elas estarão. Tudo o que o código de chamada precisa saber no momento da compilação é expresso na declaração da função. No momento do link, você fornecerá uma lista de .oarquivos, ou bibliotecas estáticas ou dinâmicas, e o cabeçalho em vigor é uma promessa de que as definições das funções estarão lá em algum lugar.

Steve Jessop
fonte
3
Para adicionar a "As razões pelas quais você pode separar são:" A principal função dos arquivos de cabeçalho é: Separar o design da estrutura de código da implementação, porque: A. Quando você se depara com estruturas realmente complicadas que envolvem muitos objetos, é muito mais fácil examinar os arquivos de cabeçalho e lembrar como eles funcionam juntos, complementados pelos comentários do cabeçalho. B. Uma pessoa que não cuida de definir toda a estrutura do objeto e outra que cuida da implementação mantém as coisas organizadas. Acima de tudo, acho que torna o código complexo mais legível.
Andres Canella
De uma maneira mais simples, posso pensar na utilidade da separação de arquivos de cabeçalho vs. cpp é separar Interface vs. Implementações, o que realmente ajuda em projetos de médio / grande porte.
Krishna Oza
Eu pretendia que fosse incluído em (2) "link contra código sem ter as definições". OK, então você ainda pode ter acesso ao repositório git com as definições em, mas o ponto é que você pode compilar seu código separadamente das implementações de suas dependências e depois vincular. Se literalmente tudo o que você queria era separar interface / implementação em arquivos diferentes, sem a preocupação de criar separadamente, então você poderia fazer isso apenas com foo_interface.h incluindo foo_implementation.h.
21715 Steve Steveop
4
@AndresCanella Não, não. Isso torna a leitura e a manutenção do código não-seu-próprio um pesadelo. Para entender completamente o que algo faz no código, você precisa pular os arquivos 2n em vez de n. Isso simplesmente não é a notação Big-Oh, 2n faz muita diferença em comparação com apenas n.
Błażej Michalik
1
Eu digo que é uma mentira que cabeçalhos ajudam. verifique a fonte do minix, por exemplo, é tão difícil seguir onde começa onde o controle é passado, onde as coisas são declaradas / definidas. um módulo de dependência. em vez disso, você precisa seguir os cabeçalhos e isso torna a leitura de qualquer código escrito dessa maneira um inferno. por outro lado, o nodejs deixa claro de onde vem sem ifdefs e é possível identificar facilmente de onde veio.
Dmitry
90

C ++ faz dessa maneira porque C fez dessa maneira, então a verdadeira questão é por que C fez dessa maneira? A Wikipedia fala um pouco disso.

Linguagens compiladas mais recentes (como Java, C #) não usam declarações de encaminhamento; identificadores são reconhecidos automaticamente dos arquivos de origem e lidos diretamente dos símbolos da biblioteca dinâmica. Isso significa que os arquivos de cabeçalho não são necessários.

Donald Byrd
fonte
12
+1 Acerta a unha na cabeça. Isso realmente não requer uma explicação detalhada.
MSalters 20/08/09
6
Ele não acertou minha cabeça :( Ainda tenho que procurar por que o C ++ precisa usar declarações de encaminhamento e por que não consegue reconhecer identificadores de arquivos de origem e ler diretamente de símbolos dinâmicos da biblioteca e por que o C ++ fez isso só porque C fez assim: p
Alexander Taylor
2
E você é um programador melhor por ter feito isso @AlexanderTaylor :)
Donald Byrd
66

Algumas pessoas consideram os arquivos de cabeçalho uma vantagem:

  • Alega-se que ele habilita / reforça / permite a separação da interface e da implementação - mas, geralmente, esse não é o caso. Os arquivos de cabeçalho estão cheios de detalhes de implementação (por exemplo, variáveis ​​de membro de uma classe precisam ser especificadas no cabeçalho, mesmo que não façam parte da interface pública), e as funções podem, e geralmente são, definidas em linha na declaração de classe no cabeçalho, destruindo novamente essa separação.
  • Às vezes, é dito que melhora o tempo de compilação, porque cada unidade de tradução pode ser processada independentemente. E, no entanto, o C ++ é provavelmente a linguagem mais lenta que existe quando se trata de tempos de compilação. Parte do motivo são as muitas inclusões repetidas do mesmo cabeçalho. Um grande número de cabeçalhos é incluído por várias unidades de tradução, exigindo que elas sejam analisadas várias vezes.

Por fim, o sistema de cabeçalho é um artefato dos anos 70 quando o C foi projetado. Naquela época, os computadores tinham muito pouca memória e manter o módulo inteiro na memória simplesmente não era uma opção. Um compilador teve que começar a ler o arquivo na parte superior e prosseguir linearmente pelo código-fonte. O mecanismo do cabeçalho permite isso. O compilador não precisa considerar outras unidades de tradução, apenas precisa ler o código de cima para baixo.

E o C ++ manteve esse sistema para compatibilidade com versões anteriores.

Hoje não faz sentido. É ineficiente, propenso a erros e complicado demais. Existem maneiras muito melhores de separar interface e implementação, se esse era o objetivo.

No entanto, uma das propostas para o C ++ 0x era adicionar um sistema de módulo adequado, permitindo a compilação de código semelhante ao .NET ou Java, em módulos maiores, tudo de uma só vez e sem cabeçalhos. Essa proposta não foi aprovada no C ++ 0x, mas acredito que ainda esteja na categoria "adoraríamos fazer isso mais tarde". Talvez em um TR2 ou similar.

jalf
fonte
Esta é a melhor resposta na página. Obrigado!
Chuck Le Butt
29

Para o meu entendimento (limitado - normalmente não sou desenvolvedor de C), isso está enraizado no C. Lembre-se de que o C não sabe o que são classes ou namespaces, é apenas um programa longo. Além disso, as funções precisam ser declaradas antes de você usá-las.

Por exemplo, o seguinte deve dar um erro do compilador:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

O erro deve ser que "SomeOtherFunction não é declarado" porque você o chama antes da declaração. Uma maneira de corrigir isso é movendo SomeOtherFunction acima de SomeFunction. Outra abordagem é declarar a assinatura das funções primeiro:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Isso permite que o compilador saiba: Procure em algum lugar no código, existe uma função chamada SomeOtherFunction que retorna nula e não aceita parâmetros. Portanto, se você encontrar um código que tente chamar SomeOtherFunction, não entre em pânico e procure por ele.

Agora, imagine que você tenha SomeFunction e SomeOtherFunction em dois arquivos .c diferentes. Você precisa # incluir "SomeOther.c" em Some.c. Agora, adicione algumas funções "particulares" ao SomeOther.c. Como C não conhece funções privadas, essa função também estaria disponível em Some.c.

É aqui que entram os arquivos .h: Eles especificam todas as funções (e variáveis) que você deseja 'Exportar' de um arquivo .c que pode ser acessado em outros arquivos .c. Dessa forma, você ganha algo como um escopo Público / Privado. Além disso, você pode fornecer esse arquivo .h para outras pessoas sem precisar compartilhar seu código-fonte - os arquivos .h também funcionam com arquivos .lib compilados.

Portanto, o principal motivo é realmente a conveniência, a proteção do código-fonte e a dissociação entre as partes do aplicativo.

Isso foi C embora. O C ++ introduziu Classes e modificadores privado / público; portanto, enquanto você ainda pode perguntar se eles são necessários, o C ++ AFAIK ainda requer declaração de funções antes de usá-los. Além disso, muitos desenvolvedores de C ++ também são ou eram devleopers de C e assumiram seus conceitos e hábitos para C ++ - por que mudar o que não está quebrado?

Michael Stum
fonte
5
Por que o compilador não pode executar o código e encontrar todas as definições de função? Parece algo que seria muito fácil de programar no compilador.
Marius
3
Se você tem a fonte, a qual geralmente não possui. O C ++ compilado é efetivamente um código de máquina com apenas informações adicionais suficientes para carregar e vincular o código. Em seguida, aponte a CPU para o ponto de entrada e deixe-a executar. Isso é fundamentalmente diferente de Java ou C #, onde o código é compilado em um bytecode intermediário contendo metadados em seu conteúdo.
DevSolar
Suponho que, em 1972, isso poderia ter sido uma operação bastante cara para o compilador.
Michael Stum
Exatamente o que Michael Stum disse. Quando esse comportamento foi definido, uma varredura linear através de uma única unidade de tradução era a única coisa que podia ser praticamente implementada.
jalf
3
Sim - compilar em um 16 amargo com armazenamento em massa de fita não é trivial.
MSalters 20/08/09
11

Primeira vantagem: se você não possui arquivos de cabeçalho, seria necessário incluir arquivos de origem em outros arquivos de origem. Isso faria com que os arquivos inclusos fossem compilados novamente quando o arquivo incluído fosse alterado.

Segunda vantagem: Permite compartilhar as interfaces sem compartilhar o código entre diferentes unidades (diferentes desenvolvedores, equipes, empresas etc.)

erelender
fonte
1
Você está sugerindo que, por exemplo, em C # 'você precisaria incluir arquivos de origem em outros arquivos de origem'? Porque obviamente você não. Para a segunda vantagem, acho que isso depende muito do idioma: você não usará os arquivos .h, por exemplo, no Delphi
Vlagged
Você precisa recompilar o projeto inteiro de qualquer maneira, a primeira vantagem realmente conta?
Marius
ok, mas eu não acho que esse recurso de idioma. É mais prático lidar com a declaração C antes da definição "problema". É como alguém famoso dizendo "isso não é um bug que é uma característica" :)
neuro
@ Marius: Sim, realmente conta. Vincular todo o projeto é diferente de compilar e vincular todo o projeto. E à medida que o número de arquivos no projeto aumenta, a compilação de todos fica realmente irritante. @ Vlagged: Você está certo, mas eu não comparei c ++ com outro idioma. Eu comparei usando apenas arquivos de origem vs usando arquivos de origem e cabeçalho.
erelender
O C # não inclui arquivos de origem em outras pessoas, mas você ainda precisa fazer referência aos módulos - e isso faz o compilador buscar os arquivos de origem (ou refletir no binário) para analisar os símbolos que seu código usa.
gbjbaanb 20/08/09
5

A necessidade de arquivos de cabeçalho resulta das limitações que o compilador possui para saber sobre as informações de tipo para funções e ou variáveis ​​em outros módulos. O programa ou a biblioteca compilada não inclui as informações de tipo exigidas pelo compilador para vincular a qualquer objeto definido em outras unidades de compilação.

Para compensar essa limitação, C e C ++ permitem declarações e essas declarações podem ser incluídas em módulos que as utilizam com a ajuda da diretiva #include do pré-processador.

Idiomas como Java ou C #, por outro lado, incluem as informações necessárias para a ligação na saída do compilador (arquivo de classe ou assembly). Portanto, não é mais necessário manter declarações independentes a serem incluídas pelos clientes de um módulo.

O motivo para as informações de ligação não serem incluídas na saída do compilador é simples: não é necessário no tempo de execução (qualquer verificação de tipo ocorre no tempo de compilação). Seria apenas um desperdício de espaço. Lembre-se de que o C / C ++ vem de uma época em que o tamanho de um executável ou biblioteca importava bastante.

VoidPointer
fonte
Eu concordo com você. Eu tive uma idéia semelhante aqui: stackoverflow.com/questions/3702132/…
smwikipedia
4

O C ++ foi projetado para adicionar recursos modernos da linguagem de programação à infraestrutura C, sem alterar desnecessariamente nada sobre o C que não fosse especificamente sobre a própria linguagem.

Sim, neste momento (10 anos após o primeiro padrão C ++ e 20 anos após começar a aumentar seriamente o uso), é fácil perguntar por que ele não possui um sistema de módulos adequado. Obviamente, qualquer nova linguagem projetada hoje não funcionaria como C ++. Mas esse não é o objetivo do C ++.

O objetivo do C ++ é ser evolutivo, uma continuação suave da prática existente, adicionando apenas novos recursos sem (com muita freqüência) quebrar coisas que funcionam adequadamente para sua comunidade de usuários.

Isso significa que isso torna algumas coisas mais difíceis (especialmente para as pessoas que estão iniciando um novo projeto), e algumas coisas mais fáceis (especialmente para as que mantêm o código existente) do que outras línguas.

Então, em vez de esperar que o C ++ se transforme em C # (o que seria inútil, pois já temos C #), por que não escolher a ferramenta certa para o trabalho? Eu mesmo, escrevo partes significativas de novas funcionalidades em uma linguagem moderna (por acaso uso C #) e tenho uma grande quantidade de C ++ existente que estou mantendo em C ++, porque não haveria valor real em reescrevê-lo tudo. Eles se integram muito bem de qualquer maneira, por isso é praticamente indolor.

Daniel Earwicker
fonte
Como você integra C # e C ++? Através do COM?
22640 Peter Mortensen
1
Existem três maneiras principais: o "melhor" depende do seu código existente. Eu usei os três. O que eu mais uso é o COM, porque meu código existente já foi projetado em torno dele, por isso é praticamente perfeito, funciona muito bem para mim. Em alguns lugares estranhos, eu uso C ++ / CLI, que oferece uma integração incrivelmente suave para qualquer situação em que você ainda não tenha interfaces COM (e você pode preferir usar interfaces COM existentes, mesmo que as possua). Finalmente, há p / invoke, que basicamente permite chamar qualquer função semelhante a C exposta a partir de uma DLL, permitindo chamar diretamente qualquer API Win32 a partir de C #.
21119 Daniel Earwicker
4

Bem, o C ++ foi ratificado em 1998, mas estava em uso por muito mais tempo do que isso, e a ratificação estava principalmente estabelecendo o uso atual, em vez de impor estrutura. E como o C ++ foi baseado em C e C possui arquivos de cabeçalho, o C ++ também os possui.

O principal motivo dos arquivos de cabeçalho é permitir a compilação separada de arquivos e minimizar as dependências.

Digamos que tenho foo.cpp e desejo usar o código dos arquivos bar.h / bar.cpp.

Posso # incluir "bar.h" em foo.cpp e, em seguida, programar e compilar foo.cpp, mesmo que o bar.cpp não exista. O arquivo de cabeçalho atua como uma promessa ao compilador de que as classes / funções em bar.h existirão em tempo de execução e já tem tudo o que precisa saber.

Obviamente, se as funções em bar.h não tiverem corpos quando tento vincular meu programa, ele não será vinculado e receberei um erro.

Um efeito colateral é que você pode fornecer aos usuários um arquivo de cabeçalho sem revelar seu código-fonte.

Outra é que, se você alterar a implementação do seu código no arquivo * .cpp, mas não alterar o cabeçalho, precisará compilar o arquivo * .cpp em vez de tudo o que o usa. Obviamente, se você colocar muita implementação no arquivo de cabeçalho, isso se tornará menos útil.

Mark Krenitsky
fonte
3

Ele não precisa de um arquivo de cabeçalho separado com as mesmas funções que no main. É necessário apenas se você desenvolver um aplicativo usando vários arquivos de código e se usar uma função que não foi declarada anteriormente.

É realmente um problema de escopo.

Alex Rodrigues
fonte
1

O C ++ foi ratificado em 1998, então, por que é projetado dessa maneira? Quais são as vantagens de ter um arquivo de cabeçalho separado?

Na verdade, os arquivos de cabeçalho se tornam muito úteis ao examinar os programas pela primeira vez, ao verificar os arquivos de cabeçalho (usando apenas um editor de texto) fornece uma visão geral da arquitetura do programa, diferente de outros idiomas em que você precisa usar ferramentas sofisticadas para exibir classes e suas funções de membro.

Diaa Sami
fonte
1

Eu acho que a verdadeira (historical) razão por trás de arquivos de cabeçalho estava fazendo como mais fácil para desenvolvedores de compilador ... mas, em seguida, arquivos de cabeçalho não dar vantagens.
Verifique este post anterior para mais discussões ...

Paolo Tedesco
fonte
1

Bem, você pode perfeitamente desenvolver C ++ sem arquivos de cabeçalho. De fato, algumas bibliotecas que usam modelos intensivamente não usam o paradigma dos arquivos de cabeçalho / código (consulte a seção boost). Mas no C / C ++ você não pode usar algo que não está declarado. Uma maneira prática de lidar com isso é usar arquivos de cabeçalho. Além disso, você obtém a vantagem de compartilhar interface sem código / implementação de compartilhamento. E acho que isso não foi previsto pelos criadores de C: quando você usa arquivos de cabeçalho compartilhados, deve usar o famoso:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

// [...]
// my header
// [...]

#endif // MY_HEADER_SWEET_GUARDIAN

esse não é realmente um recurso de idioma, mas uma maneira prática de lidar com a inclusão múltipla.

Então, acho que quando C foi criado, os problemas com a declaração de encaminhamento foram subestimados e agora, ao usar uma linguagem de alto nível como C ++, temos que lidar com esse tipo de coisa.

Outro fardo para nós, usuários pobres de C ++ ...

neuro
fonte
1

Se você deseja que o compilador descubra símbolos definidos em outros arquivos automaticamente, você precisa forçar o programador a colocar esses arquivos em locais predefinidos (como a estrutura de pacotes Java determina a estrutura de pastas do projeto). Eu prefiro arquivos de cabeçalho. Além disso, você precisaria das fontes de bibliotecas usadas ou de alguma maneira uniforme para colocar as informações necessárias pelo compilador nos binários.

Tadeusz Kopec
fonte