¿Por qué necesitamos incluir el .h mientras todo funciona cuando se incluye solo el archivo .cpp?

18

¿Por qué debemos incluir los archivos .hy .cppmientras podemos hacer que funcione únicamente al incluir el .cpparchivo?

Por ejemplo: crear file.hdeclaraciones que contengan, luego crear file.cppdefiniciones que contengan e incluir ambas en main.cpp.

Alternativamente: crear una file.cppdeclaración / definiciones que contengan (sin prototipos) incluyéndolo en main.cpp.

Ambos trabajan para mi. No puedo ver la diferencia. Tal vez sea útil tener una idea del proceso de compilación y vinculación.

reaffer
fuente
Compartir su investigación ayuda a todos . Cuéntanos qué has probado y por qué no satisfizo tus necesidades. Esto demuestra que te has tomado el tiempo para tratar de ayudarte a ti mismo, nos evita reiterar respuestas obvias y, sobre todo, te ayuda a obtener una respuesta más específica y relevante. También vea Cómo preguntar
mosquito
Recomiendo mirar las preguntas relacionadas aquí al lado. programmers.stackexchange.com/questions/56215/… y programmers.stackexchange.com/questions/115368/… son buenas lecturas
¿Por qué es esto en los programadores, y no en SO?
Lightness compite con Monica el
@LightnessRacesInOrbit porque es una cuestión de estilo de codificación y no una pregunta de "cómo hacerlo".
CashCow
@CashCow: Parece que malinterpretas SO, que no es un sitio de "cómo hacerlo". También parece que malinterpretas esta pregunta, que no es una cuestión de estilo de codificación.
Lightness compite con Monica el

Respuestas:

29

Si bien puede incluir .cpparchivos como mencionó, esta es una mala idea.

Como mencionó, las declaraciones pertenecen a los archivos de encabezado. Estos no causan problemas cuando se incluyen en varias unidades de compilación porque no incluyen implementaciones. La inclusión de la definición de una función o miembro de clase varias veces normalmente causará un problema (pero no siempre) porque el enlazador se confundirá y arrojará un error.

Lo que debería suceder es que cada .cpparchivo incluya definiciones para un subconjunto del programa, como una clase, un grupo de funciones lógicamente organizado, variables estáticas globales (use con moderación si es que lo hace), etc.

Cada unidad de compilación ( .cpparchivo) incluye las declaraciones que necesita para compilar las definiciones que contiene. Realiza un seguimiento de las funciones y clases a las que hace referencia, pero no contiene, por lo que el vinculador puede resolverlas más tarde cuando combina el código objeto en un archivo ejecutable o biblioteca.

Ejemplo

  • Foo.h -> contiene declaración (interfaz) para la clase Foo.
  • Foo.cpp -> contiene definición (implementación) para la clase Foo.
  • Main.cpp-> contiene el método principal, el punto de entrada del programa. Este código crea una instancia de un Foo y lo usa.

Ambos Foo.cppy Main.cppnecesitan incluir Foo.h. Foo.cpplo necesita porque está definiendo el código que respalda la interfaz de la clase, por lo que necesita saber qué es esa interfaz. Main.cpplo necesita porque está creando un Foo e invocando su comportamiento, por lo que tiene que saber cuál es ese comportamiento, el tamaño de un Foo en la memoria y cómo encontrar sus funciones, etc., pero todavía no necesita la implementación real.

El compilador generará Foo.odesde el Foo.cppcual contiene todo el código de clase Foo en forma compilada. También genera el Main.oque incluye el método principal y referencias no resueltas a la clase Foo.

Ahora viene el enlazador, que combina los dos archivos de objetos Foo.oy Main.oen un archivo ejecutable. Ve las referencias de Foo sin resolver, Main.opero ve que Foo.ocontiene los símbolos necesarios, por lo que "conecta los puntos", por así decirlo. Una llamada de función Main.oahora está conectada a la ubicación real del código compilado para que, en tiempo de ejecución, el programa pueda saltar a la ubicación correcta.

Si hubiera incluido el Foo.cpparchivo Main.cpp, habría dos definiciones de la clase Foo. El enlazador vería esto y diría "No sé cuál elegir, así que esto es un error". El paso de compilación tendría éxito, pero la vinculación no. (A menos que simplemente no compile, Foo.cpppero ¿por qué está en un .cpparchivo separado ?)

