¿Por qué C ++ necesita un archivo de encabezado separado?

138

Nunca entendí realmente por qué C ++ necesita un archivo de encabezado separado con las mismas funciones que en el archivo .cpp. Hace que crear clases y refactorizarlas sea muy difícil, y agrega archivos innecesarios al proyecto. Y luego está el problema de tener que incluir archivos de encabezado, pero tener que verificar explícitamente si ya se ha incluido.

C ++ fue ratificado en 1998, entonces, ¿por qué está diseñado de esta manera? ¿Qué ventajas tiene tener un archivo de encabezado separado?


Siguiente pregunta:

¿Cómo encuentra el compilador el archivo .cpp con el código, cuando todo lo que incluyo es el archivo .h? ¿Asume que el archivo .cpp tiene el mismo nombre que el archivo .h, o realmente mira a través de todos los archivos en el árbol de directorios?

Mario
fuente
2
Si desea editar un solo archivo, consulte lzz (www.lazycplusplus.com).
Richard Corden el
3
Duplicado exacto: stackoverflow.com/questions/333889 . Casi duplicado: stackoverflow.com/questions/752793
Peter Mortensen

Respuestas:

105

Parece que está preguntando acerca de cómo separar las definiciones de las declaraciones, aunque hay otros usos para los archivos de encabezado.

La respuesta es que C ++ no "necesita" esto. Si marca todo en línea (que de todos modos es automático para las funciones miembro definidas en una definición de clase), entonces no hay necesidad de la separación. Simplemente puede definir todo en los archivos de encabezado.

Las razones por las que puede desear separar son:

  1. Para mejorar los tiempos de construcción.
  2. Para vincular contra el código sin tener la fuente de las definiciones.
  3. Para evitar marcar todo "en línea".

Si su pregunta más general es, "¿por qué C ++ no es idéntico a Java?", Entonces tengo que preguntar, "¿por qué escribe C ++ en lugar de Java?" ;-pags

Más en serio, sin embargo, la razón es que el compilador de C ++ no puede simplemente acceder a otra unidad de traducción y descubrir cómo usar sus símbolos, de la forma en que javac puede y lo hace. El archivo de encabezado es necesario para declarar al compilador qué puede esperar que esté disponible en el momento del enlace.

Entonces, #includees una sustitución textual directa. Si define todo en los archivos de encabezado, el preprocesador termina creando una enorme copia y pegando de cada archivo de origen en su proyecto, e introduciéndolo en el compilador. El hecho de que el estándar C ++ se ratificó en 1998 no tiene nada que ver con esto, es el hecho de que el entorno de compilación para C ++ se basa tan estrechamente en el de C.

Convertir mis comentarios para responder a su pregunta de seguimiento:

¿Cómo encuentra el compilador el archivo .cpp con el código?

No lo hace, al menos no en el momento en que compila el código que utilizó el archivo de encabezado. Las funciones con las que se enlaza ni siquiera necesitan haber sido escritas todavía, no importa que el compilador sepa en qué .cpparchivo estarán. Todo lo que el código de llamada necesita saber en el momento de la compilación se expresa en la declaración de la función. En el momento del enlace, proporcionará una lista de .oarchivos, o bibliotecas estáticas o dinámicas, y el encabezado en efecto es una promesa de que las definiciones de las funciones estarán allí en algún lugar.

