Por que ter arquivos de cabeçalho e arquivos .cpp? [fechadas]


484

Por que o C ++ possui arquivos de cabeçalho e arquivos .cpp?



é um paradigma comum de OOP, .h é uma declaração de classe e cpp é a definição. Não é necessário saber como ele é implementado, ele / ela deve conhecer apenas a interface.
Manish Kakati

Essa é a melhor parte da interface de separação do c ++ da implementação. É sempre bom, em vez de manter todo o código no arquivo único, temos uma interface separada. Alguma quantidade de código sempre existe, como a função embutida, que faz parte dos arquivos de cabeçalho. Parece bom quando um arquivo de cabeçalho é exibido exibe a lista de funções declaradas e variáveis ​​de classe.
Miank

Há momentos em que os arquivos de cabeçalho são essenciais para a compilação - não apenas uma preferência da organização ou uma maneira de distribuir bibliotecas pré-compiladas. Digamos que você tenha uma estrutura em que game.c dependa de ambas as físicas.c e math.c; physics.c também depende de math.c. Se você incluísse arquivos .c e se esquecesse dos arquivos .h para sempre, teria declarações duplicadas de math.c e nenhuma esperança de compilação. É isso que faz mais sentido para mim porque os arquivos de cabeçalho são importantes. Espero que isso ajude mais alguém.
Samy Bencherif

Eu acho que tem a ver com o fato de que apenas caracteres alfanuméricos são permitidos em extensões. Eu nem sei se isso é verdade, apenas adivinhando #
121211554 13/10/1919

Respostas:


202

Bem, o principal motivo seria separar a interface da implementação. O cabeçalho declara "o que" uma classe (ou o que está sendo implementado) fará, enquanto o arquivo cpp define "como" ele executará esses recursos.

Isso reduz as dependências para que o código que usa o cabeçalho não precise necessariamente conhecer todos os detalhes da implementação e quaisquer outras classes / cabeçalhos necessários apenas para isso. Isso reduzirá os tempos de compilação e também a quantidade de recompilação necessária quando algo na implementação for alterado.

Não é perfeito, e você geralmente recorreria a técnicas como o Pimpl Idiom para separar adequadamente a interface e a implementação, mas é um bom começo.


178
Na verdade não é verdade. O cabeçalho ainda contém uma parte importante da implementação. Desde quando as variáveis ​​de instância privada faziam parte da interface de uma classe? Funções membro privadas? Então, o que diabos eles estão fazendo no cabeçalho publicamente visível? E desmorona ainda mais com os modelos.
jalf

13
Por isso eu disse que não é perfeito, e o idioma Pimpl é necessário para mais separação. Os modelos são uma lata de worms totalmente diferente - mesmo se a palavra-chave "exports" fosse totalmente suportada na maioria dos compiladores, ainda assim seria mais um açúcar sintático do que uma separação real.
Joris Timmermans

4
Como outros idiomas lidam com isso? por exemplo - Java? Não há conceito de arquivo de cabeçalho em Java.
Lazer

8
@Lazer: Java é mais simples de analisar. O compilador Java pode analisar um arquivo sem conhecer todas as classes em outros arquivos e verificar os tipos posteriormente. No C ++, muitas construções são ambíguas sem informações de tipo, portanto, o compilador C ++ precisa de informações sobre tipos referenciados para analisar um arquivo. É por isso que precisa de cabeçalhos.
Niki

15
@nikie: O que a "facilidade" de análise tem a ver com isso? Se o Java tivesse uma gramática pelo menos tão complexa quanto o C ++, ainda assim poderia usar apenas arquivos java. Nos dois casos, o que acontece com C? C é fácil de analisar, mas usa os cabeçalhos e os arquivos c.
Thomas Eding

609

Compilação C ++

Uma compilação em C ++ é feita em 2 fases principais:

  1. O primeiro é a compilação de arquivos de texto "de origem" em arquivos binários de "objeto": O arquivo CPP é o arquivo compilado e é compilado sem qualquer conhecimento sobre os outros arquivos CPP (ou mesmo bibliotecas), a menos que seja fornecido por declaração bruta ou inclusão de cabeçalho. O arquivo CPP geralmente é compilado em um arquivo "objeto ".OBJ ou .O.

  2. A segunda é a ligação de todos os arquivos "objetos" e, portanto, a criação do arquivo binário final (uma biblioteca ou um executável).

Onde o HPP se encaixa em todo esse processo?

Um arquivo pobre e solitário de CPP ...

A compilação de cada arquivo CPP é independente de todos os outros arquivos CPP, o que significa que se A.CPP precisar de um símbolo definido em B.CPP, como:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

