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 #include
os 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 #include
nos 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?
fonte
Respostas:
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
#include
faz é 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 testex = 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ê.
fonte
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.
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:
Novamente, isso foi definitivamente mais do que você pediu, mas espero que os detalhes minuciosos ajudem você a ver a imagem maior.
fonte
int add(int, int);
é uma declaração de função . A parte protótipo disso é justaint, 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.export
for templates foi removido do idioma em 2011. Nunca foi realmente suportado por compiladores.A solução típica é usar
.h
arquivos apenas para declarações e.cpp
arquivos para implementação. Se você precisar reutilizar a implementação, inclua o.h
arquivo correspondente no arquivo em.cpp
que a classe / função / o que for necessário é usado e vincule-o a um.cpp
arquivo já compilado (um.obj
arquivo - 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.fonte
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
fonte
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 (:
fonte
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.
fonte
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).
fonte
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.
fonte
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.
fonte
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".
fonte
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.
fonte
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ê :)
fonte
É 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 ...
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.
fonte