Steve Jessop
fuente
3
Para agregar a "Las razones por las que es posible que desee separar son:" Y creo que la función más importante de los archivos de encabezado es: Separar el diseño de la estructura del código de la implementación, porque: A. Cuando ingresa en estructuras realmente complicadas que involucran muchos objetos, es mucho más fácil examinar los archivos de encabezado y recordar cómo funcionan juntos, complementado por sus comentarios de encabezado. B. Si una persona no se encarga de definir toda la estructura del objeto y otra se encarga de la implementación, mantiene las cosas organizadas. Sobre todo, creo que hace que el código complejo sea más legible.
Andres Canella
De una manera más simple, puedo pensar en la utilidad de la separación de archivos de encabezado frente a cpp es separar Interfaz vs. Implementaciones, lo que realmente ayuda para proyectos medianos / grandes.
Krishna Oza
Tenía la intención de que se incluyera en (2), "enlace contra código sin tener las definiciones". De acuerdo, es posible que aún tenga acceso al repositorio de git con las definiciones, pero el punto es que puede compilar su código por separado de las implementaciones de sus dependencias y luego vincularlo. Si, literalmente, todo lo que deseaba era separar la interfaz / implementación en diferentes archivos, sin preocuparse por construir por separado, entonces podría hacerlo simplemente haciendo que foo_interface.h incluya foo_implementation.h.
Steve Jessop
44
@AndresCanella No, no lo hace. Hace que leer y mantener un código que no sea tuyo sea una pesadilla. Para comprender completamente lo que hace algo en el código, debe saltar a través de 2n archivos en lugar de n archivos. Esto simplemente no es notación Big-Oh, 2n hace una gran diferencia en comparación con solo n.
Błażej Michalik
1
Segundo, es una mentira que los encabezados ayudan. compruebe la fuente de minix, por ejemplo, es muy difícil seguir dónde comienza, dónde se pasa el control, dónde se declaran / definen las cosas ... si se construyera a través de módulos dinámicos separados, sería digerible al dar sentido a una cosa y luego saltar a Un módulo de dependencia. en su lugar, debe seguir los encabezados y hace que leer cualquier código escrito de esta manera sea un infierno. en contraste, nodejs deja en claro de dónde viene lo que no tiene ifdefs, y puede identificar fácilmente de dónde vino.
Dmitry
91

C ++ lo hace así porque C lo hizo así, entonces la verdadera pregunta es ¿por qué C lo hizo así? Wikipedia habla un poco de esto.

