Pergunta

Em C++, a declaração e definição de funções, variáveis ​​e constantes podem ser separadas da seguinte forma:

function someFunc();

function someFunc()
{
  //Implementation.
}

Na verdade, na definição de classes, este é frequentemente o caso.Uma classe geralmente é declarada com seus membros em um arquivo .h, e estes são então definidos em um arquivo .C correspondente.

Quais são as vantagens e desvantagens desta abordagem?

Foi útil?

Solução

Historicamente, isso era para ajudar o compilador.Você tinha que fornecer a lista de nomes antes de usá-los - seja esse o uso real ou uma declaração direta (deixando de lado o protótipo de função padrão do C).

Compiladores modernos para linguagens modernas mostram que isso não é mais uma necessidade, portanto a sintaxe de C e C++ (assim como de Objective-C e provavelmente outros) aqui é uma bagagem histórica.Na verdade, este é um dos grandes problemas do C++ que mesmo a adição de um sistema de módulos adequado não resolverá.

As desvantagens são:muitos arquivos de inclusão fortemente aninhados (rastreei árvores de inclusão antes, elas são surpreendentemente grandes) e redundância entre declaração e definição - tudo levando a tempos de codificação e tempos de compilação mais longos (já comparou os tempos de compilação entre projetos C++ e C# comparáveis?Esta é uma das razões da diferença).Os arquivos de cabeçalho devem ser fornecidos aos usuários de quaisquer componentes que você fornecer.Chances de violações de ODR.Confiança no pré-processador (muitas linguagens modernas não precisam de uma etapa de pré-processador), o que torna seu código mais frágil e mais difícil de ser analisado pelas ferramentas.

Vantagens:não muito.Você poderia argumentar que obtém uma lista de nomes de funções agrupados em um só lugar para fins de documentação - mas a maioria dos IDEs tem algum tipo de capacidade de dobrar código atualmente, e projetos de qualquer tamanho deveriam usar geradores de documentos (como doxygen) de qualquer maneira.Com uma sintaxe baseada em módulo mais limpa e sem pré-processador, é mais fácil para as ferramentas seguirem seu código e fornecerem isso e muito mais, então acho que essa "vantagem" é praticamente discutível.

Outras dicas

É um artefato de como os compiladores C/C++ funcionam.

À medida que um arquivo fonte é compilado, o pré-processador substitui cada instrução #include pelo conteúdo do arquivo incluído.Só depois o compilador tenta interpretar o resultado dessa concatenação.

O compilador então repassa esse resultado do começo ao fim, tentando validar cada instrução.Se uma linha de código invocar uma função que não foi definida anteriormente, ela desistirá.

Porém, há um problema com isso quando se trata de chamadas de função mutuamente recursivas:

void foo()
{
  bar();
}

void bar()
{
  foo();
}

Aqui, foo não compilará como bar É desconhecido.Se você alternar as duas funções, bar não compilará como foo É desconhecido.

Porém, se você separar declaração e definição, poderá ordenar as funções como desejar:

void foo();
void bar();

void foo()
{
  bar();
}

void bar()
{
  foo();
}

Aqui, quando o compilador processa foo ele já conhece a assinatura de uma função chamada bar, e está feliz.

É claro que os compiladores poderiam funcionar de uma maneira diferente, mas é assim que funcionam em C, C++ e, até certo ponto, em Objective-C.

Desvantagens:

Nenhum diretamente.Se você estiver usando C/C++ de qualquer maneira, é a melhor maneira de fazer as coisas.Se você tiver a opção de idioma/compilador, talvez possa escolher um onde isso não seja um problema.A única coisa a considerar ao dividir declarações em arquivos de cabeçalho é evitar instruções #include mutuamente recursivas - mas é para isso que servem os protetores de inclusão.

Vantagens:

  • Velocidade de compilação:Como todos os arquivos incluídos são concatenados e depois analisados, reduzindo a quantidade e a complexidade do código nos arquivos incluídos vai melhorar o tempo de compilação.
  • Evite duplicação/inlining de código:Se você definir completamente uma função em um arquivo de cabeçalho, cada arquivo objeto que inclui esse cabeçalho e faz referência a essa função conterá sua própria versão dessa função.Como observação lateral, se você querer inlining, você precisa colocar a definição completa no arquivo de cabeçalho (na maioria dos compiladores).
  • Encapsulamento/clareza:Uma classe/conjunto de funções bem definido, além de alguma documentação, deve ser suficiente para outros desenvolvedores usarem seu código.Não há (idealmente) necessidade de eles entenderem como o código funciona - então por que exigir que eles o examinem?(O contra-argumento de que pode ser útil para eles acessarem a implementação Quando solicitado ainda está de pé, é claro).

E, claro, se você não estiver interessado em expor uma função, geralmente ainda poderá optar por defini-la totalmente no arquivo de implementação, em vez de no cabeçalho.

Existem duas vantagens principais em separar a declaração e a definição no cabeçalho C++ e nos arquivos de origem.A primeira é que você evita problemas com o Uma regra de definição quando sua classe/funções/o que quer que seja #included em mais de um lugar.Em segundo lugar, ao fazer as coisas dessa maneira, você separa interface e implementação.Os usuários de sua classe ou biblioteca precisam apenas ver seu arquivo de cabeçalho para escrever o código que o utiliza.Você também pode dar um passo adiante com o Idioma de espinha e faça com que o código do usuário não precise ser recompilado toda vez que a implementação da biblioteca for alterada.

Você já mencionou a desvantagem da repetição de código entre os arquivos .h e .cpp.Talvez eu tenha escrito código C++ por muito tempo, mas não acho que seja que ruim.De qualquer forma, você precisa alterar todo o código do usuário toda vez que alterar a assinatura de uma função, então o que é mais um arquivo?Só é irritante quando você escreve uma classe pela primeira vez e precisa copiar e colar do cabeçalho para o novo arquivo de origem.

A outra desvantagem na prática é que, para escrever (e depurar!) um bom código que usa uma biblioteca de terceiros, geralmente é necessário ver o interior dele.Isso significa acesso ao código-fonte mesmo que você não possa alterá-lo.Se tudo o que você tem é um arquivo de cabeçalho e um arquivo de objeto compilado, pode ser muito difícil decidir se o bug é culpa sua ou deles.Além disso, olhar a fonte fornece informações sobre como usar e estender adequadamente uma biblioteca que a documentação pode não cobrir.Nem todo mundo envia um MSDN com sua biblioteca.E grandes engenheiros de software têm o péssimo hábito de fazer coisas com seu código que você nunca sonhou ser possível.;-)