Finalmente, la idea de diferentes tipos de archivos es irrelevante para un compilador C / C ++. Compila "archivos de texto" que, con suerte, contienen un código válido para el idioma deseado. A veces puede saber el idioma en función de la extensión del archivo. Por ejemplo, compile un .carchivo sin opciones de compilación y asumirá C, mientras que una extensión .cco .cpple indicará que asuma C ++. Sin embargo, puedo decirle fácilmente a un compilador que compile un archivo .ho incluso .docxcomo C ++, y emitirá un .oarchivo object ( ) si contiene un código C ++ válido en formato de texto sin formato. Estas extensiones son más para el beneficio del programador. Si veo Foo.hy Foo.cpp, inmediatamente asumo que el primero contiene la declaración de la clase y el segundo contiene la definición.


fuente
No entiendo el argumento que estás haciendo aquí. Si acaba de incluir Foo.cppen Main.cppque no es necesario incluir el .harchivo, que tiene uno menos de archivos, todavía ganar en la división de código en archivos separados para facilitar la lectura, y su comando de compilación es más simple. Si bien entiendo la importancia de los archivos de encabezado, no creo que sea así
Benjamin Gruenbaum
2
Para un programa trivial, solo póngalo todo en un archivo. Ni siquiera incluya ningún encabezado de proyecto (solo encabezados de biblioteca). La inclusión de archivos fuente es un mal hábito que causará problemas para cualquiera de los programas más pequeños una vez que tenga varias unidades de compilación y realmente las compile por separado.
2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.La oración más importante con respecto a la pregunta.
marczellm
@Snowman Right, que es en lo que creo que debes enfocarte: múltiples unidades de compilación. Pegar código con / sin archivos de encabezado para dividir el código en partes más pequeñas es útil y ortogonal para el propósito de los archivos de encabezado. Si todo lo que queremos hacer es dividirlo en archivos de encabezado, no nos compre nada; una vez que necesitemos enlaces dinámicos, enlaces condicionales y compilaciones más avanzadas, de repente son muy útiles.
Benjamin Gruenbaum
Hay muchas maneras de organizar la fuente para un compilador / enlazador C / C ++. El autor de la pregunta no estaba seguro del proceso, así que le expliqué la mejor práctica de cómo organizar el código y cómo funciona el proceso. Puede dividir los pelos sobre la posibilidad de organizar el código de manera diferente, pero el hecho es que le expliqué cómo el 99% de los proyectos lo hacen, lo cual es la mejor práctica por una razón. Funciona bien y mantiene tu cordura.
9

Leer más sobre el papel de la C y preprocesador de C ++ , que es conceptualmente la primera "fase" de la C o C ++ compilador (históricamente era un programa separado /lib/cpp; ahora, por razones de rendimiento que se integra dentro del compilador adecuado cc1 o cc1plus). Lea en particular la documentación del cpppreprocesador GNU . Entonces, en la práctica, el compilador conceptualmente procesa primero su unidad de compilación (o unidad de traducción ) y luego trabaja en el formulario preprocesado.

Probablemente necesitará incluir siempre el archivo de encabezado file.hsi contiene (según lo dictado por convenciones y hábitos ):

  • definiciones macro
  • definiciones de tipos (p typedef. ej . struct, classetc., ...)
  • definiciones de static inlinefunciones
  • declaraciones de funciones externas.

Tenga en cuenta que es una cuestión de convenciones (y conveniencia) ponerlas en un archivo de encabezado.

Por supuesto, su implementación file.cppnecesita todo lo anterior, por lo que quiere al #include "file.h" principio.

Esta es una convención (pero muy común). Puede evitar los archivos de encabezado y copiar y pegar su contenido en archivos de implementación (es decir, unidades de traducción). Pero no quiere eso (excepto quizás si su código C o C ++ se genera automáticamente; entonces podría hacer que el programa generador haga esa copia y pegue, imitando la función del preprocesador).

El punto es que el preprocesador está haciendo Pruebas únicas operaciones. Podría (en principio) evitarlo completamente copiando y pegando, o reemplazarlo por otro "preprocesador" o generador de código C (como gpp o m4 ).

