Por que não devo incluir arquivos cpp e usar um cabeçalho?


147

Então terminei minha primeira tarefa de programação em C ++ e recebi minha nota. Mas de acordo com a classificação, eu perdi notas para including cpp files instead of compiling and linking them. Não sou muito claro sobre o que isso significa.

Revendo meu código, optei por não criar arquivos de cabeçalho para minhas classes, mas fiz tudo nos arquivos cpp (parecia funcionar bem sem os arquivos de cabeçalho ...). Suponho que o aluno da escola quis dizer que eu escrevi '#include "mycppfile.cpp";' em alguns dos meus arquivos.

Meu raciocínio sobre #includeos arquivos cpp foi: - Tudo o que deveria entrar no arquivo de cabeçalho estava no meu arquivo cpp, então eu fingi que era como um arquivo de cabeçalho - Na moda do macaco-ver-macaco, vi aquele outro os arquivos de cabeçalho estavam #includenos arquivos, então fiz o mesmo para o meu arquivo cpp.

Então, o que exatamente eu fiz de errado, e por que isso é ruim?


36
Esta é realmente uma boa pergunta. Espero que muitos iniciantes em c ++ sejam ajudados por isso.
Mia Clarke

Respostas:


174

Que eu saiba, o padrão C ++ não conhece diferença entre os arquivos de cabeçalho e os de origem. No que diz respeito ao idioma, qualquer arquivo de texto com código legal é o mesmo que qualquer outro. No entanto, embora não seja ilegal, a inclusão de arquivos de origem em seu programa eliminará praticamente todas as vantagens que você teria ao separar seus arquivos de origem.

Essencialmente, o que #includefaz é dizer ao pré - processador para pegar o arquivo inteiro que você especificou e copiá-lo para o seu arquivo ativo antes que o compilador coloque as mãos nele. Portanto, quando você inclui todos os arquivos de origem em seu projeto, basicamente não há diferença entre o que você fez e apenas criar um grande arquivo de origem sem nenhuma separação.

"Oh, isso não é grande coisa. Se funcionar, tudo bem", eu ouço você chorar. E, em certo sentido, você estaria correto. Mas agora você está lidando com um pequeno programa minúsculo e uma CPU agradável e relativamente livre para compilá-lo para você. Você nem sempre terá tanta sorte.

Se você se aprofundar nos domínios da programação de computadores, verá projetos com contagens de linhas que podem chegar a milhões, em vez de dezenas. São muitas falas. E se você tentar compilar um desses em um computador desktop moderno, poderá demorar algumas horas em vez de segundos.

"Oh não! Isso parece horrível! No entanto, posso evitar esse destino terrível ?!" Infelizmente, não há muito o que fazer sobre isso. Se levar horas para compilar, leva horas para compilar. Mas isso realmente importa da primeira vez - depois de compilá-lo uma vez, não há razão para compilá-lo novamente.

A menos que você mude alguma coisa.

Agora, se você tivesse dois milhões de linhas de código mescladas em um gigante gigante e precisasse corrigir um bug simples, como, por exemplo x = y + 1, isso significa que é necessário compilar todos os dois milhões de linhas novamente para testar isso. E se você descobrir que pretendia fazer um teste x = y - 1, então, novamente, dois milhões de linhas de compilação estão esperando por você. São muitas horas desperdiçadas que poderiam ser melhor gastas fazendo qualquer outra coisa.

"Mas eu odeio ser improdutivo! Se ao menos houvesse uma maneira de compilar partes distintas da minha base de código individualmente, e de alguma forma conectá- las depois!" Uma excelente ideia, em teoria. Mas e se o seu programa precisar saber o que está acontecendo em um arquivo diferente? É impossível separar completamente sua base de código, a menos que você queira executar vários arquivos .exe minúsculos.

"Mas certamente deve ser possível! Programar soa como pura tortura! E se eu encontrasse uma maneira de separar interface da implementação ? Diga pegando informações suficientes desses segmentos de código distintos para identificá-las no restante do programa e colocando em algum tipo de arquivo de cabeçalho ? E assim, eu posso usar a #include diretiva pré - processador para trazer apenas as informações necessárias para compilar! "

Hmm. Você pode estar procurando algo lá. Deixe-me saber como isso funciona para você.


13
Boa resposta, senhor. Foi uma leitura divertida e fácil de entender. Eu gostaria que meu livro fosse escrito assim.
Ialm 06/11/2009

