Por que usar #ifndef CLASS_H e #define CLASS_H no arquivo .h, mas não no .cpp?

136

Eu sempre vi pessoas escreverem

class.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

A questão é: por que eles também não fazem isso para o arquivo .cpp que contém definições para funções de classe?

Digamos que sim main.cppe main.cppinclua class.h. O class.harquivo não faz includenada, então como main.cppsaber o que há no arquivo class.cpp?

user385261
fonte
5
"importação" provavelmente não é a palavra que você deseja usar aqui. Incluir.
22710 Kate
5
No C ++, não há correlação 1 para 1 entre arquivos e classes. Você pode colocar quantas classes quiser em um arquivo (ou até dividir uma classe em vários arquivos, embora isso raramente seja útil). Portanto, a macro deve ser FILE_H, não CLASS_H.
SBI

Respostas:

302

Primeiro, para responder à sua primeira consulta:

Quando você vê isso no arquivo .h :

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif

Essa é uma técnica de pré-processador para impedir que um arquivo de cabeçalho seja incluído várias vezes, o que pode ser problemático por vários motivos. Durante a compilação do seu projeto, cada arquivo .cpp (geralmente) é compilado. Em termos simples, isso significa que o compilador pegará seu arquivo .cpp , abrirá todos os arquivos #included, concatenará todos eles em um arquivo de texto maciço, executará uma análise de sintaxe e, finalmente, o converterá em código intermediário, otimizará / executará outros tarefas e, finalmente, gere a saída do assembly para a arquitetura de destino. Por esse motivo, se um arquivo estiver #includedvárias vezes sob um .cpparquivo, o compilador anexará o conteúdo do arquivo duas vezes; portanto, se houver definições nesse arquivo, você receberá um erro do compilador informando que redefiniu uma variável. Quando o arquivo é processado pela etapa do pré-processador no processo de compilação, na primeira vez em que seu conteúdo é atingido, as duas primeiras linhas verificam se FILE_Hfoi definido para o pré-processador. Caso contrário, ele definirá a diretiva. Na próxima vez que o conteúdo do arquivo for visto pelo pré-processador, a verificação será falsa e, portanto, será varrida imediatamente para o diretórioFILE_H e continuará processando o código entre ele e a e continuado depois dele. Isso evita erros de redefinição.#endifFILE_H#endif

E para resolver sua segunda preocupação:

Na programação C ++, como prática geral, separamos o desenvolvimento em dois tipos de arquivo. Uma é com uma extensão .he chamamos isso de "arquivo de cabeçalho". Eles geralmente fornecem uma declaração de funções, classes, estruturas, variáveis ​​globais, typedefs, macros e definições de pré-processamento, etc. Basicamente, eles apenas fornecem informações sobre seu código. Então temos a extensão .cpp que chamamos de "arquivo de código". Isso fornecerá definições para essas funções, membros da classe, quaisquer membros da estrutura que precisem de definições, variáveis ​​globais, etc. Portanto, o arquivo .h declara código e o .cpp arquivo implementa essa declaração. Por esse motivo, geralmente durante a compilação compilamos cada .cpparquivo em um objeto e, em seguida, vincule esses objetos (porque você quase nunca vê um arquivo .cpp incluir outro arquivo .cpp ).

Como esses externos são resolvidos é um trabalho para o vinculador. Quando seu compilador processa main.cpp , ele obtém declarações para o código em class.cpp incluindo class.h . Ele só precisa saber como são essas funções ou variáveis ​​(que é o que uma declaração fornece). Portanto, ele compila seu arquivo main.cpp em algum arquivo de objeto (chame-o de main.obj ). Da mesma forma, class.cpp é compilado em um class.objArquivo. Para produzir o executável final, um vinculador é chamado para vincular esses dois arquivos de objeto. Para quaisquer variáveis ​​ou funções externas não resolvidas, o compilador colocará um esboço onde o acesso acontece. O vinculador pegará esse stub e procurará o código ou a variável em outro arquivo de objeto listado e, se encontrado, combina o código dos dois arquivos de objeto em um arquivo de saída e substitui o stub pelo local final da função ou variável. Dessa forma, seu código em main.cpp pode chamar funções e usar variáveis ​​em class.cpp SE E SOMENTE SE ELES SÃO DECLARADOS EM class.h .

Eu espero que isso tenha sido útil.

Justin Summerlin
fonte
Eu estava tentando entender. He. Cpp nos últimos dias. Esta resposta economizou meu tempo e interesse em aprender C ++. Bem escrito. Obrigado Justin!
Rajkumar R
você realmente explicou muito bem! Talvez a resposta seria muito bom se fosse com imagens
alamin
13

O CLASS_Hé um guarda de inclusão ; é usado para evitar que o mesmo arquivo de cabeçalho seja incluído várias vezes (por rotas diferentes) no mesmo arquivo CPP (ou, mais precisamente, na mesma unidade de tradução ), o que levaria a erros de várias definições.

As proteções de inclusão não são necessárias nos arquivos CPP porque, por definição, o conteúdo do arquivo CPP é lido apenas uma vez.

Você parece ter interpretado os protetores de inclusão como tendo a mesma função que as importinstruções em outras linguagens (como Java); esse não é o caso, no entanto. O #includepróprio é aproximadamente equivalente ao de importoutras línguas.