Los lenguajes compilados más nuevos (como Java, C #) no utilizan declaraciones de reenvío; Los identificadores se reconocen automáticamente de los archivos de origen y se leen directamente de los símbolos de la biblioteca dinámica. Esto significa que no se necesitan archivos de encabezado.

Donald Byrd
fuente
13
+1 Golpea el clavo en la cabeza. Esto realmente no requiere una explicación detallada.
MSalters
66
No me dio en el clavo :( Todavía tengo que buscar por qué C ++ tiene que usar declaraciones hacia adelante y por qué no puede reconocer los identificadores de los archivos fuente y leer directamente de los símbolos de la biblioteca dinámica, y por qué C ++ lo hizo así solo porque C lo hizo así: p
Alexander Taylor
3
Y usted es un mejor programador por haberlo hecho @AlexanderTaylor :)
Donald Byrd
66

Algunas personas consideran que los archivos de encabezado son una ventaja:

  • Se afirma que habilita / hace cumplir / permite la separación de la interfaz y la implementación, pero generalmente, este no es el caso. Los archivos de encabezado están llenos de detalles de implementación (por ejemplo, las variables miembro de una clase deben especificarse en el encabezado, aunque no formen parte de la interfaz pública), y las funciones pueden definirse en línea en la declaración de clase , y a menudo lo están. en el encabezado, nuevamente destruyendo esta separación.
  • A veces se dice que mejora el tiempo de compilación porque cada unidad de traducción se puede procesar de forma independiente. Y, sin embargo, C ++ es probablemente el lenguaje más lento que existe cuando se trata de tiempos de compilación. Una parte de la razón son las muchas inclusiones repetidas del mismo encabezado. Varias unidades de traducción incluyen una gran cantidad de encabezados, lo que requiere que se analicen varias veces.

En última instancia, el sistema de encabezado es un artefacto de los años 70 cuando C fue diseñado. En aquel entonces, las computadoras tenían muy poca memoria, y mantener todo el módulo en la memoria no era una opción. Un compilador tuvo que comenzar a leer el archivo en la parte superior, y luego continuar linealmente a través del código fuente. El mecanismo de encabezado permite esto. El compilador no tiene que considerar otras unidades de traducción, solo tiene que leer el código de arriba a abajo.

Y C ++ retuvo este sistema por compatibilidad con versiones anteriores.

Hoy no tiene sentido. Es ineficiente, propenso a errores y demasiado complicado. Hay formas mucho mejores de separar la interfaz y la implementación, si ese fuera el objetivo.

Sin embargo, una de las propuestas para C ++ 0x era agregar un sistema de módulos adecuado, que permitiera compilar código similar a .NET o Java, en módulos más grandes, todo de una vez y sin encabezados. Esta propuesta no hizo el corte en C ++ 0x, pero creo que todavía está en la categoría "nos encantaría hacer esto más adelante". Quizás en un TR2 o similar.

jalf
fuente
Esta es la mejor respuesta en la página. ¡Gracias!
Chuck Le Butt
29

Según tengo entendido (limitado: normalmente no soy un desarrollador de C), esto se basa en C. Recuerde que C no sabe qué clases o espacios de nombres son, es solo un programa largo. Además, las funciones deben declararse antes de usarlas.

Por ejemplo, lo siguiente debería dar un error de compilación:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

El error debería ser que "SomeOtherFunction no está declarado" porque lo llamas antes de su declaración. Una forma de solucionar esto es moviendo SomeOtherFunction sobre SomeFunction. Otro enfoque es declarar primero la firma de funciones:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Esto le permite al compilador saber: Busque en alguna parte del código, hay una función llamada SomeOtherFunction que devuelve void y no toma ningún parámetro. Entonces, si encuentra un código que intenta llamar a SomeOtherFunction, no entre en pánico y, en su lugar, vaya a buscarlo.

Ahora, imagine que tiene SomeFunction y SomeOtherFunction en dos archivos .c diferentes. Luego debe #incluir "SomeOther.c" en Some.c. Ahora, agregue algunas funciones "privadas" a SomeOther.c. Como C no conoce funciones privadas, esa función también estaría disponible en Some.c.

Aquí es donde entran los archivos .h: especifican todas las funciones (y variables) que desea 'Exportar' desde un archivo .c al que se puede acceder en otros archivos .c. De esa manera, obtienes algo así como un alcance público / privado. Además, puede entregar este archivo .h a otras personas sin tener que compartir su código fuente: los archivos .h también funcionan con archivos compilados .lib.

Entonces, la razón principal es realmente la conveniencia, la protección del código fuente y tener un poco de desacoplamiento entre las partes de su aplicación.

Eso fue C sin embargo. C ++ introdujo clases y modificadores privados / públicos, por lo que si bien aún se puede preguntar si son necesarios, C ++ AFAIK aún requiere la declaración de funciones antes de usarlos. Además, muchos desarrolladores de C ++ también son o fueron devleopers de C ++ y asumieron sus conceptos y hábitos a C ++. ¿Por qué cambiar lo que no está roto?

Michael Stum
fuente
55
¿Por qué el compilador no puede ejecutar el código y encontrar todas las definiciones de funciones? Parece algo que sería bastante fácil de programar en el compilador.
Marius
3
Si tiene la fuente, que a menudo no tiene. C ++ compilado es efectivamente un código de máquina con suficiente información adicional para cargar y vincular el código. Luego, apunta la CPU al punto de entrada y la deja correr. Esto es fundamentalmente diferente de Java o C #, donde el código se compila en un bytecode intermedio que contiene metadatos en su contenido.
DevSolar
Supongo que en 1972, esa podría haber sido una operación bastante costosa para el compilador.
Michael Stum
Exactamente lo que dijo Michael Stum. Cuando se definió este comportamiento, un escaneo lineal a través de una sola unidad de traducción fue lo único que se pudo implementar prácticamente.
jalf
3
Sí, compilar en un 16 masa amarga con cinta de masa no es trivial.
MSalters
11

Primera ventaja: si no tiene archivos de encabezado, deberá incluir los archivos de origen en otros archivos de origen. Esto haría que los archivos incluidos se vuelvan a compilar cuando cambie el archivo incluido.

Segunda ventaja: permite compartir las interfaces sin compartir el código entre diferentes unidades (diferentes desarrolladores, equipos, empresas, etc.)

erelender
fuente
1
¿Está insinuando que, por ejemplo, en C # 'tendría que incluir los archivos fuente en otros archivos fuente'? Porque obviamente no lo haces. Para la segunda ventaja, creo que eso depende demasiado del idioma: no usará archivos .h en, por ejemplo, Delphi
Vlagged
Tienes que recompilar todo el proyecto de todos modos, ¿la primera ventaja realmente cuenta?
Marius
ok, pero no creo que sea una característica del lenguaje. Es más algo práctico tratar con la declaración C antes de la definición de "problema". Es como si alguien famoso dijera "no es un error, es una característica" :)
neuro
@ Mario: Sí, realmente cuenta. Vincular todo el proyecto es diferente de compilar y vincular todo el proyecto. A medida que aumenta el número de archivos en el proyecto, compilarlos se vuelve realmente molesto. @Vlagged: Tienes razón, pero no comparé c ++ con otro idioma. Comparé el uso de solo archivos fuente frente al uso de archivos fuente y de encabezado.
erelender
C # no incluye archivos fuente en otros, pero aún tiene que hacer referencia a los módulos, y eso hace que el compilador busque los archivos fuente (o los refleje en el binario) para analizar los símbolos que usa su código.
gbjbaanb
5

