Por que não devo include arquivos cpp e, em vez disso, usar um header?

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

Olhando para o meu código, optei por não criar arquivos de header para minhas classs, mas fiz tudo nos arquivos cpp (ele parecia funcionar bem sem arquivos de header …). Eu estou supondo que o aluno significou que eu escrevi ‘#include’ mycppfile.cpp “; ‘ em alguns dos meus arquivos.

Meu raciocínio para #include os arquivos cpp foi: – Tudo o que deveria entrar no arquivo de header estava no meu arquivo cpp, então eu fingi que era como um arquivo de header – No macaco-ver-macaco fazer moda, eu vi que outros arquivos de header foram #include ‘d nos arquivos, então eu fiz o mesmo para o meu arquivo cpp.

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

    Tanto quanto sei, o padrão C ++ não sabe a diferença entre os arquivos de header e os arquivos de código-fonte. No que diz respeito à linguagem, qualquer ficheiro de texto com código legal é o mesmo que qualquer outro. No entanto, embora não seja ilegal, include arquivos de origem em seu programa eliminará praticamente todas as vantagens que você teria de separar seus arquivos de origem em primeiro lugar.

    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 ponha as mãos nele. Então, quando você inclui todos os arquivos de origem em seu projeto juntos, não há fundamentalmente nenhuma diferença entre o que você fez e apenas criar um arquivo de origem enorme sem qualquer separação.

    “Oh, isso não é grande coisa. Se correr, tudo bem”, eu ouço você chorar. E de certo modo, você estaria correto. Mas agora você está lidando com um minúsculo pequeno programa e uma CPU agradável e relativamente livre para compilá-lo para você. Você não será sempre tão sortudo.

    Se você mergulhar nos domínios da programação séria de computadores, verá projetos com contagens de linha que podem atingir milhões, em vez de dezenas. Isso é um monte de linhas. E se você tentar compilar um desses em um computador desktop moderno, pode levar 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 só importa na primeira vez – depois de compilado 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 unidas em um monstro gigante e precisasse fazer uma correção de bug simples como, digamos, x = y + 1 , isso significa que você tem que compilar todos os dois milhões de linhas novamente para teste isso. E se você descobrir que pretendia fazer um x = y - 1 vez disso, novamente, dois milhões de linhas de compilation 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 houvesse alguma maneira de compilar partes distintas da minha base de código individualmente, e de alguma forma ligá- 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 um monte de minúsculos arquivos .exe.

    “Mas certamente deve ser possível! A programação soa como pura tortura de outra forma! E se eu encontrasse uma maneira de separar a interface da implementação ? Digesse apenas informações suficientes desses segmentos de código distintos para identificá-los para o resto do programa, e colocando -los em algum tipo de arquivo de header em vez disso? E assim, posso usar a diretiva de pré-processador #include para trazer apenas as informações necessárias para compilar!

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

    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 tradução . Por convenção, os arquivos de header contêm declarações de function, definições de tipo e definições de class. As implementações de function reais residem em unidades de tradução, ou seja, arquivos .cpp.

    A idéia por trás disso é que funções e funções de membros de class / struct são compiladas e montadas uma vez, então outras funções podem chamar esse código de um lugar sem fazer duplicatas. Seus protótipos de function são declarados como “externos” implicitamente.

     /* Function prototype, usually found in headers. */ /* Implicitly 'extern', ie the symbols 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ê quiser que uma function seja local para uma unidade de tradução, defina-a como ‘estática’. O que isto significa? Isso significa que, se você include arquivos de origem com funções externas, receberá erros de redefinição, porque o compilador se depara com a mesma implementação mais de uma vez. Então, você quer que todas as suas unidades de tradução vejam o protótipo da function, mas não o corpo da function .

    Então, como tudo isso se mistura no final? Esse é o trabalho do vinculador. Um linker lê todos os arquivos de object gerados pelo estágio 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 uma function. Quando unidades de tradução que chamam funções ou declaram tipos não conhecem a implementação para essas funções ou tipos, esses símbolos são considerados não resolvidos. O vinculador resolve o símbolo não resolvido conectando a unidade de tradução que contém o símbolo indefinido junto com aquele que contém a implementação. Ufa Isso vale para todos os símbolos visíveis externamente, sejam eles implementados em 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 pequena function, poderá torná-la inline. Isso significa que o código de máquina gerado não gera uma chamada de function externa, mas é literalmente concatenada no local. Como eles geralmente são pequenos, o tamanho da sobrecarga não importa. Você pode imaginá-los como estáticos no modo como funcionam. Por isso, é seguro implementar funções embutidas nos headers. Implementações de function dentro de uma definição de class ou struct também são freqüentemente embutidas automaticamente pelo compilador.

    A outra exceção é modelos. Como o compilador precisa ver toda a definição de tipo de modelo ao instanciá-los, não é possível dissociar a implementação da definição como com funções independentes ou classs normais. Bem, talvez isso seja possível agora, mas ter um amplo suporte a compiladores para a palavra-chave “exportar” levou 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 de modelos instanciados, semelhantes a como as funções inline funcionam. Com suporte para ‘exportação’, este não é o caso.

    Para as duas exceções, algumas pessoas acham mais “interessante” colocar as implementações de funções embutidas, funções modeladas e tipos modelados em arquivos .cpp, e depois #include o arquivo .cpp. Se isso é um header ou um arquivo de origem, realmente não importa; o pré-processador não se importa e é apenas uma convenção.

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

    • O préprocessador é executado, o que analisa todas as diretivas que começam com um ‘#’. A diretiva #include concatena o arquivo incluído com o inferior, por exemplo. Ele também faz a substituição de macro e a colagem de tokens.
    • O compilador real é executado no arquivo de texto intermediário após o estágio de pré-processador e emite código assembler.
    • O montador é executado no arquivo de assembly e emite código de máquina, isso geralmente é chamado de arquivo de object e segue o formato binário executável do sistema operacional em questão. Por exemplo, o Windows usa o PE (formato executável portátil), enquanto o Linux usa o formato ELF do Unix System V, com extensões GNU. Nesse estágio, os símbolos ainda sã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 objects gerados que foram gerados pelo montador. O linker resolve símbolos e faz muita mágica, como criar seções e segmentos, o 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.

    Mais uma vez, isso foi definitivamente mais do que você pediu, mas espero que os detalhes minuciosos ajudem você a ver a foto maior.

    A solução típica é usar arquivos .h somente para declarações e arquivos .cpp para implementação. Se você precisar reutilizar a implementação, inclua o arquivo .h correspondente no arquivo .cpp que a class / function / o que é necessário e o link .obj usados ​​em um arquivo .cpp já compilado (um arquivo .obj – normalmente usado em um projeto – ou arquivo .lib – geralmente usado para reutilização de vários projetos). Dessa forma, você não precisará recompilar tudo se apenas a implementação for alterada.

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

    Os arquivos cpp podem ser compilados antes do tempo. Isso não funciona em você #inclui-los, já que precisa “include” o código em seu programa toda vez que ele é compilado. Se você include apenas o header, ele poderá usar apenas o arquivo de header 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 odiá-lo porque os tempos de compilation vão explodir.

    Também leia isto: Padrões de Inclusão de Arquivo de Cabeçalho

    Os arquivos de header geralmente contêm declarações de funções / classs, enquanto os arquivos .cpp contêm as implementações reais. No tempo de compilation, cada arquivo .cpp é compilado em um arquivo de object (geralmente extensão .o) e o vinculador combina os vários arquivos de object no executável final. O processo de binding é geralmente muito mais rápido que a compilation.

    Benefícios desta separação: Se você estiver recompilando um dos arquivos .cpp em seu projeto, não precisará recompilar todos os outros. Você acabou de criar o novo arquivo de object para esse arquivo .cpp específico. O compilador não precisa examinar os outros arquivos .cpp. No entanto, se você quiser chamar funções em seu arquivo .cpp atual que foram implementadas nos outros arquivos .cpp, será necessário informar ao compilador quais argumentos eles usam; esse é o propósito de include os arquivos de header.

    Desvantagens: Ao compilar um determinado arquivo .cpp, o compilador não pode ‘ver’ o que está dentro dos outros arquivos .cpp. Por isso, não sabe como as funções 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 ainda (:

    A idéia básica de que os headers 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 recompilar o aplicativo inteiro quando modificar apenas um deles será muito lento. Ou quando as funções nos arquivos começarão dependendo umas das outras. Portanto, você deve separar as declarações de class em seus arquivos de header, deixar a implementação em 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 object resultantes a um programa.

    Se você #include 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 methods.

    A compilation demorará mais (o que se torna um problema em grandes projetos), se você fizer edições em arquivos cpp #included, o que forçará a recompilation de quaisquer arquivos #incluindo-os.

    Basta colocar suas declarações em arquivos de header e incluí-los (como eles realmente não geram código em si), e o vinculador irá ligar as declarações com o código cpp correspondente (que, então, só é compilado uma vez).

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

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

    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 string todas em uma class mystring, coloque a class decl para isso em um mystring.h compilando mystring.cpp para um arquivo .obj

    agora no seu programa principal (por exemplo main.cpp) você inclui header e 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 desde que o header diz o que ele pode fazer

    agora, se um amigo quiser usar sua class mystring, você lhe dará mystring.he 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 vinculá-los a ele.

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

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

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

    Se, com o tempo, o projeto evoluir e você perceber que o processo de criação está demorando muito, poderá refatorar seu código para usar vários arquivos de origem para construções incrementais mais rápidas.

    Várias das respostas discutem a separação da interface da implementação. No entanto, isso não é um recurso inerente de include arquivos, e é bastante comum para #include “header” arquivos que incorporam diretamente sua implementação (até mesmo a biblioteca padrão do C ++ faz isso em 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ê compila e vincula um programa, o compilador primeiro compila os arquivos cpp individuais e, em seguida, os vincula (conecta). Os headers nunca serão compilados, a menos que sejam incluídos em um arquivo cpp primeiro.

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

    Eu sugiro que você passe pelo Projeto de Software de Grande Escala C ++ de John Lakos . No colégio, geralmente escrevemos pequenos projetos nos quais não nos deparamos com esses problemas. O livro destaca a importância de separar interfaces e implementações.

    Arquivos de header geralmente têm interfaces que não devem ser alteradas com tanta frequência. Da mesma forma, uma olhada nos padrões, como o idioma do Construtor Virtual, ajudará você a entender melhor o conceito.

    Eu ainda estou aprendendo como você 🙂

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

    Digamos que você esteja escrevendo um livro. Se você colocar os capítulos em arquivos separados, só precisará imprimir um capítulo se tiver alterado o mesmo. Trabalhar em um capítulo não altera nenhum dos outros.

    Mas include os arquivos cpp é, do ponto de vista do compilador, como editar todos os capítulos do livro em um arquivo. Então, se você alterar, terá que imprimir todas as páginas do livro inteiro para imprimir seu capítulo revisado. Não há opção “imprimir páginas selecionadas” na geração de código de object.

    De volta 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 tem muito código, daí a necessidade de modularidade. Este tipo de base de código é surpreendentemente típico de sistemas do mundo real.