¿Por qué tener archivos de encabezado y archivos .cpp? [cerrado]

484

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

Peter Mortensen
fuente
3
Pregunta relacionada: stackoverflow.com/questions/1945846/…
Spoike
es un paradigma común de OOP, .h es una declaración de clase y cpp es la definición. Uno no necesita saber cómo se implementa, solo debe conocer la interfaz.
Manish Kakati
Esta es la mejor parte de la interfaz de separación de c ++ de la implementación. Siempre es bueno, en lugar de mantener todo el código en un solo archivo, tenemos una interfaz separada. Siempre hay cierta cantidad de código, como la función en línea, que forma parte de los archivos de encabezado. Se ve bien cuando se ve un archivo de encabezado que muestra la lista de funciones declaradas y las variables de clase.
Miank
Hay momentos en que los archivos de encabezado son esenciales para la compilación, no solo una preferencia de la organización o una forma de distribuir bibliotecas precompiladas. Digamos que tiene una estructura donde game.c depende de AMBOS physics.c y math.c; physics.c también depende de math.c. Si incluyó archivos .c y se olvidó de los archivos .h para siempre, tendría declaraciones duplicadas de math.c y ninguna esperanza de compilación. Esto es lo que tiene más sentido para mí por qué los archivos de encabezado son importantes. Espero que ayude a alguien más.
Samy Bencherif
Creo que tiene que ver con el hecho de que solo se permiten caracteres alfanuméricos en las extensiones. Ni siquiera sé si eso es cierto, solo adivinando
user12211554

Respuestas:

202

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

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

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

MadKeithV
fuente
178
No es realmente cierto. El encabezado todavía contiene una parte importante de la implementación. ¿Desde cuándo las variables de instancia privada son parte de la interfaz de una clase? Funciones de miembros privados? Entonces, ¿qué demonios están haciendo en el encabezado públicamente visible? Y se desmorona aún más con las plantillas.
jalf
13
Es por eso que dije que no es perfecto, y el idioma Pimpl es necesario para una mayor separación. Las plantillas son una lata completamente diferente de gusanos: incluso si la palabra clave "exportaciones" fuera totalmente compatible en la mayoría de los compiladores, seguiría siendo azúcar sintáctica en lugar de una separación real.
Joris Timmermans
44
¿Cómo manejan esto otros idiomas? por ejemplo, Java? No hay un concepto de archivo de encabezado en Java.
Lazer
8
@Lazer: Java es más fácil de analizar. El compilador de Java puede analizar un archivo sin conocer todas las clases en otros archivos y verificar los tipos más adelante. En C ++, muchas construcciones son ambiguas sin información de tipo, por lo que el compilador de C ++ necesita información sobre los tipos referenciados para analizar un archivo. Por eso necesita encabezados.
Niki
15
@nikie: ¿Qué tiene que ver la "facilidad" de análisis? Si Java tuviera una gramática que fuera al menos tan compleja como C ++, aún podría usar archivos java. En cualquier caso, ¿qué pasa con C? C es fácil de analizar, pero utiliza tanto encabezados como archivos c.
Thomas Eding
609

Compilación C ++

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

  1. El primero es la compilación de archivos de texto "fuente" en archivos binarios de "objeto": el archivo CPP es el archivo compilado y se compila sin ningún conocimiento acerca de los otros archivos CPP (o incluso bibliotecas), a menos que se alimente a través de una declaración sin formato o inclusión de encabezado. El archivo CPP generalmente se compila 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 archivo CPP solitario y pobre ...

La compilació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 se compilará porque A.CPP no tiene forma de saber que existe "doSomethingElse" ... A menos que haya una declaración en A.CPP, como:

// A.CPP
void doSomethingElse() ; // From B.CPP

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

Luego, si tiene C.CPP que usa el mismo símbolo, copie / pegue la declaración ...

¡COPIAR / PEGAR ALERTA!

Sí, hay un problema Las copias / pastas son peligrosas y difíciles de mantener. Lo que significa que sería genial si tuviéramos alguna forma de NO copiar / pegar, y aún así declarar el símbolo ... ¿Cómo podemos hacerlo? Mediante la inclusión de algún archivo de texto, que comúnmente tiene el sufijo .h, .hxx, .h ++ o, mi preferido para 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
}

Como includefunciona

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 pequeña cosa: ¿por qué incluir B.HPP en B.CPP?

En el caso actual, esto no es necesario, y B.HPP tiene la doSomethingElsedeclaración de función, y B.CPP tiene ladoSomethingElse definición de función (que es, en sí misma, una declaración). Pero en un caso más general, donde B.HPP se usa para declaraciones (y código en línea), podría no 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 esas declaraciones de B.HPP. Con todo, es "buen gusto" que una fuente incluya por defecto su encabezado.

Conclusión

Por lo tanto, el archivo de encabezado es necesario porque el compilador de C ++ no puede buscar declaraciones de símbolos por sí 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 de la existencia de los archivos HPP se explica anteriormente.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

o incluso más simple

#pragma once

