Así que terminé mi primera tarea de programación en C ++ y recibí mi calificación. Pero según la clasificación, perdí marcas por including cpp files instead of compiling and linking them
. No tengo muy claro qué significa eso.
Echando un vistazo a mi código, decidí 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 calificador quiso decir que escribí '#include "mycppfile.cpp";' en algunos de mis archivos
Mi razonamiento para #include
incluir los archivos cpp fue: - Todo lo que se suponía que debía ir al archivo de encabezado estaba en mi archivo cpp, por lo que fingí que era como un archivo de encabezado. los archivos de encabezado estaban #include
en los archivos, así que hice lo mismo para mi archivo cpp.
Entonces, ¿qué hice mal exactamente y por qué es malo?
fuente
Respuestas:
Que yo sepa, el estándar C ++ no conoce la diferencia entre los archivos de encabezado y los archivos de origen. En lo que respecta al idioma, 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 programa eliminará prácticamente cualquier ventaja que hubiera tenido de separar sus archivos fuente en primer lugar.
Esencialmente, lo que
#include
hace es decirle al preprocesador que tome todo el archivo que ha especificado y lo copie en su archivo activo antes de que el compilador lo tenga en sus manos. Por lo tanto, cuando incluye todos los archivos de origen en su proyecto juntos, básicamente no hay diferencia entre lo que ha hecho, y solo hacer un gran archivo de origen 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 correcto. Pero en este momento se trata de un pequeño programa muy pequeño y una CPU agradable y relativamente libre de obstáculos para compilarlo por usted. No siempre tendrás tanta suerte.
Si alguna vez profundiza en los ámbitos de la programación informática seria, verá proyectos con recuentos de líneas que pueden llegar a millones, en lugar de docenas. Eso son muchas líneas. Y si intentas compilar uno de estos en una computadora de escritorio moderna, puede tomar algunas horas en lugar de segundos.
"¡Oh, no! ¡Eso suena horrible! Sin embargo, ¿puedo evitar este terrible destino?" Desafortunadamente, no hay mucho que puedas hacer al respecto. Si lleva horas compilar, lleva horas compilar. Pero eso solo importa la primera vez: una vez que lo ha compilado una vez, no hay razón para compilarlo nuevamente.
A menos que cambies algo.
Ahora, si tenía dos millones de líneas de código fusionadas en un gigante gigante, y necesita hacer una solución simple de error, como, por ejemplo
x = y + 1
, eso significa que debe compilar los dos millones de líneas nuevamente para probar esto. Y si descubre que tenía la intención de hacer unax = y - 1
en su lugar, nuevamente, le esperan dos millones de líneas de compilación. Son muchas horas de tiempo desperdiciadas que podrían gastarse mejor haciendo cualquier otra cosa"¡Pero odio ser improductivo! ¡Ojalá hubiera alguna forma de compilar partes distintas de mi base de código individualmente, y de alguna manera vincularlas después!" Una excelente idea, en teoría. Pero, ¿qué pasa si su programa necesita saber qué está pasando en un archivo diferente? Es imposible separar completamente su base de código a menos que desee ejecutar un montón de pequeños archivos .exe pequeños.
"¡Pero seguramente debe ser posible! ¡La programación suena como pura tortura de otra manera! ¿Qué pasa si encuentro alguna forma de separar la interfaz de la implementación ? Digamos tomando la información suficiente de estos segmentos de código distintos para identificarlos en el resto del programa, y colocando en lugar de eso, ¿ algún tipo de archivo de encabezado ? Y de esa manera, ¡puedo usar la
#include
directiva de preprocesador para traer solo la información necesaria para compilar! "Hmm Puede que tengas algo allí. Permíteme saber como funciona para tí.
fuente
Esta es probablemente una respuesta más detallada de lo que quería, pero creo que una explicación decente está justificada.
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 tipo y definiciones de clase. 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 compilan y ensamblan una vez, luego otras funciones pueden llamar a ese código desde un lugar sin hacer duplicados. Sus funciones se declaran como "externas" implícitamente.
Si desea que una función sea local para una unidad de traducción, debe definirla como 'estática'. ¿Qué significa esto? Significa que si incluye archivos de origen con funciones externas, obtendrá errores de redefinición, porque el compilador se encuentra con la misma implementación más de una vez. Por lo tanto, desea que todas sus unidades de traducción vean la declaración de la función pero no el cuerpo de la función .
Entonces, ¿cómo se mezcla todo al final? Ese es el trabajo del enlazador. Un vinculador lee todos los archivos de objetos generados por 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 funciones o declaran tipos no conocen la implementación de esas funciones o tipos, se dice que esos símbolos no están resueltos. 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 desde el exterior, 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 de máquina generado no genera una llamada de función externa, sino que literalmente se concatena in situ. Como generalmente son pequeños, el tamaño de la sobrecarga no importa. Puedes imaginar que sean 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 son incorporadas automáticamente por el compilador.
La otra excepción son las plantillas. Dado que el compilador necesita ver la definición completa del tipo de plantilla al instanciarlos, no es posible desacoplar la implementación de la definición como con funciones independientes o clases normales. Bueno, tal vez esto sea posible ahora, pero obtener un amplio soporte del compilador para la palabra clave "exportar" tomó mucho, mucho tiempo. Entonces, sin soporte para 'exportar', las unidades de traducción obtienen sus propias copias locales de tipos y funciones con plantilla instanciadas, de forma similar a cómo funcionan las funciones en línea. Con soporte para 'exportar', este no es el caso.
Para las dos excepciones, algunas personas encuentran "más agradable" poner las implementaciones de funciones en línea, funciones con plantillas y tipos con plantillas en archivos .cpp y luego #incluir el archivo .cpp. Si esto es un encabezado o un archivo fuente realmente no importa; Al preprocesador no le importa y es solo una convención.
Un resumen rápido de todo el proceso desde el código C ++ (varios archivos) hasta un ejecutable final:
Nuevamente, esto fue definitivamente más de lo que pediste, pero espero que los detalles esenciales te ayuden a ver la imagen más grande.
fuente
int add(int, int);
Es una declaración de función . La parte prototipo es justaint, int
. Sin embargo, todas las funciones en C ++ tienen un prototipo, por lo que el término realmente solo tiene sentido en C. He editado su respuesta a este efecto.export
para plantillas se ha eliminado del lenguaje en 2011. Nunca fue realmente compatible con los compiladores.La solución típica es usar
.h
archivos solo para declaraciones y.cpp
archivos para implementación. Si necesita reutilizar la implementación, incluya el.h
archivo correspondiente en el.cpp
archivo donde se usa la clase / función / lo que sea necesario y enlace con un.cpp
archivo ya compilado (ya sea un.obj
archivo, generalmente usado dentro de un proyecto) o un archivo .lib, generalmente usado para reutilizar desde múltiples proyectos). De esta manera, no necesita recompilar todo si solo cambia la implementación.fuente
Piense en los archivos cpp como un cuadro negro y los archivos .h como guías sobre cómo usar esos cuadros negros.
Los archivos cpp pueden compilarse con anticipación. Esto no funciona en usted # inclúyalos, ya que debe "incluir" el código en su programa cada vez que lo compila. Si solo incluye el encabezado, puede usar el archivo de encabezado para determinar cómo usar el archivo cpp precompilado.
Aunque esto no hará una gran diferencia para su primer proyecto, si comienza a escribir grandes programas cpp, la gente lo odiará porque los tiempos de compilación van a explotar.
También lea esto: el archivo de encabezado incluye patrones
fuente
Los archivos de encabezado generalmente contienen declaraciones de funciones / clases, mientras que los archivos .cpp contienen las implementaciones reales. En el momento de la compilación, cada archivo .cpp se compila en un archivo de objeto (generalmente extensión .o), y el vinculador combina los diversos archivos de objeto en el ejecutable final. El proceso de vinculación es generalmente mucho más rápido que la compilación.
Beneficios de esta separación: si está volviendo a compilar uno de los archivos .cpp en su proyecto, no tiene que volver a compilar todos los demás. Simplemente cree el nuevo archivo de objeto para ese archivo .cpp en particular. El compilador no tiene que mirar los otros archivos .cpp. Sin embargo, si desea llamar a funciones en su archivo .cpp actual que se implementaron en los otros archivos .cpp, debe decirle al compilador qué argumentos toman; ese es el propósito de incluir los archivos de encabezado.
Desventajas: al compilar un archivo .cpp determinado, el compilador no puede "ver" lo que 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 tan agresivamente. Pero creo que todavía no necesita preocuparse por eso (:
fuente
La idea básica de que los encabezados solo se incluyen y los archivos cpp solo se compilan. Esto será más útil una vez que tenga muchos archivos cpp, y volver a compilar toda la aplicación cuando modifique solo uno de ellos será demasiado lento. O cuando las funciones en los archivos comenzarán dependiendo una de la otra. Por lo tanto, debe separar las declaraciones de clase en sus archivos de encabezado, dejar la implementación en archivos cpp y escribir un Makefile (u otra cosa, dependiendo de las herramientas que esté utilizando) para compilar los archivos cpp y vincular los archivos de objetos resultantes en un programa.
fuente
Si #incluye un archivo cpp en varios otros archivos de su programa, el compilador intentará compilar el archivo cpp varias veces y generará un error, ya que habrá múltiples implementaciones de los mismos métodos.
La compilación tomará más tiempo (lo que se convierte en un problema en proyectos grandes), si realiza ediciones en # archivos incluidos de cpp, que luego fuerzan la recompilación de cualquier archivo # incluyéndolos.
Simplemente coloque sus declaraciones en archivos de encabezado e inclúyalas (ya que en realidad no generan código per se), y el vinculador conectará las declaraciones con el código cpp correspondiente (que luego solo se compila una vez).
fuente
Si bien es ciertamente 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 de origen (.cpp).
Como convención, esto ayuda a aclarar dónde está todo y hace una clara distinción entre la interfaz y la implementación de sus módulos. También significa que nunca tendrá que verificar si un archivo .cpp está incluido en otro, antes de agregarle algo que podría romperse si se definiera en varias unidades diferentes.
fuente
reutilización, arquitectura y encapsulación de datos
Aquí hay 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 compilando mystring.cpp en un archivo .obj
ahora en su programa principal (por ejemplo, main.cpp) incluye encabezado y enlace con mystring.obj. para usar mystring en su programa 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.h y mystring.obj, él tampoco necesariamente necesita saber cómo funciona mientras funcione.
más tarde, si tiene más archivos .obj, puede combinarlos en un archivo .lib y vincularlos a ellos.
También puede decidir cambiar el archivo mystring.cpp e implementarlo de manera más efectiva, esto no afectará su main.cpp o su programa de amigos.
fuente
Si funciona para usted, entonces no tiene nada de malo, excepto que revolverá las plumas de las personas que piensan que solo hay una manera de hacer las cosas.
Muchas de las respuestas dadas aquí abordan optimizaciones para proyectos de software a gran escala. Estas son cosas buenas que debe saber, pero no tiene sentido optimizar un proyecto pequeño como si fuera un proyecto grande, eso es lo que se conoce como "optimización prematura". Dependiendo de su entorno de desarrollo, puede haber una complejidad adicional significativa involucrada en la configuración de una configuración de compilación para admitir múltiples archivos fuente por programa.
Si, con el tiempo, su proyecto se desarrolla y se encuentran que el proceso de construcción está tomando demasiado tiempo, entonces se puede refactorizar el código para utilizar varios archivos de origen para construye más rápido incremento.
Varias de las respuestas analizan 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 ++ lo hace en un grado significativo).
Lo único realmente "poco convencional" sobre lo que ha hecho fue nombrar sus archivos incluidos ".cpp" en lugar de ".h" o ".hpp".
fuente
Cuando compila y vincula un programa, el compilador primero compila los archivos cpp individuales y luego los vincula (conecta). Los encabezados nunca se compilarán, a menos que se incluyan primero en un archivo cpp.
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 los detalles. De esta manera, no tiene que volver a compilar todos los archivos cpp si realiza un cambio en uno.
fuente
Le sugeriré que siga el Diseño de software C ++ a gran escala de John Lakos . En la universidad, usualmente escribimos pequeños proyectos en los que no encontramos tales 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 deben cambiarse con tanta frecuencia. Del mismo modo, una mirada a patrones como la expresión de Virtual Constructor lo ayudará a comprender el concepto más a fondo.
Todavía estoy aprendiendo como tú :)
fuente
Es como escribir un libro, desea imprimir capítulos terminados 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 compilador, como editar todos los capítulos del libro en un solo archivo. Luego, si lo cambia, debe imprimir todas las páginas de todo el libro para poder imprimir su capítulo revisado. No existe la opción "imprimir páginas seleccionadas" en la generación de código objeto.
Volver al software: tengo Linux y Ruby src por ahí. Una medida aproximada de líneas de código ...
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.
fuente