¿Por qué no debería incluir archivos cpp y, en su lugar, usar un encabezado?

Así que terminé mi primera asignación de progtwigción en C ++ y recibí mi calificación. Pero de acuerdo con la calificación, perdí marcas por including cpp files instead of compiling and linking them . No estoy muy claro sobre lo que eso significa.

Echando un vistazo a mi código, elegí no crear archivos de encabezado para mis clases, pero hice todo en los archivos cpp (parecía funcionar bien sin archivos de encabezado …). Supongo que el evaluador quiso decir que escribí ‘#include’ mycppfile.cpp ‘;’ en algunos de mis archivos.

Mi razonamiento para #include include’ing los archivos cpp era: – Todo lo que se suponía que debía ir al archivo de encabezado estaba en mi archivo cpp, así que fingí que era como un archivo de encabezado – En la moda mono-veo-mono, vi que otros archivos de cabecera eran #include ‘d en los archivos, así que hice lo mismo con mi archivo cpp.

Entonces, ¿qué hice exactamente mal y por qué es malo?

Según mi leal saber y entender, el estándar de C ++ no conoce diferencia entre los archivos de cabecera y los archivos fuente. En lo que respecta al lenguaje, cualquier archivo de texto con código legal es el mismo que cualquier otro. Sin embargo, aunque no es ilegal, incluir archivos fuente en su progtwig eliminará prácticamente todas las ventajas que tendría al separar sus archivos fuente en primer lugar.

Básicamente, lo que hace #include es decirle al preprocesador que tome todo el archivo que ha especificado y lo copie en su archivo activo antes de que el comstackdor lo tenga en sus manos. Por lo tanto, cuando incluye todos los archivos fuente en su proyecto, básicamente no hay diferencia entre lo que ha hecho y simplemente crear un gran archivo fuente sin ninguna separación.

“Oh, eso no es gran cosa. Si funciona, está bien,” te escucho llorar. Y en cierto sentido, estarías en lo cierto. Pero en este momento está tratando con un pequeño y pequeño progtwig, y ​​una CPU agradable y relativamente libre para comstackrlo por usted. No siempre serás tan afortunado.

Si alguna vez se adentra en los reinos de la progtwigción informática seria, verá proyectos con recuentos de líneas que pueden llegar a millones, en lugar de docenas. Eso es un montón de líneas. Y si intenta comstackr uno de estos en una computadora de escritorio moderna, puede tomar una cuestión de horas en lugar de segundos.

“¡Oh no! ¡Eso suena horrible! ¿Sin embargo, puedo evitar este terrible destino?” Lamentablemente, no hay mucho que puedas hacer al respecto. Si lleva horas comstackr, lleva horas comstackr. Pero eso solo importa la primera vez; una vez que lo haya comstackdo una vez, no hay razón para comstackrlo nuevamente.

A menos que cambies algo.

Ahora, si tuviera dos millones de líneas de código fusionadas en un gigantesco gigante, y necesitara una solución de error simple como, por ejemplo, x = y + 1 , eso significa que tiene que comstackr los dos millones de líneas nuevamente para poder prueba esto Y si descubres que querías hacer una x = y - 1 lugar, de nuevo, dos millones de líneas de comstackción te están esperando. Eso es muchas horas desperdiciadas que podrían ser mejor gastadas haciendo cualquier otra cosa.

“¡Pero odio ser improductivo! ¡Si hubiera alguna forma de comstackr partes distintas de mi base de código individualmente y, de alguna manera, unirlas luego!” Una excelente idea, en teoría. Pero, ¿qué pasa si su progtwig necesita saber qué está pasando en un archivo diferente? Es imposible separar por completo su base de código, a menos que desee ejecutar una gran cantidad de pequeños archivos .exe en su lugar.

“¡Pero seguramente debe ser posible! ¡De lo contrario, la progtwigción suena a tortura pura! ¿Qué pasaría si encontrara alguna forma de separar la interfaz de la implementación ? Digamos tomando la información suficiente de estos segmentos de código para identificarlos al rest del progtwig, y ​​poniendo ellos en algún tipo de archivo de encabezado en su lugar? Y de esa manera, puedo usar la directiva de preprocesador #include para traer solo la información necesaria para comstackr! “

Hmm. Puedes estar en algo allí. Permíteme saber como funciona para tí.

Esta es probablemente una respuesta más detallada de lo que usted quería, pero creo que se justifica una explicación decente.

