Pergunta

Assim que eu terminei meu primeiro C ++ atribuição de programação e recebi o meu grau. Mas de acordo com a classificação, perdi marcas para including cpp files instead of compiling and linking them. Eu não sou muito claro sobre o que isso significa.

Tomar um back olhada no meu código, eu escolhi não para criar arquivos de cabeçalho para minhas aulas, mas fez tudo nos arquivos CPP (parecia funcionar bem sem arquivos de cabeçalho ...). Eu estou supondo que o aluno significava que eu escrevi '#include 'mycppfile.cpp';' em alguns dos meus arquivos.

Meu raciocínio para #include'ing os arquivos CPP foi: - Tudo o que era suposto ir para o arquivo de cabeçalho estava no meu arquivo cpp, então eu fingia que era como um arquivo de cabeçalho -. Em macaco-ver-macaco faz moda, vi que outros arquivos de cabeçalho foram #include'd nos arquivos, então eu fiz o mesmo para meu arquivo cpp

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

Foi útil?

Solução

Para o melhor de meu conhecimento, o padrão C ++ não conhece diferença entre arquivos de cabeçalho e arquivos de origem. Na medida em que a linguagem está em causa, qualquer arquivo de texto com o código legal é o mesmo que qualquer outro. No entanto, embora não seja ilegal, incluindo arquivos de origem em seu programa irá praticamente eliminar quaisquer vantagens que você tem de separar os arquivos de origem em primeiro lugar.

Essencialmente, o que #include faz é dizer ao pré-processador para tirar todo o arquivo que você especificou, e copiá-lo em seu arquivo ativo antes do compilador recebe em suas mãos isto. Então, quando você incluir todos os arquivos de origem no seu projeto juntos, há fundamentalmente nenhuma diferença entre o que você fez, e apenas fazer um arquivo de origem enorme sem qualquer separação em tudo.

"Oh, isso não é grande coisa. Se ele é executado, ele está bem", eu ouvi-lo chorar. E num certo sentido, você estaria correto. Mas agora você está lidando com um pequeno programa minúsculo pouco, e uma agradável e CPU relativamente desimpedido para compilá-lo para você. Você não vai ser sempre a mesma sorte.

Se você sempre aprofundar os reinos de programação de computador sério, você poderá ver projectos com contagens de linha que podem chegar a milhões de pessoas, em vez de dezenas. Isso é um monte de linhas. E se você tentar compilar um deles em um computador desktop moderno, pode levar uma questão de horas em vez de segundos.

"Oh, não! Isso soa horrível! No entanto I pode evitar esse destino dire?!" Infelizmente, não há muito que você pode fazer sobre isso. Se demorar horas para compilação, leva horas para compilação. Mas isso só é realmente importante pela primeira vez -. Uma vez que você tenha compilado uma vez, não há nenhuma razão para compilá-lo novamente

A menos que você mudar alguma coisa.

Agora, se você tivesse dois milhões de linhas de código mesclados em um gigante gigante, e necessidade de fazer uma correção simples erro, como, por exemplo, x = y + 1, isso significa que você tem que compilar todos os dois milhões de linhas novamente para teste isto. E se você descobrir que você pretendia fazer um x = y - 1 em vez disso, em seguida, novamente, dois milhões de linhas de compilação estão esperando por você. Isso é muitas horas de tempo perdido que fazer poderia ser melhor gasto qualquer outra coisa.

"Mas eu odeio ser! Improdutiva Se ao menos houvesse alguma maneira de compilação partes distintas da minha base de código individualmente, e de alguma maneira link -los juntos depois!" Uma excelente idéia, na teoria. Mas e se as suas necessidades de programa para saber o que está acontecendo em um arquivo diferente? É impossível separar completamente sua base de código, a menos que você deseja executar um monte de minúsculos pequenos arquivos .exe vez.

"Mas, certamente, deve ser possível! Programação soa como pura tortura contrário! E se eu encontrei alguma maneira de separar interface da implementação ? Say tomando apenas as informações necessárias a partir destes códigos distintos segmentos para identificá-los para o resto do programa, e colocá-los em algum tipo de cabeçalho arquivo em vez disso? e dessa forma, eu posso usar o #include pré-processador diretiva para trazer apenas as informações necessárias para compilar! "

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

Outras dicas

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 um unidade de tradução . Por convenção, arquivos de cabeçalho conter 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 tradução, arquivos i.e .cpp.

A idéia por trás disso é que as funções e as funções de membro de classe / struct são compilados e montados uma vez, em seguida, outras funções podem chamar esse código de um lugar sem fazer duplicatas. Suas funções são declaradas como "externo" 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ê quer uma função para ser local para uma unidade de tradução, você defini-lo como 'estática'. O que isto significa? Isso significa que se você incluir arquivos de origem com funções extern, você receberá erros de redefinição, porque o compilador se depara com a mesma implementação mais de uma vez. Então, você quer todas as suas unidades de tradução para ver a declaração Função , mas não o corpo da função .