Un problema adicional es que los estándares recientes de C (o C ++) definen varios encabezados estándar. La mayoría de las implementaciones realmente implementan estos encabezados estándar como archivos (específicos de implementación) , pero creo que sería posible que una implementación conforme implemente estándares incluidos (como #include <stdio.h>para C o #include <vector>C ++) con algunos trucos de magia (por ejemplo, usando alguna base de datos o algunos información dentro del compilador).

Si usa compiladores de GCC (por ejemplo, gcco g++) puede usar el -Hindicador para informarse sobre cada inclusión y los -C -Eindicadores para obtener el formulario preprocesado. Por supuesto, hay muchos otros indicadores de compilación que afectan el preprocesamiento (por ejemplo, -I /some/dir/para agregar /some/dir/para buscar archivos incluidos y -Dpara predefinir alguna macro de preprocesador, etc., etc.).

NÓTESE BIEN. Las versiones futuras de C ++ (tal vez C ++ 20 , tal vez incluso más tarde) podrían tener módulos C ++ .

Basile Starynkevitch
fuente
1
Si los enlaces se pudren, ¿sería una buena respuesta?
Dan Pichelman el
44
Edité mi respuesta para mejorarla, y elegí cuidadosamente los enlaces, a wikipedia o a la documentación de GNU, que creo que seguirán siendo relevantes durante mucho tiempo.
Basile Starynkevitch
3

Debido al modelo de compilación de unidades múltiples de C ++, necesita una forma de tener código que aparece en su programa solo una vez (definiciones), y necesita una forma de tener código que aparece en cada unidad de traducción de su programa (declaraciones).

De esto nace el idioma de encabezado de C ++. Es una convención por una razón.

Usted puede volcar todo su programa en una sola unidad de traducción, pero esto trae consigo problemas con la reutilización de código, prueba de la unidad y la manipulación de dependencia entre módulos. También es solo un gran desastre.

La ligereza corre con Mónica
fuente
0

La respuesta seleccionada de ¿Por qué necesitamos escribir un archivo de encabezado? es una explicación razonable, pero quería agregar detalles adicionales.

Parece que lo racional para los archivos de encabezado tiende a perderse en la enseñanza y la discusión de C / C ++.

El archivo de encabezados proporciona una solución a dos problemas de desarrollo de aplicaciones:

  1. Separación de interfaz e implementación.
  2. Tiempos de compilación / enlace mejorados para programas grandes

C / C ++ puede escalar desde programas pequeños hasta programas muy grandes de líneas multimillonarias y miles de archivos. El desarrollo de aplicaciones puede escalar de equipos de un desarrollador a cientos de desarrolladores.

Puedes usar varios sombreros como desarrollador. En particular, puede ser el usuario de una interfaz para funciones y clases, o puede ser el escritor de una interfaz de funciones y clases.

Cuando está utilizando una función, necesita conocer la interfaz de la función, qué parámetros usar, qué funciones devuelve y necesita saber qué hace la función. Esto se documenta fácilmente en el archivo de encabezado sin tener que mirar la implementación. ¿Cuándo has leído la implementación de printf? Comprar lo usamos todos los días.

Cuando usted es el desarrollador de una interfaz, el sombrero cambia la otra dirección. El archivo de encabezado proporciona la declaración de la interfaz pública. El archivo de encabezado define lo que otra implementación necesita para usar esta interfaz. La información interna y privada de esta nueva interfaz no (y no debe) declararse en el archivo de encabezado. El archivo de encabezado público debe ser todo lo que alguien necesita para usar el módulo.

Para el desarrollo a gran escala, compilar y vincular puede llevar mucho tiempo. De muchos minutos a muchas horas (¡incluso a muchos días!). Dividir el software en interfaces (encabezados) e implementaciones (fuentes) proporciona un método para compilar solo archivos que necesitan compilarse en lugar de reconstruir todo.

Además, los archivos de encabezado permiten al desarrollador proporcionar una biblioteca (ya compilada) así como un archivo de encabezado. Es posible que otros usuarios de la biblioteca nunca vean la implementación adecuada, pero aún pueden usar la biblioteca con el archivo de encabezado. Hace esto todos los días con la biblioteca estándar C / C ++.

Incluso si está desarrollando una aplicación pequeña, el uso de técnicas de desarrollo de software a gran escala es un buen hábito. Pero también debemos recordar por qué usamos estos hábitos.

Bill Door
fuente
0

Es posible que también te preguntes por qué no solo colocas todo tu código en un solo archivo.

La respuesta más simple es el mantenimiento del código.

Hay momentos en que es razonable crear una clase:

  • todo en línea dentro de un encabezado
  • todo dentro de una unidad de compilación y sin encabezado.

El momento en que es razonable alinear totalmente en un encabezado es cuando la clase es realmente una estructura de datos con algunos captadores y definidores básicos y quizás un constructor que toma valores para inicializar sus miembros.

(Las plantillas, que deben estar todas en línea, son un problema ligeramente diferente).

El otro momento para crear una clase dentro de un encabezado es cuando puede usar la clase en múltiples proyectos y específicamente quiere evitar tener que vincular bibliotecas.

Un momento en el que puede incluir una clase completa dentro de una unidad de compilación y no exponer su encabezado es:

  • Una clase "impl" que solo usa la clase que implementa. Es un detalle de implementación de esa clase, y externamente no se usa.

  • Una implementación de una clase base abstracta que se crea mediante algún tipo de método de fábrica que devuelve un puntero / referencia / puntero inteligente) a la clase base. El método de fábrica estaría expuesto por la clase misma no lo estaría. (Además, si la clase tiene una instancia que se registra en una tabla a través de una instancia estática, ni siquiera necesita exponerse a través de una fábrica).

  • Una clase de tipo "functor".