En C y C ++, un archivo fuente se define como una unidad de traducción . Por convención, los archivos de encabezado contienen declaraciones de funciones, definiciones de tipos y definiciones de clases. Las implementaciones de funciones reales residen en unidades de traducción, es decir, archivos .cpp.

La idea detrás de esto es que las funciones y las funciones miembro de clase / estructura se comstackn y ensamblan una vez, luego otras funciones pueden llamar a ese código desde un solo lugar sin hacer duplicados. Sus prototipos de funciones se declaran implícitamente como “extern”.

 /* Function prototype, usually found in headers. */ /* Implicitly 'extern', ie the symbols is visible everywhere, not just locally.*/ int add(int, int); /* function body, or function definition. */ int add(int a, int b) { return a + b; } 

Si desea que una función sea local para una unidad de traducción, la define como ‘estática’. ¿Qué significa esto? Significa que si incluye archivos fuente con funciones externas, obtendrá errores de redefinición, porque el comstackdor se encuentra con la misma implementación más de una vez. Entonces, quiere que todas sus unidades de traducción vean el prototipo de la función, pero no el cuerpo de la función .

Entonces, ¿cómo se purga todo junto al final? Ese es el trabajo del enlazador. Un vinculador lee todos los archivos de objetos que genera la etapa de ensamblador y resuelve los símbolos. Como dije antes, un símbolo es solo un nombre. Por ejemplo, el nombre de una variable o una función. Cuando las unidades de traducción que llaman a funciones o declaran tipos no conocen la implementación para esas funciones o tipos, se dice que esos símbolos están sin resolver. El enlazador resuelve el símbolo no resuelto conectando la unidad de traducción que contiene el símbolo indefinido junto con el que contiene la implementación. Uf. Esto es cierto para todos los símbolos visibles externamente, ya sea que estén implementados en su código o proporcionados por una biblioteca adicional. Una biblioteca es realmente solo un archivo con código reutilizable.

Hay dos excepciones notables. Primero, si tiene una función pequeña, puede hacerla en línea. Esto significa que el código máquina generado no genera una llamada de función externa, sino que está literalmente concatenado en el lugar. Como generalmente son pequeños, el tamaño de arriba no importa. Puedes imaginar que son estáticos en la forma en que funcionan. Por lo tanto, es seguro implementar funciones en línea en los encabezados. Las implementaciones de funciones dentro de una definición de clase o estructura también a menudo son inlineadas automáticamente por el comstackdor.

La otra excepción son las plantillas. Como el comstackdor necesita ver toda la definición del tipo de plantilla al instanciarlas, no es posible desacoplar la implementación de la definición como con las funciones independientes o las clases normales. Bueno, quizás esto sea posible ahora, pero obtener el respaldo generalizado del comstackdor para la palabra clave “exportar” tomó mucho, mucho tiempo. Por lo tanto, sin soporte para ‘exportar’, las unidades de traducción obtienen sus propias copias locales de funciones y tipos creados con plantillas, de forma similar a como funcionan las funciones en línea. Con soporte para ‘exportar’, este no es el caso.

Para las dos excepciones, a algunas personas les resulta “más agradable” colocar las implementaciones de las funciones en línea, las funciones con plantilla y los tipos de plantilla en archivos .cpp, y luego #incluir el archivo .cpp. Si esto es un encabezado o un archivo fuente en realidad no importa; al preprocesador no le importa y es solo una convención.

Un resumen rápido de todo el proceso desde código C ++ (varios archivos) y hasta un ejecutable final:

  • Se ejecuta el preprocesador , que analiza todas las directivas que comienzan con ‘#’. La directiva #include concatena el archivo incluido con inferior, por ejemplo. También hace macro-reemplazo y token-pasting.
  • El comstackdor real se ejecuta en el archivo de texto intermedio después de la etapa del preprocesador y emite un código de ensamblador.
  • El ensamblador se ejecuta en el archivo de ensamblaje y emite código de máquina, esto generalmente se llama un archivo de objeto y sigue el formato binario ejecutable del sistema operativo en cuestión. Por ejemplo, Windows usa el PE (formato ejecutable portátil), mientras que Linux usa el formato Unix System V ELF, con extensiones GNU. En esta etapa, los símbolos todavía están marcados como indefinidos.
  • Finalmente, se ejecuta el enlazador . Todas las etapas previas se ejecutaron en cada unidad de traducción en orden. Sin embargo, la etapa del enlazador funciona en todos los archivos de objetos generados que fueron generados por el ensamblador. El vinculador resuelve símbolos y hace mucha magia como crear secciones y segmentos, que depende de la plataforma de destino y el formato binario. Los progtwigdores no están obligados a saber esto en general, pero seguramente ayuda en algunos casos.