Então, como é que tudo se purê juntos no final? Esse é o trabalho do vinculador. Um vinculador lê todos os arquivos objeto que é gerado pela fase assembler e símbolos resolve. Como eu disse anteriormente, um símbolo é apenas um nome. Por exemplo, o nome de uma variável ou uma função. Quando as unidades de tradução que funções ou tipos de declaração de chamada não sei a implementação para essas funções ou tipos, esses símbolos são disse a ser resolvido. O ligante resolve o símbolo não resolvido através da ligação da unidade de tradução que contém o símbolo indefinido em conjunto com o que contém a implantação. Ufa. Isto é verdade para todos os símbolos visíveis externamente, se eles são implementados no seu código, ou fornecidos por uma biblioteca adicional. Uma biblioteca é realmente apenas um arquivo com código reutilizável.

Há duas exceções notáveis. Primeiro, se você tem uma pequena função, você pode fazê-lo em linha. Isto significa que o código de máquina gerado não gera uma chamada de função externa, mas está literalmente concatenado no local. Uma vez que eles geralmente são pequenos, o tamanho sobrecarga não importa. Você pode imaginá-los a ser estático na forma como eles funcionam. Por isso é seguro para implementar funções embutidas nos cabeçalhos. implementações de função dentro de uma classe ou definição de struct são também muitas vezes embutido automaticamente pelo compilador.

A outra exceção é modelos. Desde o compilador precisa ver toda a definição do tipo de modelo ao instanciar-los, não é possível dissociar a implementação da definição como com funções independentes ou classes normais. Bem, talvez isso é possível agora, mas recebendo suporte do compilador generalizada para a "exportação" palavra-chave levou um longo, longo tempo. Assim, sem suporte para 'exportar', unidades de tradução obter suas próprias cópias locais de tipos templated instanciado e funções, semelhante à forma como linha funções de trabalho. Com suporte para 'exportar', este não é o caso.

Para as duas exceções, algumas pessoas acham que é "agradável" para colocar as implementações de funções embutidas, funções templated e tipos templated em arquivos .cpp, em seguida, incluir o arquivo .cpp. Se isto é um cabeçalho ou um arquivo de origem não importa realmente; o pré-processador não se preocupa e é apenas uma convenção.

Um breve resumo de todo o processo a partir do código C ++ (vários arquivos) e uma final executável:

  • O pré-processador é executado, que analisa todas as directivas que começa com um '#'. A directiva #include concatena o arquivo incluído no inferior, por exemplo. Ele também faz macro-substituição e token-colar.
  • O real compilador é executado no arquivo de texto intermediária após a fase de pré-processador, e emite assembler código.
  • O assembler é executado no código de máquina arquivo de montagem e emite, isso geralmente é chamado de objearquivo ct e segue o formato binário executável do sistema operativo em questão. Por exemplo, o Windows usa o PE (formato executável portátil), enquanto o Linux utiliza o formato Unix System V ELF, com extensões GNU. Nesta fase, símbolos ainda são marcados como indefinida.
  • Finalmente, o vinculador é executado. Todas as fases anteriores foram corridas em cada unidade de tradução a fim. No entanto, a fase vinculador funciona em todos os arquivos objeto gerados que foram gerados pela montadora. O vinculador resolve símbolos e faz um monte de magia como a criação de seções e segmentos, que é dependente da plataforma de destino e formato binário. Os programadores não são obrigados a conhecer esta em geral, mas certamente ajuda em alguns casos.

Novamente, isso foi definitivamente mais do que você pediu, mas espero que os detalhes essenciais ajuda a ver a foto maior.

A solução típica é a utilização de arquivos .h para declarações apenas e arquivos .cpp para implementação. Se você precisar de reutilizar a implementação você incluir o arquivo .h correspondente no arquivo .cpp onde a classe / função necessária / whatever é usado e ligação contra um arquivo .cpp já compilado (um arquivo .obj - geralmente utilizado dentro de um projeto - ou .lib arquivo - geralmente utilizado para a reutilização de vários projetos). Desta forma, você não precisa de tudo recompilação se apenas as alterações de implementação.

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

Os arquivos CPP pode ser compilado antes do tempo. Isso não funciona em você # include-los, como ele precisa real "incluir" o código em seu programa cada vez que compila-lo. Se você acabou de incluir o cabeçalho, ele pode apenas usar o arquivo de cabeçalho para determinar como usar o arquivo cpp pré-compilada.

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

Além disso ter uma leitura deste: Arquivo Header Incluir padrões

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