// The declarations in the B.hpp file
paercebal
fuente
2
@nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?No es necesario. Mientras el CPP "incluya" al HPP, el precompilador automáticamente copiará y pegará el contenido del archivo HPP en el archivo CPP. Actualicé la respuesta para aclarar eso.
paercebal
77
@Bob: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. No lo hace Solo conoce los tipos proporcionados por el usuario, que, la mitad del tiempo, ni siquiera se molestará en leer el valor de retorno. Entonces, ocurren conversiones implícitas. Y luego, cuando tienes el código: foo(bar)ni siquiera puedes estar seguro de que foosea ​​una función. Por lo tanto, el compilador debe tener acceso a la información en los encabezados para decidir si la fuente se compila correctamente o no ... Luego, una vez que se compila el código, el vinculador solo vinculará las llamadas de funciones.
paercebal
3
@Bob: [continúa] ... Ahora, el enlazador podría hacer el trabajo realizado por el compilador, supongo, lo que haría posible su opción. (Supongo que este es el tema de la propuesta de "módulos" para el próximo estándar). Seems, they're just a pretty ugly arbitrary design.: Si C ++ se hubiera creado en 2012, de hecho. Pero recuerde que C ++ se basó en C en la década de 1980, y en ese momento, las restricciones eran bastante diferentes en ese momento (IIRC, se decidió para fines de adopción mantener los mismos enlazadores que los de C).
paercebal
1
@paercebal Gracias por la explicación y las notas, paercebal! ¿Por qué no puedo estar seguro de que foo(bar)es una función, si se obtiene como un puntero? De hecho, hablando de mal diseño, culpo a C, no a C ++. Realmente no me gustan algunas restricciones de C puro, como tener archivos de encabezado o que las funciones devuelvan un solo valor, mientras tomo múltiples argumentos en la entrada (¿no se siente natural que la entrada y la salida se comporten de manera similar? ; ¿por qué múltiples argumentos, pero solo salida?) :)
Boris Burkov
1
@Bobo:: Why can't I be sure, that foo(bar) is a functionfoo podría ser un tipo, por lo que tendría un constructor de clase llamado. In fact, speaking of bad design, I blame C, not C++: Puedo culpar a C por muchas cosas, pero haber sido diseñado en los años 70 no será una de ellas. Una vez más, las limitaciones de ese tiempo ... such as having header files or having functions return one and only one value: las tuplas pueden ayudar a mitigar eso, así como pasar argumentos por referencia. Ahora, ¿cuál sería la sintaxis para recuperar los valores múltiples devueltos, y valdría la pena cambiar el idioma?
Paercebal
93

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 múltiples archivos.

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

jalf
fuente
3
Me pregunto por qué los archivos de encabezado (o lo que realmente se necesitaba para la compilación / vinculación) no fueron simplemente "autogenerados".
Mateen Ulhaq
54

Debido a que en C ++, el código ejecutable final no contiene ninguna información de símbolo, es un código de máquina más o menos puro.

Por lo tanto, necesita una manera 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.

relajarse
fuente
16

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

andref
fuente
44
¿Por qué es desafortunada la herencia de C ++ de C?
Lokesh
3
@Lokesh Debido a su equipaje :(
陳 力
1
¿Cómo puede ser esto una respuesta?
Shuvo Sarker
14
@ShuvoSarker porque, como lo han demostrado miles de lenguajes, no existe una explicación técnica para que C ++ haga que los programadores escriban firmas de funciones dos veces. La respuesta a "¿por qué?" es "historia"
Boris
15

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

Como necesita esa información para decirle a su compilador "esta función está disponible más tarde cuando el enlazador está haciendo su trabajo", tuvieron que encontrar un segundo archivo donde se pudiera almacenar esta información compartida.

La mayoría de los lenguajes después de C / C ++ almacenan esta información en la salida (código de bytes de Java, por ejemplo) o no usan un formato precompilado en absoluto, siempre se distribuyen en forma de origen y se compilan sobre la marcha (Python, Perl).

Aaron Digulla
fuente
No funcionaría, referencias cíclicas. No puede construir a.lib desde a.cpp antes de construir b.lib desde b.cpp, pero tampoco puede construir b.lib antes de a.lib.
MSalters
20
Java resolvió eso, Python puede hacerlo, cualquier lenguaje moderno puede hacerlo. Pero en el momento en que se inventó C, la RAM era tan costosa y escasa que simplemente no era una opción.
Aaron Digulla
6

Es la forma preprocesadora de declarar interfaces. Pones la interfaz (declaraciones de método) 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.

Martin v. Löwis
fuente
4

A menudo querrá tener una definición de una interfaz sin tener que enviar el código completo. Por ejemplo, si tiene una biblioteca compartida, enviaría un archivo de encabezado 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 encabezado se utilizan, en mi humilde opinión, para al menos dos propósitos:

  • Es decir, al mantener las interfaces separadas de la implementación, es más fácil leer el código.
  • Tiempo de compilación. Al usar solo la interfaz donde sea posible, en lugar de la implementación completa, el tiempo de compilación se puede reducir porque el compilador simplemente puede hacer una referencia a la interfaz en lugar de tener que analizar el código real (que, idealmente, solo tendría que hacerse una sola vez)
user21037
fuente
3
¿Por qué los vendedores de bibliotecas no pueden simplemente enviar un archivo de "encabezado" generado? Un archivo de "encabezado" sin preprocesador debería proporcionar un rendimiento mucho mejor (a menos que la implementación se haya roto realmente)
Tom Hawtin - tackline
Creo que es irrelevante si el archivo de encabezado se genera o se escribe a mano, la pregunta no era "¿por qué las personas escriben ellos mismos los archivos de encabezado?", Sino "por qué tenemos archivos de encabezado". Lo mismo ocurre con los encabezados sin preprocesador. Claro, esto sería más rápido.
-5

Respondiendo a la respuesta de MadKeithV ,

Esto reduce las dependencias, de modo que el código que usa el encabezado no necesariamente necesita conocer todos los detalles de la implementación y cualquier otra clase / encabezado necesario solo para eso. Esto reducirá los tiempos de compilación y también la cantidad de compilació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 encabezados evitaríamos este tipo de dolor de cabeza ...

Alex v
fuente