En otras palabras, donde no desea que nadie incluya el encabezado.

Sé lo que podría estar pensando ... Si es solo por mantenimiento, al incluir el archivo cpp (o un encabezado totalmente integrado) puede editar fácilmente un archivo para "encontrar" el código y simplemente reconstruirlo.

Sin embargo, "mantenibilidad" no es solo tener el código ordenado. Es una cuestión de impactos del cambio. En general, se sabe que si mantiene un encabezado sin cambios y simplemente cambia un (.cpp)archivo de implementación , no será necesario reconstruir la otra fuente porque no debería haber efectos secundarios.

Esto hace que sea "más seguro" realizar tales cambios sin preocuparse por el efecto de imitación y eso es lo que realmente significa "mantenibilidad".

CashCow
fuente
-1

El ejemplo de Snowman necesita una pequeña extensión para mostrar exactamente por qué se necesitan archivos .h.

Agregue otra barra de clase a la obra que también depende de la clase Foo.

Foo.h -> contiene una declaración para la clase Foo

Foo.cpp -> contiene la definición (impementación) de la clase Foo

Main.cpp -> utiliza variables del tipo Foo.

Bar.cpp -> también usa variables de tipo Foo.

Ahora todos los archivos cpp deben incluir Foo.h Sería un error incluir Foo.cpp en más de otro archivo cpp. Linker fallaría porque la clase Foo se definiría más de una vez.

SkaarjFace
fuente
1
Eso tampoco lo es. Eso podría resolverse exactamente de la misma manera que se resuelve en los .harchivos de todos modos, con incluir guardias y es exactamente el mismo problema que tiene con los .harchivos de todos modos. Esto no es para qué .hsirven los archivos.
Benjamin Gruenbaum
No estoy seguro de cómo usaría incluir guardias porque funcionan solo por archivo (como lo hace el preprocesador). En este caso, ya que Main.cpp y Bar.cpp son archivos diferentes, Foo.cpp se incluiría solo una vez en ambos (guardado o no, no hace ninguna diferencia). Main.cpp y Bar.cpp compilarían. Pero el error de enlace todavía ocurre debido a la doble definición de la clase Foo. Sin embargo, también hay herencia que ni siquiera mencioné. Declaración de módulos externos (dll), etc.
SkaarjFace
No, sería una unidad de compilación, no se realizaría ningún enlace ya que incluye el .cpparchivo.
Benjamin Gruenbaum
No dije que no se compilará. Pero no vinculará main.o y bar.o en el mismo ejecutable. Sin vincular cuál es el punto de compilación en absoluto.
SkaarjFace
Uhh ... ¿conseguir que se ejecute el código? Todavía puede vincularlo pero simplemente no vincular los archivos entre sí, sino que serían la misma unidad de compilación.
Benjamin Gruenbaum
-2

Si escribe el código completo en el mismo archivo, su código será feo.
En segundo lugar, no podrás compartir tu clase escrita con otros. Según la ingeniería de software, debe escribir el código del cliente por separado. El cliente no debe saber cómo está funcionando su programa. Solo necesitan salida. Si escribe todo el programa en el mismo archivo, perderá la seguridad de su programa.

Talha Junaid
fuente