Martin B
fonte
2
"dentro do mesmo arquivo CPP" deve ler "dentro da mesma unidade de tradução".
dreamlax
@dreamlax: Bom ponto - era o que eu ia escrever originalmente, mas então achei que alguém que não entende incluir guardas só ficará confuso com o termo "unidade de tradução". Vou editar a resposta para adicionar "unidade de tradução" entre parênteses - esse deve ser o melhor dos dois mundos.
Martin B
6

Não - pelo menos durante a fase de compilação.

A tradução de um programa c ++ do código fonte para o código da máquina é realizada em três fases:

  1. Pré - processamento - O pré-processador analisa todo o código-fonte em busca de linhas que começam com # e executa as diretivas. No seu caso, o conteúdo do seu arquivo class.hé inserido no lugar da linha #include "class.h. Como você pode incluir seu arquivo de cabeçalho em vários locais, as #ifndefcláusulas evitam erros de declaração duplicados, pois a diretiva de pré-processador é indefinida apenas na primeira vez em que o arquivo de cabeçalho é incluído.
  2. Compilação - O Compilador agora converte todos os arquivos de código-fonte pré-processados ​​em arquivos de objetos binários.
  3. Vinculação - o vinculador vincula (daí o nome) os arquivos do objeto. Uma referência à sua classe ou a um de seus métodos (que deve ser declarada em class.he definido em class.cpp) é resolvida para o respectivo deslocamento em um dos arquivos de objeto. Eu escrevo 'um dos seus arquivos de objeto', já que sua classe não precisa ser definida em um arquivo chamado class.cpp, pode estar em uma biblioteca vinculada ao seu projeto.

Em resumo, as declarações podem ser compartilhadas através de um arquivo de cabeçalho, enquanto o mapeamento de declarações para definições é feito pelo vinculador.

sum1stolemyname
fonte
4

Essa é a distinção entre declaração e definição. Os arquivos de cabeçalho geralmente incluem apenas a declaração e o arquivo de origem contém a definição.

Para usar algo, você só precisa saber que é declaração, não é definição. Somente o vinculador precisa conhecer a definição.

Portanto, é por isso que você incluirá um arquivo de cabeçalho em um ou mais arquivos de origem, mas não incluirá um arquivo de origem em outro.

Além disso, você quer dizer #includee não importar.

Brian R. Bondy
fonte
3

Isso é feito para os arquivos de cabeçalho, para que o conteúdo apareça apenas uma vez em cada arquivo de origem pré-processado, mesmo se incluído mais de uma vez (geralmente porque está incluído em outros arquivos de cabeçalho). Na primeira vez em que é incluído, o símbolo CLASS_H(conhecido como guarda de inclusão ) ainda não foi definido, portanto todo o conteúdo do arquivo está incluído. Isso define o símbolo; portanto, se ele for incluído novamente, o conteúdo do arquivo (dentro do #ifndef/#endif bloco ) será ignorado.

Não é necessário fazer isso para o próprio arquivo de origem, já que (normalmente) não está incluído em nenhum outro arquivo.

Para sua última pergunta, class.hdeve conter a definição da classe e declarações de todos os seus membros, funções associadas e qualquer outra coisa, para que qualquer arquivo que o inclua tenha informações suficientes para usar a classe. As implementações das funções podem ir em um arquivo de origem separado; você só precisa das declarações para chamá-las.

Mike Seymour
fonte
2

O main.cpp não precisa saber o que há no class.cpp . Ele apenas precisa conhecer as declarações das funções / classes que vai usar, e essas declarações estão em class.h .

O vinculador vincula entre os locais onde as funções / classes declaradas em class.h são usadas e suas implementações em class.cpp

Igor Oks
fonte
1

.cpparquivos não são incluídos (usando #include) em outros arquivos. Portanto, eles não precisam incluir proteção. Main.cppsaberá os nomes e assinaturas da classe na qual você implementou class.cppapenas porque você especificou tudo isso class.h- esse é o objetivo de um arquivo de cabeçalho. (Cabe a você garantir que class.hdescreva com precisão o código no qual você implementa class.cpp.) O código executável class.cppserá disponibilizado para o código executável main.cppgraças aos esforços do vinculador.

Kate Gregory
fonte
1

Geralmente, é esperado que módulos de código, como .cpparquivos, sejam compilados uma vez e vinculados a vários projetos, para evitar a compilação repetitiva desnecessária da lógica. Por exemplo, você g++ -o class.cppproduziria o class.oqual você poderia vincular de vários projetos ao usog++ main.cpp class.o .

Nós poderíamos usar #include como nosso vinculador, como você parece sugerir, mas isso seria tolo quando soubermos vincular adequadamente o uso do compilador com menos pressionamentos de teclas e menos repetições desnecessárias da compilação, em vez de nosso código com mais pressionamentos de teclas e mais desperdícios repetição de compilação ...

Os arquivos de cabeçalho ainda precisam ser incluídos em cada um dos vários projetos, no entanto, porque isso fornece a interface para cada módulo. Sem esses cabeçalhos, o compilador não saberia sobre nenhum dos símbolos introduzidos pelos .oarquivos.

É importante perceber que os arquivos de cabeçalho são o que introduz as definições de símbolos para esses módulos; Uma vez que isso é realizado, faz sentido que várias inclusões possam causar redefinições de símbolos (o que causa erros); portanto, usamos protetores para impedir essas redefinições.

autista
fonte
0

por causa dos Headerfiles, define o que a classe contém (Membros, estruturas de dados) e os arquivos cpp a implementam.

E, é claro, a principal razão para isso é que você pode incluir um arquivo .h várias vezes em outros arquivos .h, mas isso resultaria em várias definições de uma classe, o que é inválido.

Quonux
fonte