@veol Search for Head Primeira série de livros - não sei se eles têm uma versão em C ++. headfirstlabs.com
Amarghosh 7/11/2009

2
Esta é (definitivamente) a melhor redação que ouvi até agora ou que contemplei. Justin Case, um iniciante talentoso, alcançou um projeto com um milhão de pressionamentos de tecla, ainda não enviado, e um louvável "primeiro projeto" que vê a luz do aplicativo em uma base real de usuários, reconheceu um problema solucionado pelos fechamentos. Soa notavelmente semelhante aos estados avançados da definição de problema original do OP menos o "codificado isso quase cem vezes e não pode imaginar o que fazer para nulo (como nenhum objeto) vs nulo (como sobrinho) sem usar a programação por exceções".
Nicholas Jordan

É claro que tudo isso se desfaz dos modelos, porque a maioria dos compiladores não suporta / implementa a palavra-chave 'export'.
KitsuneYMG

1
Outro ponto é que você tem muitas bibliotecas de última geração (se pensar no BOOST) que usam apenas classes de cabeçalhos ... Ei, espera? Por que o programador experiente não separa a interface da implementação? Parte da resposta pode ser o que Blindly disse, outra parte pode ser que um arquivo é melhor que dois quando é possível, e outra parte é que a vinculação tem um custo que pode ser bastante alto. Eu já vi programas rodando dez vezes mais rápido com a inclusão direta da fonte e do compilador otimizando. Porque vincular principalmente a otimização de blocos.
kriss

45

Esta é provavelmente uma resposta mais detalhada do que você queria, mas acho que uma explicação decente é justificada.

Em C e C ++, um arquivo de origem é definido como uma unidade de conversão . Por convenção, os arquivos de cabeçalho contêm declarações de função, definições de tipo e definições de classe. As implementações de funções reais residem em unidades de conversão, como arquivos .cpp.

A idéia por trás disso é que funções e funções de membro de classe / estrutura são compiladas e montadas uma vez; outras funções podem chamar esse código de um local sem criar duplicatas. Suas funções são declaradas como "externas" implicitamente.

/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);

/* function body, or function definition. */
int add(int a, int b) 
{
   return a + b;
}

Se você deseja que uma função seja local para uma unidade de conversão, defina-a como 'estática'. O que isto significa? Isso significa que, se você incluir arquivos de origem com funções externas, obterá erros de redefinição, porque o compilador encontra a mesma implementação mais de uma vez. Portanto, você deseja que todas as suas unidades de tradução vejam a declaração da função, mas não o corpo da função .

Então, como tudo se mistura no final? Esse é o trabalho do vinculador. Um vinculador lê todos os arquivos de objeto gerados pelo estágio do assembler e resolve os símbolos. Como eu disse anteriormente, um símbolo é apenas um nome. Por exemplo, o nome de uma variável ou função. Quando as unidades de conversão que chamam funções ou declaram tipos não sabem a implementação dessas funções ou tipos, diz-se que esses símbolos não foram resolvidos. O vinculador resolve o símbolo não resolvido conectando a unidade de conversão que mantém o símbolo indefinido junto com o que contém a implementação. Ufa. Isso é válido para todos os símbolos visíveis externamente, sejam eles implementados no seu código ou fornecidos por uma biblioteca adicional. Uma biblioteca é realmente apenas um arquivo com código reutilizável.

Existem duas exceções notáveis. Primeiro, se você tiver uma função pequena, poderá integrá-la. Isso significa que o código de máquina gerado não gera uma chamada de função externa, mas é literalmente concatenado no local. Como geralmente são pequenas, o tamanho da sobrecarga não importa. Você pode imaginar que eles sejam estáticos no modo como funcionam. Portanto, é seguro implementar funções embutidas nos cabeçalhos. As implementações de funções dentro de uma definição de classe ou estrutura também são frequentemente incorporadas automaticamente pelo compilador.

A outra exceção são os modelos. Como o compilador precisa ver toda a definição do tipo de modelo ao instancia-los, não é possível dissociar a implementação da definição como nas funções independentes ou nas classes normais. Bem, talvez isso seja possível agora, mas obter amplo suporte do compilador para a palavra-chave "export" levou muito, muito tempo. Portanto, sem suporte para 'exportação', as unidades de tradução obtêm suas próprias cópias locais de tipos e funções instanciados de modelo, semelhante à maneira como as funções embutidas funcionam. Com suporte para 'exportação', este não é o caso.