Ele não será compilado porque o A.CPP não tem como saber que "doSomethingElse" existe ... A menos que haja uma declaração no A.CPP, como:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

Em seguida, se você tiver o C.CPP que usa o mesmo símbolo, copie / cole a declaração ...

ALERTA DE COPIAR / COLAR!

Sim, há um problema. As cópias / pastas são perigosas e difíceis de manter. O que significa que seria legal se tivéssemos alguma maneira de NÃO copiar / colar e ainda declarar o símbolo ... Como podemos fazer isso? Pela inclusão de algum arquivo de texto, que geralmente é sufixado por .h, .hxx, .h ++ ou, o meu preferido para arquivos C ++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

Como includefunciona?

A inclusão de um arquivo irá, em essência, analisar e depois copiar e colar seu conteúdo no arquivo CPP.

Por exemplo, no código a seguir, com o cabeçalho A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... a fonte B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... se tornará após a inclusão:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

Uma pequena coisa - por que incluir B.HPP em B.CPP?

No caso atual, isso não é necessário e B.HPP possui a doSomethingElsedeclaração da função e B.CPP possui a doSomethingElsedefinição da função (que é, por si só, uma declaração). Porém, em um caso mais geral, em que o B.HPP é usado para declarações (e código embutido), não pode haver uma definição correspondente (por exemplo, enumerações, estruturas simples, etc.), portanto, a inclusão pode ser necessária se o B.CPP usa essas declarações de B.HPP. Em suma, é "bom gosto" que uma fonte inclua por padrão seu cabeçalho.

Conclusão

O arquivo de cabeçalho é, portanto, necessário, porque o compilador C ++ não pode procurar apenas declarações de símbolo e, portanto, você deve ajudá-lo incluindo essas declarações.

Uma última palavra: você deve colocar os protetores de cabeçalho em torno do conteúdo dos arquivos HPP, para garantir que várias inclusões não quebrem nada, mas, apesar de tudo, acredito que o principal motivo da existência de arquivos HPP esteja explicado acima.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

ou ainda mais simples

#pragma once

// The declarations in the B.hpp file

2
@ nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?Não há necessidade. Desde que o CPP "inclua" o HPP, o pré-compilador fará automaticamente a cópia e colar do conteúdo do arquivo HPP no arquivo CPP. Eu atualizei a resposta para esclarecer isso.
paercebal

7
@Bob: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Não, não faz. Ele conhece apenas os tipos fornecidos pelo usuário, que, na metade do tempo, nem se preocupam em ler o valor de retorno. Então, conversões implícitas acontecem. E então, quando você tem o código:, foo(bar)você nem pode ter certeza de que fooé uma função. Portanto, o compilador precisa ter acesso às informações nos cabeçalhos para decidir se a fonte é compilada corretamente ou não ... Então, uma vez que o código é compilado, o vinculador vincula apenas as chamadas de funções.
paercebal

3
@ Bob: [continuando] ... Agora, o vinculador poderia fazer o trabalho feito pelo compilador, eu acho, o que tornaria sua opção possível. (Acho que esse é o assunto da proposição "módulos" para o próximo padrão). Seems, they're just a pretty ugly arbitrary design.: Se C ++ tivesse sido criado em 2012, de fato. Mas lembre-se de que o C ++ foi construído sobre o C na década de 1980 e, na época, as restrições eram bem diferentes na época (IIRC, foi decidido para fins de adoção manter os mesmos vinculadores que os do C).
paercebal

1
@ paercebal Obrigado pela explicação e notas, paercebal! Por que não tenho certeza, isso foo(bar)é uma função - se é obtida como um ponteiro? De fato, por falar em design ruim, culpo C, não C ++. Eu realmente não gosto de algumas restrições de C puro, como ter arquivos de cabeçalho ou funções retornar um e apenas um valor, enquanto utiliza vários argumentos na entrada (não é natural que uma entrada e uma saída se comportem de maneira semelhante ; por isso vários argumentos, mas única saída) :)?
Boris Burkov

1
@Bobo:: Why can't I be sure, that foo(bar) is a functionfoo poderia ser um tipo, então você teria um construtor de classe chamado. In fact, speaking of bad design, I blame C, not C++: Eu posso culpar C por muitas coisas, mas ter sido projetado nos anos 70 não será um deles. Novamente, as restrições daquele tempo ... such as having header files or having functions return one and only one value: as tuplas podem ajudar a atenuar isso, além de transmitir argumentos por referência. Agora, qual seria a sintaxe para recuperar vários valores retornados e valeria a pena mudar o idioma?
paercebal

93

Como C, onde o conceito se originou, tem 30 anos e, naquela época, era a única maneira viável de vincular código de vários arquivos.

