Por que você pode ter a definição de método dentro do arquivo de cabeçalho em C ++, quando em C não pode?

23

Em C, você não pode ter a definição / implementação de função dentro do arquivo de cabeçalho. No entanto, em C ++, você pode ter a implementação completa do método dentro do arquivo de cabeçalho. Por que o comportamento é diferente?

Joshua Partogi
fonte

Respostas:

28

Em C, se você definir uma função em um arquivo de cabeçalho, essa função aparecerá em cada módulo compilado que inclui esse arquivo de cabeçalho e um símbolo público será exportado para a função. Portanto, se a adição de função for definida no cabeçalho.h, e foo.c e bar.c incluirem o cabeçalho.h, então foo.o e bar.o incluirão cópias da adição.

Quando você vincula esses dois arquivos de objeto, o vinculador verá que o acréscimo de símbolo está definido mais de uma vez e não permitirá.

Se você declarar que a função é estática, nenhum símbolo será exportado. Os arquivos de objeto foo.o e bar.o ainda conterão cópias separadas do código para a função e poderão usá-los, mas o vinculador não poderá ver nenhuma cópia da função, portanto não vai reclamar. Obviamente, nenhum outro módulo poderá ver a função também. E seu programa ficará cheio de duas cópias idênticas da mesma função.

Se você declarar apenas a função no arquivo de cabeçalho, mas não a definir, e depois defini-la em apenas um módulo, o vinculador verá uma cópia da função e todos os módulos do seu programa poderão vê-lo e use-o. E seu programa compilado conterá apenas uma cópia da função.

Assim, você pode ter a definição de função no arquivo de cabeçalho em C, é apenas estilo ruim, forma incorreta e uma péssima idéia geral.

(Por "declarar", quero dizer fornecer um protótipo de função sem um corpo; por "definir" quero dizer fornecer o código real do corpo da função; essa é a terminologia C padrão.)

David Conrad
fonte
2
Não é uma idéia tão ruim - esse tipo de coisa pode ser encontrado até nos cabeçalhos do GNU Libc.
SK-logic
mas e o agrupamento idiomático do arquivo de cabeçalho em uma diretiva de compilação condicional? Então, mesmo com a função declarada AND definida no cabeçalho, ela será carregada apenas uma vez. Eu sou novo em C, então posso estar entendendo mal.
user305964
2
@papiro O problema é que esse invólucro protege apenas durante uma única execução do compilador. Portanto, se foo.c for compilado para foo.o em uma execução, bar.c para bar.o em outra e foo.o e bar.o estiverem vinculados a a.out em um terceiro (como é típico), esse empacotamento não impede várias instâncias, uma em cada arquivo de objeto.
David Conrad
O problema não descrito aqui #ifndef HEADER_Hé o que deveria impedir?
Robert Harvey
27

C e C ++ se comportam da mesma forma nesse aspecto - você pode ter inlinefunções nos cabeçalhos. No C ++, qualquer método cujo corpo esteja dentro da definição de classe é implicitamente inline. Se você deseja fazer o mesmo em C, declare as funções static inline.

Simon Richter
fonte
" declare as funçõesstatic inline " ... e você ainda terá várias cópias da função em cada unidade de tradução que a usa. No C ++ com não- static inlinefunção, você terá apenas uma cópia. Para realmente ter a implementação no cabeçalho em C, você deve 1) marcar a implementação como inline(por exemplo inline void func(){do_something();}) e 2) realmente dizer que essa função estará em alguma unidade de tradução específica (por exemplo void func();).
Ruslan
6

O conceito de um arquivo de cabeçalho precisa de uma pequena explicação:

Você fornece um arquivo na linha de comando do compilador ou faz um '#include'. A maioria dos compiladores aceita um arquivo de comando com a extensão c, C, cpp, c ++ etc. como arquivo de origem. No entanto, eles geralmente incluem uma opção de linha de comando para permitir o uso de qualquer extensão arbitrária em um arquivo de origem.

Geralmente, o arquivo fornecido na linha de comando é chamado de 'Origem' e o arquivo incluído é chamado de 'Cabeçalho'.

A etapa do pré-processador realmente leva todos eles e faz com que tudo pareça um único arquivo grande para o compilador. O que estava no cabeçalho ou na fonte não é realmente relevante neste momento. Geralmente, há uma opção de um compilador que pode mostrar a saída desse estágio.