Para as duas exceções, algumas pessoas acham "melhor" colocar as implementações de funções embutidas, funções e tipos de modelo nos arquivos .cpp e #incluir o arquivo .cpp. Se isso é um cabeçalho ou um arquivo de origem, não importa; o pré-processador não se importa e é apenas uma convenção.

Um resumo rápido de todo o processo, do código C ++ (vários arquivos) e até o executável final:

  • O pré - processador é executado, que analisa todas as diretivas que começam com um '#'. A diretiva #include concatena o arquivo incluído com inferior, por exemplo. Também faz substituição de macro e colagem de token.
  • O compilador real é executado no arquivo de texto intermediário após o estágio do pré-processador e emite o código do assembler.
  • O montador é executado no arquivo de montagem e emite código de máquina, geralmente chamado de arquivo de objeto e segue o formato executável binário do sistema operacional em questão. Por exemplo, o Windows usa o PE (formato executável portátil), enquanto o Linux usa o formato Unix System V ELF, com extensões GNU. Nesta fase, os símbolos ainda estão marcados como indefinidos.
  • Finalmente, o vinculador é executado. Todos os estágios anteriores foram executados em cada unidade de tradução em ordem. No entanto, o estágio do vinculador funciona em todos os arquivos de objetos gerados que foram gerados pelo assembler. O vinculador resolve símbolos e faz muita mágica, como criar seções e segmentos, que depende da plataforma de destino e do formato binário. Os programadores não precisam saber disso em geral, mas certamente ajuda em alguns casos.

Novamente, isso foi definitivamente mais do que você pediu, mas espero que os detalhes minuciosos ajudem você a ver a imagem maior.


2
Obrigado por sua explicação completa. Admito que ainda não faz sentido para mim e acho que precisarei ler sua resposta novamente (e novamente).
Ialm

1
+1 para uma excelente explicação. Pena que provavelmente assustará todos os novatos em C ++. :)
goldPseudo

1
Heh, não se sinta mal. No Stack Overflow, a resposta mais longa raramente é a melhor.

int add(int, int);é uma declaração de função . A parte protótipo disso é justa int, int. No entanto, todas as funções em C ++ têm um protótipo; portanto, o termo realmente só faz sentido em C. Editei sua resposta para esse efeito.
precisa

exportfor templates foi removido do idioma em 2011. Nunca foi realmente suportado por compiladores.
precisa

10

A solução típica é usar .harquivos apenas para declarações e .cpparquivos para implementação. Se você precisar reutilizar a implementação, inclua o .harquivo correspondente no arquivo em .cppque a classe / função / o que for necessário é usado e vincule-o a um .cpparquivo já compilado (um .objarquivo - geralmente usado em um projeto - ou arquivo .lib - geralmente usado para reutilização de vários projetos). Dessa forma, você não precisa recompilar tudo se apenas a implementação mudar.


6

Pense nos arquivos cpp como uma caixa preta e nos arquivos .h como os guias sobre como usar essas caixas pretas.

Os arquivos cpp podem ser compilados com antecedência. Isso não funciona em você #inclui-os, pois ele precisa "incluir" o código no seu programa toda vez que ele o compila. Se você incluir apenas o cabeçalho, ele poderá usar o arquivo de cabeçalho para determinar como usar o arquivo cpp pré-compilado.

Embora isso não faça muita diferença para o seu primeiro projeto, se você começar a escrever grandes programas cpp, as pessoas vão odiar você porque os tempos de compilação vão explodir.

Leia também: Padrões de inclusão de arquivo de cabeçalho


Obrigado pelo exemplo mais concreto. Tentei ler o seu link, mas agora estou confuso ... qual é a diferença entre incluir explicitamente um cabeçalho e uma declaração direta?
Ialm 06/11/2009

Este é um ótimo artigo. Veol, aqui estão incluídos os cabeçalhos nos quais o compilador precisa de informações sobre o tamanho da classe. A declaração de encaminhamento é usada quando você está usando apenas ponteiros.
pankajt 7/11/2009