Una vez más, esto fue definitivamente más de lo que pediste, pero espero que los detalles esenciales te ayuden a ver el outlook completo.

La solución típica es usar archivos .h para declaraciones solamente y archivos .cpp para implementación. Si necesita reutilizar la implementación, incluya el archivo .h correspondiente en el archivo .cpp donde se utiliza la clase / función / lo que sea necesario y vincule con un archivo .cpp ya comstackdo (ya sea un archivo .obj , generalmente utilizado en un proyecto). – o archivo .lib – usualmente usado para reutilizar de proyectos múltiples). De esta forma, no necesita recomstackr todo si solo cambia la implementación.

Piense en los archivos cpp como un recuadro negro y los archivos .h como guías sobre cómo usar esos recuadros negros.

Los archivos cpp se pueden comstackr con anticipación. Esto no funciona en ti #include them, ya que necesita “incluir” el código en tu progtwig cada vez que lo comstack. Si solo incluye el encabezado, puede usar el archivo de encabezado para determinar cómo usar el archivo cpp precomstackdo.

Aunque esto no supondrá una gran diferencia para su primer proyecto, si comienza a escribir grandes progtwigs cpp, la gente lo odiará porque los tiempos de comstackción van a explotar.

También lea esto: el archivo de encabezado incluye patrones

Los archivos de encabezado generalmente contienen declaraciones de funciones / clases, mientras que los archivos .cpp contienen las implementaciones reales. En tiempo de comstackción, cada archivo .cpp se comstack en un archivo de objeto (generalmente extensión .o), y el enlazador combina los diversos archivos de objeto en el ejecutable final. El proceso de enlace generalmente es mucho más rápido que la comstackción.

Beneficios de esta separación: si está recomstackndo uno de los archivos .cpp en su proyecto, no tiene que volver a comstackr todos los demás. Usted acaba de crear el nuevo archivo de objeto para ese archivo .cpp en particular. El comstackdor no tiene que mirar los otros archivos .cpp. Sin embargo, si desea llamar funciones en su archivo .cpp actual que se implementaron en los otros archivos .cpp, tiene que decirle al comstackdor qué argumentos toman; ese es el propósito de incluir los archivos de encabezado.