Hoje, é um hack horrível que destrói totalmente o tempo de compilação em C ++, causa inúmeras dependências desnecessárias (porque as definições de classe em um arquivo de cabeçalho expõem muitas informações sobre a implementação) e assim por diante.


3
Eu me pergunto por que os arquivos de cabeçalho (ou o que realmente era necessário para a compilação / vinculação) não eram simplesmente "gerados automaticamente"?
Mateen Ulhaq

54

Como no C ++, o código executável final não carrega nenhuma informação de símbolo, é um código de máquina mais ou menos puro.

Portanto, você precisa de uma maneira de descrever a interface de um pedaço de código, separado do próprio código. Esta descrição está no arquivo de cabeçalho.


16

Porque o C ++ os herdou de C. Infelizmente.


4
Por que a herança de C ++ de C é lamentável?
precisa

3
@Lokesh Por causa de sua bagagem :(
陳 力

1
Como isso pode ser uma resposta?
Shuvo Sarker

14
@ShuvoSarker porque, como demonstraram milhares de linguagens, não há explicação técnica para o C ++ fazer programadores escreverem assinaturas de função duas vezes. A resposta para "por quê?" é "história".
Boris

15

Porque as pessoas que criaram o formato da biblioteca não queriam "desperdiçar" espaço para informações raramente usadas, como macros do pré-processador C e declarações de função.

Como você precisa dessas informações para informar ao seu compilador "esta função estará disponível mais tarde quando o vinculador estiver executando seu trabalho", eles precisaram criar um segundo arquivo onde essas informações compartilhadas poderiam ser armazenadas.

A maioria das linguagens após o C / C ++ armazena essas informações na saída (Java bytecode, por exemplo) ou elas não usam um formato pré-compilado, sempre são distribuídas no formato de origem e compilam em tempo real (Python, Perl).


Não funcionaria, referências cíclicas. Você não pode criar a.lib a partir de a.cpp antes de criar b.lib a partir de b.cpp, mas você também não pode criar b.lib antes de a.lib.
MSalters 3/08/08

20
Java resolveu que, Python pode fazê-lo, qualquer linguagem moderna pode fazê-lo. Mas no momento em que o C foi inventado, a RAM era tão cara e escassa que simplesmente não era uma opção.
Aaron Digulla 3/08/08

6

É a maneira pré-processador de declarar interfaces. Você coloca a interface (declarações de método) no arquivo de cabeçalho e a implementação no cpp. Os aplicativos que usam sua biblioteca precisam apenas conhecer a interface, que eles podem acessar através de #include.


4

Frequentemente, você desejará ter uma definição de interface sem precisar enviar o código inteiro. Por exemplo, se você tiver uma biblioteca compartilhada, enviaria um arquivo de cabeçalho com ele, que define todas as funções e símbolos usados ​​na biblioteca compartilhada. Sem os arquivos de cabeçalho, você precisaria enviar a fonte.

Dentro de um único projeto, os arquivos de cabeçalho são usados, IMHO, para pelo menos dois propósitos:

  • Clareza, ou seja, mantendo as interfaces separadas da implementação, é mais fácil ler o código
  • Tempo de compilação. Usando apenas a interface, sempre que possível, em vez da implementação completa, o tempo de compilação pode ser reduzido, pois o compilador pode simplesmente fazer uma referência à interface em vez de precisar analisar o código real (o que, idealmente, só precisa ser feito uma única vez).

3
Por que os fornecedores de bibliotecas não puderam enviar apenas um arquivo de "cabeçalho" gerado? Um arquivo de "cabeçalho" livre do pré-processador deve oferecer um desempenho muito melhor (a menos que a implementação tenha sido realmente interrompida).
Tom Hawtin - defina 2/08/08

Eu acho que é irrelevante se o arquivo de cabeçalho é gerado ou escrito à mão, a pergunta não era "por que as pessoas escrevem os arquivos de cabeçalho?", Era "por que temos arquivos de cabeçalho". O mesmo vale para cabeçalhos sem pré-processador. Claro, isso seria mais rápido.

-5

Respondendo à resposta de MadKeithV ,

Isso reduz as dependências para que o código que usa o cabeçalho não precise necessariamente conhecer todos os detalhes da implementação e quaisquer outras classes / cabeçalhos necessários apenas para isso. Isso reduzirá o tempo de compilação e também a quantidade de recompilação necessária quando algo na implementação for alterado.

Outro motivo é que um cabeçalho fornece um ID exclusivo para cada classe.

Então, se tivermos algo como

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

Teremos erros, quando tentarmos construir o projeto, já que A faz parte de B, com cabeçalhos evitaríamos esse tipo de dor de cabeça ...

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.