declaração direta: int someFunction (int neededValue); observe o uso de informações de tipo e (geralmente) sem chaves. Isso, como indicado, informa ao compilador que, em algum momento, você precisará de uma função que receba um int e retorne um int, o compilador pode reservar uma chamada para ele usando essas informações. Isso seria chamado de declaração direta. Compiladores mais sofisticados devem ser capazes de encontrar a função sem precisar disso, incluindo um cabeçalho pode ser uma maneira útil de declarar várias declarações avançadas.
Nicholas Jordan

6

Os arquivos de cabeçalho geralmente contêm declarações de funções / classes, enquanto os arquivos .cpp contêm as implementações reais. No momento da compilação, cada arquivo .cpp é compilado em um arquivo de objeto (geralmente extensão .o) e o vinculador combina os vários arquivos de objeto no executável final. O processo de vinculação geralmente é muito mais rápido que a compilação.

Benefícios dessa separação: se você estiver recompilando um dos arquivos .cpp no ​​seu projeto, não precisará recompilar todos os outros. Você acabou de criar o novo arquivo de objeto para esse arquivo .cpp específico. O compilador não precisa examinar os outros arquivos .cpp. No entanto, se você quiser chamar funções no seu arquivo .cpp atual que foram implementadas nos outros arquivos .cpp, será necessário informar ao compilador quais argumentos eles usam; esse é o objetivo de incluir os arquivos de cabeçalho.