Desventajas: al comstackr un archivo .cpp determinado, el comstackdor no puede “ver” qué hay dentro de los otros archivos .cpp. Por lo tanto, no sabe cómo se implementan las funciones allí y, como resultado, no puede optimizar de forma tan agresiva. Pero creo que no necesita preocuparse con eso por el momento (:

La idea básica de que los encabezados solo están incluidos y los archivos cpp solo se comstackn. Esto será más útil una vez que tenga muchos archivos cpp, y volver a comstackr toda la aplicación cuando modifique solo uno de ellos será demasiado lento. O cuando las funciones en los archivos comenzarán dependiendo de cada uno. Por lo tanto, debe separar las declaraciones de clase en los archivos de encabezado, dejar la implementación en archivos cpp y escribir un Makefile (o algo más, según las herramientas que esté usando) para comstackr los archivos cpp y vincular los archivos de objeto resultantes en un progtwig.

Si #incluye un archivo cpp en varios otros archivos en su progtwig, el comstackdor intentará comstackr el archivo cpp varias veces, y generará un error ya que habrá múltiples implementaciones de los mismos métodos.

La comstackción tardará más tiempo (lo que se convierte en un problema en proyectos grandes) si realiza ediciones en #incluidos archivos cpp, que luego fuerzan la recomstackción de cualquier archivo #incluyendo ellos.

Simplemente ponga sus declaraciones en los archivos de cabecera e inclúyalos (ya que en realidad no generan código per se), y el enlazador enlazará las declaraciones con el código cpp correspondiente (que luego solo se comstack una vez).

Si bien es posible hacer lo que hizo, la práctica estándar es colocar declaraciones compartidas en archivos de encabezado (.h) y definiciones de funciones y variables (implementación) en archivos fuente (.cpp).

Como una convención, esto ayuda a dejar en claro dónde está todo, y hace una distinción clara entre la interfaz y la implementación de sus módulos. También significa que nunca debe verificar si un archivo .cpp está incluido en otro, antes de agregarle algo que podría romperse si se definió en varias unidades diferentes.

reutilización, architecture y encapsulación de datos

he aquí un ejemplo:

Supongamos que crea un archivo cpp que contiene una forma simple de rutinas de cadena, todo en una clase mystring, coloca la clase decl para esto en un mystring.h comstackndo mystring.cpp en un archivo .obj

ahora en su progtwig principal (por ej. main.cpp) incluye encabezado y enlace con mystring.obj. para usar mystring en su progtwig no le importan los detalles de cómo se implementa mystring ya que el encabezado dice lo que puede hacer

ahora, si un amigo quiere usar tu clase de mystring, le das mystring.hy mystring.obj, pero no necesariamente necesita saber cómo funciona, siempre y cuando funcione.

más adelante, si tiene más archivos .obj de este tipo, puede combinarlos en un archivo .lib y vincularlos en su lugar.

también puede decidir cambiar el archivo mystring.cpp e implementarlo de manera más efectiva, esto no afectará su main.cpp o su progtwig de amigos.

Si funciona para usted, entonces no tiene nada de malo, excepto que arruinará las plumas de las personas que piensan que solo hay una forma de hacer las cosas.

Muchas de las respuestas dadas aquí abordan las optimizaciones para proyectos de software a gran escala. Estas son buenas cosas que debe saber, pero no tiene sentido optimizar un proyecto pequeño como si fuera un proyecto grande, es lo que se conoce como “optimización prematura”. Dependiendo de su entorno de desarrollo, puede haber una complejidad extra significativa involucrada en la configuración de una comstackción para admitir múltiples archivos de origen por progtwig.

Si, con el tiempo, su proyecto evoluciona y descubre que el proceso de construcción tarda demasiado, entonces puede refactorizar su código para usar múltiples archivos fuente para comstackciones incrementales más rápidas.

Varias de las respuestas discuten la separación de la interfaz de la implementación. Sin embargo, esta no es una característica inherente de los archivos de inclusión, y es bastante común # incluir archivos de “encabezado” que incorporan directamente su implementación (incluso la Biblioteca estándar de C ++ hace esto en un grado significativo).

Lo único realmente “poco convencional” sobre lo que ha hecho es nombrar sus archivos incluidos “.cpp” en lugar de “.h” o “.hpp”.

Cuando comstack y vincula un progtwig, el comstackdor primero comstack los archivos cpp individuales y luego los vincula (conecta). Los encabezados nunca se comstackrán, a menos que estén incluidos en un archivo cpp primero.

Normalmente, los encabezados son declaraciones y cpp son archivos de implementación. En los encabezados define una interfaz para una clase o función, pero omite cómo implementa realmente los detalles. De esta manera no tiene que volver a comstackr cada archivo cpp si realiza un cambio en uno.

Le sugiero que vaya a Diseño de software de C ++ a gran escala por John Lakos . En la universidad, generalmente escribimos pequeños proyectos en los que no encontramos esos problemas. El libro destaca la importancia de separar las interfaces y las implementaciones.

Los archivos de encabezado generalmente tienen interfaces que se supone que no se deben cambiar con tanta frecuencia. Del mismo modo, una mirada a patrones como el lenguaje de Virtual Constructor te ayudará a comprender más el concepto.

Todavía estoy aprendiendo como tú 🙂

Es como escribir un libro, desea imprimir capítulos completos solo una vez

Digamos que estás escribiendo un libro. Si coloca los capítulos en archivos separados, solo necesita imprimir un capítulo si lo ha cambiado. Trabajar en un capítulo no cambia ninguno de los otros.

Pero incluir los archivos cpp es, desde el punto de vista del comstackdor, como editar todos los capítulos del libro en un solo archivo. Luego, si lo cambia, debe imprimir todas las páginas del libro completo para que se imprima el capítulo revisado. No existe la opción “imprimir páginas seleccionadas” en la generación de código objeto.

Volver al software: tengo a Linux y Ruby src por ahí. Una medida aproximada de líneas de código …

  Linux Ruby 100,000 100,000 core functionality (just kernel/*, ruby top level dir) 10,000,000 200,000 everything 

Cualquiera de esas cuatro categorías tiene mucho código, de ahí la necesidad de modularidad. Este tipo de código base es sorprendentemente típico de los sistemas del mundo real.