O padrão exige que, ao usar uma função, uma declaração esteja no escopo.Isso significa que o compilador deve ser capaz de verificar em um protótipo (a declaração em um arquivo de cabeçalho) o que você está passando para ele.Exceto, é claro, para funções variáveis ​​- tais funções não validam argumentos.

Pense em C, quando isso não era necessário.Naquela época, os compiladores não tratavam nenhuma especificação de tipo de retorno como padrão para int.Agora, suponha que você tenha uma função foo() que retornou um ponteiro para void.Porém, como você não tinha uma declaração, o compilador pensará que deve retornar um número inteiro.Em alguns sistemas Motorola, por exemplo, números inteiros e ponteiros seriam retornados em registros diferentes.Agora, o compilador não usará mais o registro correto e, em vez disso, retornará seu ponteiro convertido para um número inteiro no outro registro.No momento em que você tenta trabalhar com esse ponteiro, o inferno começa.

Declarar funções dentro do cabeçalho é bom.Mas lembre-se, se você declarar e definir no cabeçalho, certifique-se de que eles estejam embutidos.Uma maneira de conseguir isso é colocar a definição dentro da definição da classe.Caso contrário, acrescente o inline palavra-chave.Caso contrário, você encontrará uma violação de ODR quando o cabeçalho for incluído em vários arquivos de implementação.

Você basicamente tem 2 visualizações na classe/função/qualquer coisa:

A declaração, onde você declara o nome, os parâmetros e os membros (no caso de uma estrutura/classe), e a definição onde você define o que as funções fazem.

Entre as desvantagens está a repetição, mas uma grande vantagem é que você pode declarar sua função como int foo(float f) e deixe os detalhes na implementação (= definição), para que qualquer pessoa que queira usar sua função foo inclua apenas seu arquivo de cabeçalho e links para sua biblioteca/arquivo de objeto, para que os usuários da biblioteca e também os compiladores tenham apenas que cuidar da interface definida, o que ajuda a entender as interfaces e acelera os tempos de compilação.

Uma vantagem que ainda não vi: API

Qualquer biblioteca ou código de terceiros que NÃO seja de código aberto (ou seja,proprietários) não terão sua implementação junto com a distribuição.A maioria das empresas simplesmente não se sente confortável em fornecer código-fonte.A solução fácil, basta distribuir as declarações de classe e assinaturas de funções que permitem o uso da DLL.

Isenção de responsabilidade:Não estou dizendo se é certo, errado ou justificado, só estou dizendo que já vi muito isso.

Vantagem

As classes podem ser referenciadas a partir de outros arquivos apenas incluindo a declaração.As definições podem então ser vinculadas posteriormente no processo de compilação.

Uma grande vantagem das declarações forward é que quando usadas com cuidado você pode reduzir as dependências de tempo de compilação entre os módulos.

Se ClassA.h precisar se referir a um elemento de dados em ClassB.h, muitas vezes você pode usar apenas referências diretas em ClassA.h e incluir ClassB.h em ClassA.cc em vez de ClassA.h, reduzindo assim o tempo de compilação dependência.

Para grandes sistemas, isso pode economizar muito tempo na construção.

  1. A separação proporciona uma visão limpa e organizada dos elementos do programa.
  2. Possibilidade de criar e vincular módulos/bibliotecas binários sem divulgar fontes.
  3. Vincule binários sem recompilar fontes.

Quando feita corretamente, essa separação reduz o tempo de compilação quando apenas a implementação foi alterada.

Desvantagem

Isso leva a muita repetição.A maior parte da assinatura da função precisa ser colocada em dois ou mais lugares (como observou Paulious).

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