Desvantagens: Ao compilar um determinado arquivo .cpp, o compilador não pode 'ver' o que está dentro dos outros arquivos .cpp. Portanto, ele não sabe como as funções são implementadas e, como resultado, não pode otimizar de maneira tão agressiva. Mas acho que você ainda não precisa se preocupar com isso (:


5

A idéia básica de que os cabeçalhos são incluídos apenas e os arquivos cpp são compilados apenas. Isso se tornará mais útil quando você tiver muitos arquivos cpp e a recompilação de todo o aplicativo quando você modificar apenas um deles será muito lento. Ou quando as funções nos arquivos começarão dependendo uma da outra. Portanto, você deve separar as declarações de classe nos arquivos de cabeçalho, deixar a implementação nos arquivos cpp e escrever um Makefile (ou qualquer outra coisa, dependendo de quais ferramentas você está usando) para compilar os arquivos cpp e vincular os arquivos de objeto resultantes a um programa.


3

Se você incluir um arquivo cpp em vários outros arquivos em seu programa, o compilador tentará compilar o arquivo cpp várias vezes e gerará um erro, pois haverá várias implementações dos mesmos métodos.

A compilação levará mais tempo (o que se torna um problema em grandes projetos), se você fizer edições em #included arquivos cpp, que forçarão a recompilação de qualquer arquivo #incluindo-os.

Basta colocar suas declarações em arquivos de cabeçalho e incluí-las (como elas não geram código por si só), e o vinculador conectará as declarações com o código cpp correspondente (que só será compilado uma vez).


Portanto, além de ter tempos de compilação mais longos, começarei a ter problemas quando # incluir meu arquivo cpp em vários arquivos diferentes que usam as funções nos arquivos cpp incluídos?
Ialm 06/11/2009

Sim, isso é chamado de colisão de namespace. De interesse aqui é se a vinculação a libs introduz problemas no espaço para nome. Em geral, acho que os compiladores produzem melhores tempos de compilação para o escopo da unidade de tradução (tudo em um arquivo), o que introduz problemas de espaço para nome - o que leva à separação novamente ... você pode incluir o arquivo de inclusão em cada unidade de tradução (suposto) existe até um pragma (#pragma uma vez) que deve impor isso, mas que é uma suposição de supositório. Cuidado para não confiar cegamente em bibliotecas (arquivos .O) de qualquer lugar, pois os links de 32 bits não são impostos.
Nicholas Jordan

2

Embora certamente seja possível fazer o que você fez, a prática padrão é colocar declarações compartilhadas nos arquivos de cabeçalho (.h) e definições de funções e variáveis ​​- implementação - nos arquivos de origem (.cpp).

Como convenção, isso ajuda a deixar claro onde está tudo e faz uma distinção clara entre interface e implementação de seus módulos. Isso também significa que você nunca precisa verificar se um arquivo .cpp está incluído em outro, antes de adicionar algo a ele que poderia quebrar se fosse definido em várias unidades diferentes.


2

reutilização, arquitetura e encapsulamento de dados

aqui está um exemplo:

digamos que você crie um arquivo cpp que contenha uma forma simples de rotinas de strings em uma classe mystring, coloque a classe decl para isso em um mystring.h compilando mystring.cpp em um arquivo .obj

agora no seu programa principal (por exemplo, main.cpp) você inclui o cabeçalho e o link com o mystring.obj. Para usar o mystring no seu programa, você não se importa com os detalhes de como o mystring é implementado, já que o cabeçalho diz o que ele pode fazer

Agora, se um amigo deseja usar sua classe mystring, você fornece a ele mystring.he o mystring.obj, ele também não precisa necessariamente saber como funciona, desde que funcione.

mais tarde, se você tiver mais desses arquivos .obj, poderá combiná-los em um arquivo .lib e vincular a ele.

você também pode decidir alterar o arquivo mystring.cpp e implementá-lo com mais eficiência, isso não afetará o seu main.cpp ou o programa de amigos.


2

Se funcionar para você, não há nada errado com isso - exceto que isso abalará as penas das pessoas que pensam que há apenas uma maneira de fazer as coisas.

Muitas das respostas fornecidas aqui abordam otimizações para projetos de software em larga escala. Essas são boas coisas para se saber, mas não há sentido em otimizar um projeto pequeno como se fosse um projeto grande - isso é conhecido como "otimização prematura". Dependendo do seu ambiente de desenvolvimento, pode haver uma complexidade extra significativa na definição de uma configuração de compilação para suportar vários arquivos de origem por programa.

Se, ao longo do tempo, suas evolui projeto e você achar que o processo de construção está demorando muito, então você pode refatorar seu código para usar vários arquivos de origem para compilações mais rápido incremental.

Várias das respostas discutem a separação da interface da implementação. No entanto, esse não é um recurso inerente aos arquivos de inclusão e é bastante comum #incluir arquivos de "cabeçalho" que incorporam diretamente sua implementação (mesmo a Biblioteca Padrão do C ++ faz isso em um grau significativo).

A única coisa realmente "não convencional" sobre o que você fez foi nomear os arquivos incluídos ".cpp" em vez de ".h" ou ".hpp".


1

Quando você compila e vincula um programa, o compilador primeiro compila os arquivos cpp individuais e depois os vincula (conecta). Os cabeçalhos nunca serão compilados, a menos que sejam incluídos em um arquivo cpp primeiro.

Normalmente, cabeçalhos são declarações e cpp são arquivos de implementação. Nos cabeçalhos, você define uma interface para uma classe ou função, mas deixa de fora como realmente implementa os detalhes. Dessa forma, você não precisa recompilar todos os arquivos cpp se fizer uma alteração em um.


se você deixar a implementação fora do arquivo de cabeçalho, desculpe-me, mas isso me parece uma interface Java, certo?
gansub

1

Vou sugerir que você faça um projeto de software C ++ em larga escala de John Lakos . Na faculdade, costumamos escrever pequenos projetos nos quais não encontramos esses problemas. O livro destaca a importância de separar interfaces e implementações.

Arquivos de cabeçalho geralmente possuem interfaces que não devem ser alteradas com tanta frequência. Da mesma forma, uma análise de padrões como o idioma do Virtual Constructor ajudará você a entender melhor o conceito.

Eu ainda estou aprendendo como você :)


Obrigado pela sugestão do livro. Eu não sei se vou chegar à fase de fazer escala C ++ grande programas embora ...
ialm

é divertido codificar programas em grande escala e para muitos um desafio. Estou começando a gostar dele :)
pankajt

1

É como escrever um livro, você deseja imprimir capítulos concluídos apenas uma vez

Diga que você está escrevendo um livro. Se você colocar os capítulos em arquivos separados, precisará imprimir um capítulo apenas se o tiver alterado. Trabalhar em um capítulo não muda nenhum dos outros.

Mas incluir os arquivos cpp é, do ponto de vista do compilador, como editar todos os capítulos do livro em um arquivo. Então, se você o alterar, será necessário imprimir todas as páginas do livro inteiro para imprimir o capítulo revisado. Não há opção "imprimir páginas selecionadas" na geração de código do objeto.

Voltar ao software: Eu tenho Linux e Ruby src por aí. Uma medida aproximada de linhas de código ...

     Linux       Ruby
   100,000    100,000   core functionality (just kernel/*, ruby top level dir)
10,000,000    200,000   everything 

Qualquer uma dessas quatro categorias possui muito código, daí a necessidade de modularidade. Esse tipo de base de código é surpreendentemente típico dos sistemas do mundo real.

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.