Portanto, para cada arquivo fornecido na linha de comando do compilador, um arquivo enorme é fornecido ao compilador. Isso pode ter código / dados que ocuparão memória e / ou criarão um símbolo a ser referenciado a partir de outros arquivos. Agora, cada um deles irá gerar uma imagem de 'objeto'. O vinculador pode fornecer um 'símbolo duplicado' se o mesmo símbolo for encontrado em mais de dois arquivos de objetos que estão sendo vinculados. Talvez seja essa a razão; não é aconselhável colocar código em um arquivo de cabeçalho, o que pode criar símbolos no arquivo de objeto.

Os 'embutidos' geralmente são embutidos ... mas, ao depurar, eles podem não estar embutidos. Então, por que o vinculador não fornece erros multiplamente definidos? Simples ... Esses são símbolos 'fracos' e, desde que todos os dados / códigos de um símbolo fraco de todos os objetos tenham o mesmo tamanho e conteúdo, o vinculado manterá uma cópia e largará a cópia de outros objetos. Funciona.

vrdhn
fonte
3

Você pode fazer isso no C99: inlineé garantido que as funções sejam fornecidas em outro lugar; portanto, se uma função não foi incorporada, sua definição é traduzida em uma declaração (ou seja, a implementação é descartada). E, claro, você pode usar static.

SK-logic
fonte
1

Citações padrão em C ++

O rascunho padrão do C ++ 17 N4659 10.1.6 "O especificador em linha" diz que os métodos estão implicitamente embutidos:

4 Uma função definida dentro de uma definição de classe é uma função embutida.

e mais adiante, vemos que os métodos em linha não apenas podem, mas devem ser definidos em todas as unidades de tradução:

6 Uma função ou variável em linha deve ser definida em todas as unidades de tradução em que é usada ou deve ter exatamente a mesma definição em todos os casos (6.2).

Isso também é mencionado explicitamente em uma nota em 12.2.1 "Funções-membro":

1 Uma função membro pode ser definida (11.4) em sua definição de classe; nesse caso, é uma função membro em linha (10.1.6) [...]

3 [Nota: Pode haver no máximo uma definição de uma função membro não embutida em um programa. Pode haver mais de uma definição de função de membro em linha em um programa. Veja 6.2 e 10.1.6. - nota final]

Implementação do GCC 8.3

main.cpp

struct MyClass {
    void myMethod() {}
};

int main() {
    MyClass().myMethod();
}

Compile e visualize símbolos:

g++ -c main.cpp
nm -C main.o

saída:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

então vemos man nmque o MyClass::myMethodsímbolo está marcado como fraco nos arquivos de objeto ELF, o que implica que ele pode aparecer em vários arquivos de objeto:

"W" "w" O símbolo é um símbolo fraco que não foi identificado especificamente como um símbolo de objeto fraco. Quando um símbolo definido fraco é vinculado a um símbolo definido normal, o símbolo definido normal é usado sem erros. Quando um símbolo indefinido fraco é vinculado e o símbolo não é definido, o valor do símbolo é determinado de maneira específica do sistema, sem erros. Em alguns sistemas, maiúsculas indica que um valor padrão foi especificado.

Ciro Santilli adicionou uma nova foto
fonte
-4

Provavelmente pelo mesmo motivo que você deve colocar a implementação completa do método dentro da definição de classe em Java.

Eles podem parecer semelhantes, com colchetes irregulares e muitas das mesmas palavras-chave, mas são idiomas diferentes.

Paul Butcher
fonte
1
Não, não é realmente uma resposta real, e um dos principais objetivos da C ++ era para ser compatível com C.
Ed S.
4
Não, ele foi projetado para ter "Um alto grau de compatibilidade C" e "Nenhuma incompatibilidade gratuita com C". (ambos da Stroustrup). Concordo que poderia ser dada uma resposta mais aprofundada, para destacar por que essa incompatibilidade específica não é gratuita. Sinta-se livre para fornecer um.
Paul Butcher
Eu teria, mas Simon Richter já tinha quando publiquei isso. Podemos discutir sobre a diferença entre "compatibilidade com versões anteriores" e "Um alto grau de compatibilidade com C", mas o fato é que essa resposta está incorreta. A última declaração estaria correta se estivéssemos comparando C # e C ++, mas não tanto com C e C ++.
Ed S.