Os benefícios dessa separação: Se você estiver recompilando um dos arquivos .cpp em seu projeto, você não tem que recompilar todos os outros. Você acabou de criar o novo arquivo objeto para esse arquivo .cpp particular. O compilador não tem que olhar para os outros arquivos .cpp. No entanto, se você quiser chamar funções em seu arquivo .cpp corrente que foram implementados em outros arquivos .cpp, você tem que dizer ao compilador que argumentos eles tomam; que é 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 outros arquivos .cpp. Por isso, não sei como as funções não são implementadas, e como resultado não pode otimizar de forma tão agressiva. Mas eu acho que você não precisa se preocupar com isso agora (:

A idéia básica que os cabeçalhos só são incluídos e arquivos cpp só são compilados. Isso vai se tornar mais útil quando você tem muitos arquivos CPP, e recompilar o aplicativo inteiro quando você modifica apenas um deles será muito lento. Ou quando as funções nos arquivos começará dependendo uns dos outros. Assim, você deve separar declarações de classe em seus arquivos de cabeçalho, implementação licença em arquivos cpp e escrever um Makefile (ou qualquer outra coisa, dependendo do que ferramentas que você está usando) para compilar os arquivos cpp e vincular os arquivos objeto resultante em um programa.

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

A compilação vai demorar mais tempo (que se torna um problema em grandes projetos), se você fazer edições em arquivos CPP #included, que, em seguida, forçar recompilação de quaisquer arquivos #including-los.

Basta colocar suas declarações em arquivos de cabeçalho e incluem aqueles (como eles realmente não gerar o código em si), e o vinculador irá ligar as declarações com o código CPP correspondente (que só então é compilado uma vez).

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

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

re-usabilidade, arquitetura e dados encapsulamento

aqui está um exemplo:

dizer que você criar um arquivo cpp que contém um formulário simples de rotinas de cordas tudo em um mystring classe, você coloca a decl classe para isso em um MyString.h compilar MyString.cpp para um arquivo obj

Agora, em seu programa principal (por exemplo main.cpp) você incluir cabeçalho e ligação com o mystring.obj. para uso mystring em seu programa que você não se preocupam com os detalhes como mystring é implementado desde o cabeçalho diz o que pode fazer

Agora, se um amigo quer usar sua classe mystring você dar-lhe MyString.h eo mystring.obj, ele também não precisa necessariamente saber como ele funciona, desde que ele funciona.

mais tarde, se você tem mais tais ficheiros.OBJ você pode combiná-los em um arquivo .lib e link para que, em vez.

Você também pode decidir alterar o arquivo MyString.cpp e implementá-lo de forma mais eficaz, isso não irá afectar a sua main.cpp ou o seu programa de amigos.

Se ele funciona para você, então não há nada de errado com ele -., Exceto que ele vai irritar as penas das pessoas que pensam que só há uma maneira de fazer as coisas

Muitas das respostas dadas aqui otimizações de endereço para projetos de software em grande escala. Estas são coisas boas para saber sobre, mas não há nenhum ponto na otimização de um projeto pequeno como se fosse um grande projeto - que é o que é conhecido como "otimização prematura". Dependendo do seu ambiente de desenvolvimento, pode haver complexidade extra significativo envolvido na criação de uma configuração de compilação para suportar múltiplos 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 pode refactor seu código para usar vários arquivos de origem para compilações mais rápido incremental.

Várias das respostas discutem separar a interface da implementação. No entanto, esta não é uma característica inerente a incluir arquivos, e é bastante comum #include arquivos "header" que incorporam diretamente a sua implementação (até mesmo a biblioteca padrão C ++ faz isso para um grau significativo).

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

Quando você compilar e vincular um programa do primeiro compilador compila os arquivos CPP individuais e, em seguida, eles link (conexão)-los. Os cabeçalhos nunca será compilado, a não ser incluídos em um arquivo cpp em primeiro lugar.

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 você deixar de fora como você realmente implementar os detalhes. Desta forma, você não tem que recompilar cada arquivo cpp, se você fizer uma alteração em um.

Vou sugerir que você passar por Large Scale C ++ Software Design por John Lakos . Na faculdade, que normalmente escrever pequenos projetos em que não se deparar com tais problemas. O livro destaca a importância de separar interfaces e implementações.

Os arquivos de cabeçalho geralmente têm interfaces que são supostamente para não ser mudado com tanta freqüência. Da mesma forma uma olhada em padrões como idioma Construtor virtual irá ajudá-lo a entender melhor o conceito.

Eu ainda estou aprendendo como você:)

É como escrever um livro, você quer imprimir capítulos acabados somente uma vez

Digamos que você está escrevendo um livro. Se você colocar os capítulos em arquivos separados, então você só precisa imprimir um capítulo se tenha alterado. Trabalhando em um capítulo não alterar qualquer um dos outros.

Mas incluindo os arquivos CPP é, do ponto de vista do compilador, como a edição de todos os capítulos do livro em um arquivo. Então, se você alterá-lo você tem que imprimir todas as páginas de todo o livro, a fim de obter o seu capítulo revisto impresso. Não há opção "imprimir páginas selecionadas" na geração de código objeto.

Voltar ao software: Eu tenho Linux e Ruby src em torno de mentir. A 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 tem um monte de código, daí a necessidade de modularidade. Este tipo de base de código é surpreendentemente típico de sistemas do mundo real.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top