¿Por qué C ++ tiene archivos de encabezado y archivos .cpp?
¿Por qué C ++ tiene archivos de encabezado y archivos .cpp?
Respuestas:
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.
Una compilación en C ++ se realiza en 2 fases principales:
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".
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?
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 ...
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
}
include
funcionaLa 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.
}
En el caso actual, esto no es necesario, y B.HPP tiene la doSomethingElse
declaració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.
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
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.
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 foo
sea 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.
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).
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?) :)
Why can't I be sure, that foo(bar) is a function
foo 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?
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.
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.
Porque C ++ los heredó de C. Lamentablemente.
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).
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.
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:
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 ...