La necesidad de archivos de encabezado resulta de las limitaciones que tiene el compilador para conocer la información de tipo para funciones y / o variables en otros módulos. El programa compilado o la biblioteca no incluye la información de tipo requerida por el compilador para enlazar a cualquier objeto definido en otras unidades de compilación.

Para compensar esta limitación, C y C ++ permiten declaraciones y estas declaraciones pueden incluirse en módulos que las utilizan con la ayuda de la directiva #include del preprocesador.

Los lenguajes como Java o C #, por otro lado, incluyen la información necesaria para el enlace en la salida del compilador (archivo de clase o ensamblaje). Por lo tanto, ya no es necesario mantener declaraciones independientes para que los clientes de un módulo las incluyan.

La razón por la cual la información de enlace no se incluye en la salida del compilador es simple: no es necesaria en tiempo de ejecución (cualquier tipo de verificación ocurre en tiempo de compilación). Simplemente desperdiciaría espacio. Recuerde que C / C ++ provienen de una época en la que el tamaño de un archivo ejecutable o biblioteca importaba bastante.

VoidPointer
fuente
Estoy de acuerdo contigo. Tengo una idea similar aquí: stackoverflow.com/questions/3702132/…
smwikipedia
4

C ++ fue diseñado para agregar características modernas de lenguaje de programación a la infraestructura de C, sin cambiar innecesariamente nada sobre C que no se tratara específicamente del lenguaje en sí.

Sí, en este punto (10 años después del primer estándar C ++ y 20 años después de que comenzó a crecer seriamente en uso) es fácil preguntarse por qué no tiene un sistema de módulos adecuado. Obviamente, cualquier lenguaje nuevo que se esté diseñando hoy no funcionaría como C ++. Pero ese no es el punto de C ++.

El objetivo de C ++ es ser evolutivo, una continuación fluida de la práctica existente, solo agregando nuevas capacidades sin (con demasiada frecuencia) romper cosas que funcionen adecuadamente para su comunidad de usuarios.

Esto significa que hace que algunas cosas sean más difíciles (especialmente para las personas que comienzan un nuevo proyecto), y algunas cosas más fáciles (especialmente para aquellos que mantienen el código existente) que lo harían otros idiomas.

Entonces, en lugar de esperar que C ++ se convierta en C # (lo cual sería inútil ya que ya tenemos C #), ¿por qué no elegir la herramienta adecuada para el trabajo? Yo mismo, me esfuerzo por escribir fragmentos significativos de nuevas funcionalidades en un lenguaje moderno (uso C #), y tengo una gran cantidad de C ++ existente que mantengo en C ++ porque no tendría ningún valor real reescribirlo. todas. Se integran muy bien de todos modos, por lo que es en gran medida indoloro.

Daniel Earwicker
fuente
¿Cómo se integra C # y C ++? A través de COM?
Peter Mortensen
1
Hay tres formas principales, la "mejor" depende de su código existente. He usado los tres. El que más uso es COM porque mi código existente ya fue diseñado alrededor de él, por lo que es prácticamente perfecto, funciona muy bien para mí. En algunos lugares extraños, uso C ++ / CLI, que ofrece una integración increíblemente fluida para cualquier situación en la que aún no tenga interfaces COM (y puede preferir usar interfaces COM existentes incluso si las tiene). Finalmente, hay p / invoke que básicamente le permite llamar a cualquier función similar a C expuesta desde una DLL, por lo que le permite llamar directamente a cualquier API Win32 desde C #.
Daniel Earwicker
4

