¿Por qué tienen archivos de cabecera y archivos .cpp?

¿Por qué C ++ tiene archivos de cabecera y archivos .cpp?

Bueno, la razón principal sería separar la interfaz de la implementación. El encabezado declara “qué” hará una clase (o lo que se está implementando), mientras que el archivo cpp define “cómo” realizará esas características.

Esto reduce las dependencias, por lo que el código que usa el encabezado no necesariamente necesita conocer todos los detalles de la implementación y cualquier otra clase / cabecera necesaria solo para eso. Esto reducirá los tiempos de comstackción y también la cantidad de recomstackción necesaria cuando algo cambia en la implementación.

No es perfecto, y normalmente recurrirías a técnicas como Pimpl Idiom para separar adecuadamente la interfaz y la implementación, pero es un buen comienzo.

Comstackción de C ++

Una comstackción en C ++ se realiza en 2 fases principales:

  1. El primero es la comstackción de archivos de texto “fuente” en archivos “objeto” binarios: el archivo CPP es el archivo comstackdo y se comstack sin ningún conocimiento sobre los otros archivos CPP (o incluso bibliotecas), a menos que se le suministre a través de statement cruda o inclusión del encabezado. El archivo CPP generalmente se comstack en un archivo .OBJ o .O “objeto”.

  2. El segundo es la vinculación de todos los archivos “objeto” y, por lo tanto, la creación del archivo binario final (ya sea una biblioteca o un ejecutable).

¿Dónde encaja el HPP en todo este proceso?

Un pobre archivo CPP solitario …

La comstackción de cada archivo CPP es independiente de todos los demás archivos CPP, lo que significa que si A.CPP necesita un símbolo definido en B.CPP, como:

// A.CPP void doSomething() { doSomethingElse(); // Defined in B.CPP } // B.CPP void doSomethingElse() { // Etc. } 

No comstackrá porque A.CPP no tiene manera de saber que “doSomethingElse” existe … A menos que haya una statement en A.CPP, como:

 // A.CPP void doSomethingElse() ; // From B.CPP void doSomething() { doSomethingElse() ; // Defined in B.CPP } 

Entonces, si tiene C.CPP que usa el mismo símbolo, entonces copie / pegue la statement …

COPY / PASTE ALERT!

Sí, hay un problema Copiar / pegar es peligroso y difícil de mantener. Lo que significa que sería genial si tuviéramos alguna manera de NO copiar / pegar, y todavía declarar el símbolo … ¿Cómo podemos hacerlo? Por la inclusión de algún archivo de texto, que normalmente tiene el sufijo por .h, .hxx, .h ++ o, mi preferido para los archivos C ++, .hpp:

 // B.HPP (here, we decided to declare every symbol defined in B.CPP) void doSomethingElse() ; // A.CPP #include "B.HPP" void doSomething() { doSomethingElse() ; // Defined in B.CPP } // B.CPP #include "B.HPP" void doSomethingElse() { // Etc. } // C.CPP #include "B.HPP" void doSomethingAgain() { doSomethingElse() ; // Defined in B.CPP } 

¿Cómo include trabajo?

La inclusión de un archivo, en esencia, analizará y luego copiará y pegará su contenido en el archivo CPP.

Por ejemplo, en el siguiente código, con el encabezado A.HPP:

 // A.HPP void someFunction(); void someOtherFunction(); 

… la fuente B.CPP:

 // B.CPP #include "A.HPP" void doSomething() { // Etc. } 

… se convertirá después de la inclusión:

 // B.CPP void someFunction(); void someOtherFunction(); void doSomething() { // Etc. } 

Una cosa pequeña: ¿por qué incluir B.HPP en B.CPP?

En el caso actual, esto no es necesario, y B.HPP tiene la statement de función doSomethingElse, y B.CPP tiene la definición de función doSomethingElse (que es, en sí misma, una statement). Pero en un caso más general, donde B.HPP se usa para declaraciones (y código en línea), no puede haber una definición correspondiente (por ejemplo, enumeraciones, estructuras simples, etc.), por lo que la inclusión podría ser necesaria si B.CPP usa esa statement de B.HPP. En general, es “de buen gusto” que una fuente incluya por defecto su encabezado.

