1. Modo de compilación de C++

Normalmente, un programa en C++ contiene solo dos tipos de archivos: archivos .cpp y archivos .h. Los archivos .cpp se denominan archivos fuente de C++ y contienen código fuente de C++; los archivos .h se denominan archivos de cabecera de C++ y también contienen código fuente de C++.

El lenguaje C++ admite la "compilación por separado" (separate compilation). Es decir, todo el contenido de un programa se puede dividir en diferentes partes y colocarlas en diferentes archivos .cpp. Lo que hay en un archivo .cpp es relativamente independiente; durante la compilación no necesita comunicarse con otros archivos, solo necesita, después de compilarse en archivos objeto, realizar un enlazado (link) con los otros archivos objeto. Por ejemplo, en el archivo a.cpp se define una función global "void a(){}", y en el archivo b.cpp se necesita llamar a esta función. Aun así, los archivos a.cpp y b.cpp no necesitan saber de la existencia del otro, sino que pueden compilarse por separado y, después de compilarlos en archivos objeto, enlazarlos, y todo el programa puede ejecutarse.

¿Cómo se logra esto? Desde la perspectiva de escribir el programa, es muy sencillo. En el archivo b.cpp, antes de llamar a la función "void a()", basta con declarar la función "voida();". Esto se debe a que el compilador, al compilar b.cpp, genera una tabla de símbolos (symbol table); símbolos como "void a()" cuya definición no se ve se guardan en esa tabla. Al realizar el enlazado, el compilador busca la definición de ese símbolo en otros archivos objeto. Una vez que la encuentra, el programa puede generarse sin problemas.

Obsérvese que aquí se mencionan dos conceptos: "definición" y "declaración". En términos simples, "definición" es describir completamente un símbolo: si es una variable o una función, qué tipo devuelve, qué parámetros necesita, etc. "Declaración" solo declara la existencia de ese símbolo, es decir, le dice al compilador: este símbolo está definido en otro archivo; lo uso aquí por ahora, y cuando enlaces, ve a buscarlo a otro lado para ver qué es realmente. Al definir, se debe definir completamente un símbolo (variable o función) según la sintaxis de C++, mientras que al declarar solo se necesita escribir el prototipo de ese símbolo. Cabe señalar que un símbolo puede declararse varias veces en todo el programa, pero solo debe definirse una vez. Imaginemos: si un símbolo tiene dos definiciones diferentes, ¿a cuál debe hacer caso el compilador?Este mecanismo aporta muchos beneficios a los programadores de C++, y también da lugar a un método de escritura de programas. Consideremos: si hay una función muy usada "void f() {}", que es llamada en muchos archivos .cpp de todo el programa, entonces solo necesitamos definir esta función en un archivo y declararla en los demás. Una sola función no es problema, declararla es solo una frase. Pero si hay muchas funciones, por ejemplo un montón de funciones matemáticas, varios cientos, ¿qué se hace? ¿Se puede garantizar que cada programador pueda recordar y escribir con exactitud las formas de todas las funciones?

2. Qué es un archivo de cabecera

Obviamente, la respuesta es imposible. Pero hay un método muy sencillo que puede ayudar a los programadores a ahorrarse la molestia de recordar tantos prototipos de funciones: podemos escribir de antemano todas las declaraciones de esos cientos de funciones, colocarlas en un archivo, y cuando el programador las necesite, copiarlas todas en su código fuente.

Este método es factible, pero sigue siendo demasiado engorroso y además resulta torpe. Así que el archivo de cabecera puede desempeñar su papel. El llamado archivo de cabecera, en realidad, tiene el mismo contenido que un archivo .cpp: ambos son código fuente de C++. Pero el archivo de cabecera no necesita compilarse. Ponemos todas las declaraciones de funciones en un archivo de cabecera; cuando un archivo fuente .cpp las necesita, pueden incluirse en ese archivo .cpp mediante un macrocomando "#include", fusionando así su contenido en el archivo .cpp. Cuando el archivo .cpp se compila, los archivos .h incluidos ejercen su función.

Pongamos un ejemplo: supongamos que todas las funciones matemáticas son solo dos: f1 y f2. Entonces colocamos sus definiciones en math.cpp:

/* math.cpp */ double f1() { //do something here.... return; } double f2(double a) { //do something here... return a * a; } /* end of math.cpp */

Y colocamos las declaraciones de "estas" funciones en un archivo de cabecera math.h:

/* math.h */ double f1(); double f2(double); /* end of math.h */

En otro archivo, main.cpp, si quiero llamar a estas dos funciones, solo necesito incluir el archivo de cabecera:

/* main.cpp */ #include "math.h" main() { int number1 = f1(); int number2 = f2(number1); } /* end of main.cpp */

Así se tiene un programa completo. Cabe señalar que el archivo .h no necesita escribirse después del comando del compilador, pero debe estar en un lugar donde el compilador pueda encontrarlo (por ejemplo, en el mismo directorio que main.cpp). Tanto main.cpp como math.cpp pueden compilarse por separado, generando main.o y math.o; luego se enlazan estos dos archivos objeto y el programa puede ejecutarse.

3. #include

#include es un macrocomando proveniente del lenguaje C, que actúa antes de que el compilador compile, es decir, durante la precompilación. La función de #include es incluir el contenido del archivo que se escribe a continuación, completa y sin cambiar ni una palabra, en el archivo actual. Vale la pena mencionar que no tiene ninguna otra función o efecto secundario; su única función es reemplazar cada lugar donde aparece por el contenido del archivo que se indica a continuación. Es una simple sustitución de texto, nada más. Por lo tanto, la primera línea del archivo main.cpp (#include"math.h") se reemplazará por el contenido del archivo math.h antes de la compilación. Es decir, cuando el proceso de compilación está a punto de comenzar, el contenido de main.cpp ya ha cambiado:

/* ~main.cpp */ double f1(); double f2(double); main() { int number1 = f1(); int number2 = f2(number1); } /* end of ~main.cpp */

Ni más ni menos, justo lo necesario. De manera análoga, si además de main.cpp hay muchos otros archivos .cpp que también usan las funciones f1 y f2, todos ellos solo necesitan escribir una línea #include "math.h" antes de usar estas dos funciones.


4. Qué se debe escribir en un archivo de cabecera

A través de la discusión anterior, podemos entender que el papel del archivo de cabecera es ser incluido por otros archivos .cpp. Ellos mismos no participan en la compilación, pero en realidad su contenido se compila en múltiples archivos .cpp. Con la regla de "una sola definición", es fácil deducir que en el archivo de cabecera solo deben colocarse declaraciones de variables y funciones, no sus definiciones. Porque el contenido de un archivo de cabecera se introduce en varios archivos .cpp diferentes, y todos ellos se compilan. Poner declaraciones no es problema; si se pone una definición, equivale a que aparezcan definiciones de un símbolo (variable o función) en varios archivos. Aunque estas definiciones sean idénticas, para el compilador no es legal.

Por lo tanto, un punto que se debe recordar es que en los archivos de cabecera .h solo pueden existir declaraciones de variables o funciones, y no definiciones. Es decir, solo se deben escribir oraciones como: extern int a; y void f();. Estas son declaraciones. Si se escribe una oración como inta; o void f() {}, en cuanto el archivo de cabecera sea incluido por dos o más archivos .cpp, el compilador dará un error de inmediato. (Sobre extern, ya se ha discutido antes; aquí no se repetirá la diferencia entre definición y declaración.)

Sin embargo, esta regla tiene tres excepciones:

  • Primero, en un archivo de cabecera se pueden escribir definiciones de objetos const. Porque los objetos const globales no tienen declaración extern por defecto, solo son válidos en el archivo actual. Si se escribe un objeto de este tipo en un archivo de cabecera, incluso si es incluido en varios archivos .cpp, el objeto solo es válido en el archivo que lo incluye y es invisible para otros archivos, por lo que no causa definiciones múltiples. Al mismo tiempo, como este objeto en esos archivos .cpp proviene del mismo archivo de cabecera, se garantiza que el valor del objeto const en todos esos archivos .cpp sea el mismo; se matan dos pájaros de un tiro. De manera similar, las definiciones de objetos static también se pueden colocar en un archivo de cabecera.
  • Segundo, en un archivo de cabecera se pueden escribir definiciones de funciones inline. Porque las funciones inline requieren que el compilador las expanda en línea en el lugar donde las encuentra, según su definición; no son como las funciones normales que pueden declararse primero y enlazarse después (las funciones inline no se enlazan). Por lo tanto, el compilador necesita ver la definición completa de la función inline en tiempo de compilación. Si las funciones inline, como las funciones normales, solo pudieran definirse una vez, el asunto sería difícil. Porque dentro de un archivo aún está bien: puedo escribir la definición de la función inline al principio, lo que garantiza que cada uso posterior vea la definición. Pero ¿qué pasa si también uso esta función en otros archivos? Casi no hay una buena solución para esto, por lo que C++ establece que una función inline puede definirse varias veces en un programa, siempre que la función inline aparezca solo una vez en un archivo .cpp y que su definición sea la misma en todos los archivos .cpp; entonces puede compilarse. Evidentemente, poner la definición de la función inline en un archivo de cabecera es una decisión muy sensata.
  • Tercero, en el archivo de cabecera se puede escribir la definición de una clase (class). Porque al crear un objeto de una clase en un programa, el compilador solo puede saber cómo debe distribuirse el objeto de esa clase si la definición de la clase es completamente visible. Por lo tanto, el requisito sobre la definición de una clase es básicamente el mismo que el de las funciones inline. Así que poner la definición de la clase en el archivo de cabecera e incluir ese archivo de cabecera en el archivo .cpp que usa la clase es una buena práctica. Aquí vale la pena mencionar que la definición de una clase contiene miembros de datos y funciones miembro. Los miembros de datos solo se definen (se les asigna espacio) cuando se crea el objeto concreto, pero las funciones miembro deben definirse desde el principio; esto es lo que normalmente llamamos la implementación de la clase. Generalmente, nuestro enfoque es poner la definición de la clase en el archivo de cabecera y poner el código de implementación de las funciones miembro en un archivo .cpp. Esto es válido y es un buen método. Sin embargo, hay otro método: escribir directamente el código de implementación de las funciones miembro dentro de la definición de la clase. En las clases de C++, si una función miembro se define dentro del cuerpo de la definición de la clase, el compilador la considera una función inline. Por lo tanto, es legal escribir la definición de la función miembro dentro del cuerpo de la definición de la clase y ponerla junto con el archivo de cabecera. Ten en cuenta que si la definición de la función miembro se escribe en el archivo de cabecera de la definición de la clase, pero no dentro de la definición de la clase, no es legal, porque entonces esa función miembro ya no es inline. Una vez que el archivo de cabecera es incluido por dos o más archivos .cpp, esa función miembro queda redefinida.

5. Medidas de protección en los archivos de cabecera

Considera esto: si el archivo de cabecera solo contiene declaraciones, puede ser incluido varias veces por el mismo archivo .cpp sin problema, porque la aparición de declaraciones no está restringida. Sin embargo, las tres excepciones en archivos de cabecera mencionadas anteriormente también son un uso muy común de los archivos de cabecera. Entonces, si en un archivo de cabecera aparece cualquiera de las tres excepciones anteriores y es incluido varias veces por un .cpp, el problema es grave. Porque aunque los elementos sintácticos en estas tres excepciones "pueden definirse en múltiples archivos fuente", "solo pueden aparecer una vez en un archivo fuente". Imagina que a.h contiene la definición de la clase A y b.h contiene la definición de la clase B. Como la definición de la clase B depende de la clase A, b.h también hace #include de a.h. Ahora hay un archivo fuente que usa tanto la clase A como la clase B, así que el programador incluye tanto a.h como b.h en ese archivo fuente. En este momento, surge el problema: ¡la definición de la clase A aparece dos veces en ese archivo fuente! Entonces todo el programa no puede compilar. Podrías pensar que es un error del programador: debería saber que b.h incluye a.h, pero en realidad no debería tener que saberlo.

Usar "#define" junto con la compilación condicional puede resolver bien este problema. En un archivo de cabecera, se define un nombre mediante #define, y mediante la compilación condicional #ifndef...#endif se hace que el compilador decida si continuar compilando el contenido posterior del archivo de cabecera según si ese nombre está definido o no. Este método es simple, pero recuerda siempre incluirlo al escribir archivos de cabecera.


Diferencias entre archivos de cabecera y archivos fuente en C++

1. Cómo se asocia un archivo fuente con un archivo de cabecera mediante #include

  • 1. Los archivos de cabecera del sistema se escriben entre corchetes angulares, de modo que el compilador los busca en el directorio de archivos del sistema.
  • 2. Los archivos definidos por el usuario se escriben entre comillas dobles. El compilador primero los busca en el directorio del usuario, luego en el directorio de instalación de C++ (por ejemplo, en VC se puede especificar y modificar la ruta de búsqueda de archivos de biblioteca; en Unix y Linux se puede configurar mediante variables de entorno) y finalmente en los archivos del sistema.

#include "xxx.h" (siempre pensé que "" y <> no tenían diferencia, pero tinyxml.h es un archivo que no es del sistema, por lo que hay que usar "")

2. Cómo se asocia un archivo de cabecera con un archivo fuente

Este problema en realidad dice: dado que el archivo de cabecera "a.h" declara una serie de funciones, y "b.cpp" implementa esas funciones, si quiero usar en "c.cpp" esas funciones declaradas en "a.h" e implementadas en "b.cpp", normalmente uso #include "a.h" en "c.cpp". Entonces, ¿cómo encuentra c.cpp la implementación en b.cpp?

En realidad, los nombres de archivo .cpp y .h no tienen ninguna relación directa; muchos compiladores aceptan otras extensiones. Por ejemplo, ahora veo el código fuente de nuestra empresa: los archivos .cpp han sido reemplazados por archivos .cc.

En Turbo C, la compilación se realiza mediante la línea de comandos; el parámetro de la línea de comandos es el nombre del archivo, por defecto .cpp y .h, pero también se puede personalizar como .xxx, etc.

En el libro "Programación en C" del profesor Tan Haoqiang se menciona que durante el preprocesamiento del compilador, el comando #include debe someterse a un "procesamiento de inclusión de archivos": copiar todo el contenido de file2.c en el lugar de #include "file2.c". Esto también explica por qué a muchos compiladores no les importa cuál sea la extensión del archivo, porque el preprocesamiento de #include realiza un trabajo de "copiar e insertar código".

Al compilar, no se busca la implementación de las funciones en el archivo b.cpp; eso solo se hace en el enlazado (link). Usar #include "a.h" en b.cpp o c.cpp es en realidad introducir las declaraciones correspondientes para que la compilación pueda pasar; el programa no se preocupa de dónde está la implementación ni de cómo está implementada. Después de compilar el archivo fuente se genera un archivo objeto (archivo .o o .obj). En el archivo objeto, esas funciones y variables se consideran símbolos. En el enlazado, es necesario indicar en el makefile qué archivo .o o .obj se debe enlazar (aquí, el archivo .o o .obj generado por b.cpp). En ese momento, el enlazador va a ese archivo .o o .obj a buscar las funciones implementadas en b.cpp y las construye (build) en el archivo ejecutable especificado en el makefile.

En Unix, incluso se puede no incluir el archivo de cabecera en el archivo fuente; solo hay que indicarlo en el makefile (aunque esto reduce considerablemente la legibilidad del programa, es un mal hábito ^_^). En VC, en general no es necesario escribir el makefile uno mismo; solo hay que incluir todos los archivos necesarios en el proyecto, y VC se encargará automáticamente de escribir el makefile.

Normalmente, el compilador de C++ busca el símbolo necesario en todos los archivos .o o .obj, no solo en un archivo determinado ni se detiene al encontrarlo en uno. Por lo tanto, si la misma función está implementada en varios archivos diferentes, o se define la misma variable global, al enlazar se mostrará el aviso "redefined".

En resumen

El archivo .h puede contener:

  • Declaraciones de datos miembro de la clase, pero no asignaciones.
  • Definición y asignación de miembros de datos estáticos de la clase, pero no se recomienda; basta con una declaración.
  • Declaraciones de funciones miembro de la clase.
  • Declaraciones de funciones que no son miembros de la clase.
  • Definición de constantes, como: constint a=5;
  • Definiciones de funciones estáticas.
  • Definiciones de funciones inline de la clase.

No puede contener:

  • 1. Declaraciones de todas las variables no estáticas (que no sean miembros de datos de la clase).
  • 2. Las declaraciones de espacios de nombres por defecto no deben colocarse en el archivo de cabecera; "using namespace std;" etc. debe colocarse en el .cpp. En el archivo .h se debe usar std::string.

Dirección original: https://blog.csdn.net/qq_35038153/article/details/71293265