Bueno, C ++ fue ratificado en 1998, pero había estado en uso durante mucho más tiempo que eso, y la ratificación estaba principalmente estableciendo el uso actual en lugar de imponer una estructura. Y dado que C ++ se basó en C, y C tiene archivos de encabezado, C ++ también los tiene.

La razón principal de los archivos de encabezado es permitir la compilación separada de archivos y minimizar las dependencias.

Digamos que tengo foo.cpp y quiero usar el código de los archivos bar.h / bar.cpp.

Puedo #incluir "bar.h" en foo.cpp, y luego programar y compilar foo.cpp incluso si bar.cpp no ​​existe. El archivo de encabezado actúa como una promesa para el compilador de que las clases / funciones en bar.h existirán en tiempo de ejecución, y ya tiene todo lo que necesita saber.

Por supuesto, si las funciones en bar.h no tienen cuerpos cuando intento vincular mi programa, entonces no se vinculará y obtendré un error.

Un efecto secundario es que puede proporcionar a los usuarios un archivo de encabezado sin revelar su código fuente.

Otra es que si cambia la implementación de su código en el archivo * .cpp, pero no cambia el encabezado, solo necesita compilar el archivo * .cpp en lugar de todo lo que lo usa. Por supuesto, si pones mucha implementación en el archivo de encabezado, entonces esto se vuelve menos útil.

Mark Krenitsky
fuente
3

No necesita un archivo de encabezado separado con las mismas funciones que en main. Solo lo necesita si desarrolla una aplicación usando múltiples archivos de código y si usa una función que no fue declarada previamente.

Es realmente un problema de alcance.

Alex Rodrigues
fuente
1

C ++ fue ratificado en 1998, entonces, ¿por qué está diseñado de esta manera? ¿Qué ventajas tiene tener un archivo de encabezado separado?

En realidad, los archivos de encabezado se vuelven muy útiles cuando se examinan los programas por primera vez, al revisar los archivos de encabezado (usando solo un editor de texto) le da una visión general de la arquitectura del programa, a diferencia de otros lenguajes donde tiene que usar herramientas sofisticadas para ver clases y sus funciones miembro.

Diaa Sami
fuente
1

Creo que la verdadera (histórica) razón detrás de los archivos de cabecera como estaba haciendo más fácil para los desarrolladores de compiladores ... pero entonces, archivos de cabecera no dar ventajas.
Mira esta publicación anterior para más discusiones ...

Paolo Tedesco
fuente
1

Bueno, puedes desarrollar perfectamente C ++ sin archivos de encabezado. De hecho, algunas bibliotecas que usan plantillas de forma intensiva no usan el paradigma de los archivos de encabezado / código (ver impulso). Pero en C / C ++ no puede usar algo que no está declarado. Una forma práctica de lidiar con eso es usar archivos de encabezado. Además, obtiene la ventaja de compartir la interfaz sin compartir el código / implementación. Y creo que no fue previsto por los creadores de C: cuando usas archivos de encabezado compartidos tienes que usar el famoso:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

// [...]
// my header
// [...]

#endif // MY_HEADER_SWEET_GUARDIAN

eso no es realmente una característica del lenguaje, sino una forma práctica de lidiar con la inclusión múltiple.

Entonces, creo que cuando se creó C, se subestimaron los problemas con la declaración directa y ahora cuando se usa un lenguaje de alto nivel como C ++ tenemos que lidiar con este tipo de cosas.

Otra carga para nosotros, los usuarios pobres de C ++ ...

neuro
fuente
1

Si desea que el compilador descubra los símbolos definidos en otros archivos automáticamente, debe forzar al programador a colocar esos archivos en ubicaciones predefinidas (como la estructura de paquetes Java determina la estructura de carpetas del proyecto). Prefiero los archivos de encabezado. También necesitaría fuentes de bibliotecas que use o alguna forma uniforme de poner la información que necesita el compilador en binarios.

Tadeusz Kopec
fuente