Conclusión

Por lo tanto, el archivo de encabezado es necesario, porque el comstackdor de C ++ no puede buscar declaraciones de símbolos solo, y por lo tanto, debe ayudarlo al incluir esas declaraciones.

Una última palabra: debe colocar protectores de encabezado alrededor del contenido de sus archivos HPP, para asegurarse de que las inclusiones múltiples no rompan nada, pero en general, creo que la razón principal para la existencia de los archivos HPP se explica más arriba.

 #ifndef B_HPP_ #define B_HPP_ // The declarations in the B.hpp file #endif // B_HPP_ 

Debido a que C, donde se originó el concepto, tiene 30 años, y en aquel entonces, era la única forma viable de vincular el código de varios archivos.

Hoy en día, es un hack horrible que destruye totalmente el tiempo de comstackción en C ++, causa innumerables dependencias innecesarias (porque las definiciones de clase en un archivo de encabezado exponen demasiada información sobre la implementación), y así sucesivamente.

Porque en C ++, el código ejecutable final no contiene ninguna información de símbolos, es más o menos código de máquina puro.

Por lo tanto, necesita una forma de describir la interfaz de un fragmento de código, que es independiente del código en sí. Esta descripción está en el archivo de encabezado.

Porque las personas que diseñaron el formato de la biblioteca no querían “desperdiciar” espacio para información raramente utilizada, como macros de preprocesador C y declaraciones de funciones.

Como necesita esa información para decirle a su comstackdor que “esta función está disponible más adelante cuando el enlazador está haciendo su trabajo”, tuvieron que crear un segundo archivo donde se pudiera almacenar esta información compartida.

La mayoría de los lenguajes posteriores a C / C ++ almacenan esta información en la salida (bytecode de Java, por ejemplo) o no usan ningún formato precomstackdo, se distribuyen siempre en forma de fuente y se comstackn sobre la marcha (Python, Perl).

Porque C ++ los heredó de C. Desafortunadamente.

Es la forma preprocesadora de declarar interfaces. Coloca la interfaz (declaraciones de métodos) en el archivo de encabezado y la implementación en el cpp. Las aplicaciones que usan su biblioteca solo necesitan conocer la interfaz, a la que pueden acceder a través de #include.

A menudo querrás tener una definición de interfaz sin tener que enviar el código completo. Por ejemplo, si tiene una biblioteca compartida, debe enviar un archivo de encabezado con ella que define todas las funciones y símbolos utilizados en la biblioteca compartida. Sin archivos de encabezado, necesitaría enviar la fuente.

Dentro de un solo proyecto, los archivos de cabecera se utilizan, en mi humilde opinión, para al menos dos propósitos:

  • Claridad, es decir, al mantener las interfaces separadas de la implementación, es más fácil leer el código
  • Tiempo de comstackción. Al usar solo la interfaz donde sea posible, en lugar de la implementación completa, el tiempo de comstackción puede reducirse porque el comstackdor simplemente puede hacer una referencia a la interfaz en lugar de tener que analizar el código real (lo que, idealmente, solo tendría que hacerse) una sola vez).

Respondiendo a la respuesta de MadKeithV ,

Esto reduce las dependencias, por lo que el código que usa el encabezado no necesariamente necesita conocer todos los detalles de la implementación y cualquier otra clase / cabecera necesaria solo para eso. Esto reducirá los tiempos de comstackción, y también la cantidad de recomstackción necesaria cuando algo en la implementación cambia.

Otra razón es que un encabezado proporciona una identificación única para cada clase.

Entonces, si tenemos algo como

 class A {..}; class B : public A {...}; class C { include A.cpp; include B.cpp; ..... }; 

Tendremos errores cuando intentemos construir el proyecto, ya que A es parte de B, con cabeceras evitaríamos este tipo de dolor de cabeza …