¿Por qué usar #ifndef CLASS_H y #define CLASS_H en el archivo .h pero no en .cpp?

137

Siempre he visto gente escribir

clase.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

La pregunta es, ¿por qué no hacen eso también para el archivo .cpp que contiene definiciones para funciones de clase?

Digamos que tengo main.cpp, e main.cppincluye class.h. El class.harchivo no contiene includenada, entonces, ¿cómo main.cppsabe qué hay en el archivo class.cpp?

usuario385261
fuente
55
"importar" probablemente no sea la palabra que quiere usar aquí. Incluir.
Kate Gregory
55
En C ++, no hay correlación 1 a 1 entre archivos y clases. Puede poner tantas clases en un archivo como desee (o incluso dividir una clase en varios archivos, aunque esto rara vez es útil). Por lo tanto, la macro debería ser FILE_H, no CLASS_H.
sbi

Respuestas:

304

Primero, para abordar su primera consulta:

Cuando vea esto en el archivo .h :

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif

Esta es una técnica de preprocesador para evitar que un archivo de encabezado se incluya varias veces, lo que puede ser problemático por varias razones. Durante la compilación de su proyecto, cada archivo .cpp (generalmente) se compila. En términos simples, esto significa que el compilador tomará su archivo .cpp , abrirá todos los archivos #included, los concatenará en un solo archivo de texto masivo y luego realizará un análisis de sintaxis y finalmente lo convertirá en algún código intermedio, optimizará / realizará otro tareas, y finalmente generar la salida del ensamblaje para la arquitectura de destino. Debido a esto, si un archivo está #includedvarias veces bajo un .cpparchivo, el compilador agregará su contenido de archivo dos veces, por lo que si hay definiciones dentro de ese archivo, recibirá un error del compilador que le indica que redefinió una variable. Cuando el archivo es procesado por el paso del preprocesador en el proceso de compilación, la primera vez que se alcanza su contenido, las dos primeras líneas verificarán siFILE_H se ha definido para el preprocesador. De lo contrario, definirá FILE_Hy continuará procesando el código entre este y la #endifdirectiva. La próxima vez que el preprocesador vea el contenido de ese archivo, la verificación contra FILE_Hserá falsa, por lo que se escaneará inmediatamente #endify continuará después. Esto evita errores de redefinición.

Y para abordar su segunda preocupación:

En la programación en C ++ como práctica general, separamos el desarrollo en dos tipos de archivos. Uno es con una extensión de .h y lo llamamos un "archivo de encabezado". Por lo general, proporcionan una declaración de funciones, clases, estructuras, variables globales, typedefs, preprocesamiento de macros y definiciones, etc. Básicamente, solo le brindan información sobre su código. Luego tenemos la extensión .cpp que llamamos un "archivo de código". Esto proporcionará definiciones para esas funciones, miembros de clase, cualquier miembro de estructura que necesite definiciones, variables globales, etc. Por lo tanto, el archivo .h declara el código, y el archivo .cpp implementa esa declaración. Por esta razón, generalmente durante la compilación compilamos cada .cpparchivo en un objeto y luego vincular esos objetos (porque casi nunca ve un archivo .cpp incluye otro archivo .cpp ).

Cómo se resuelven estos elementos externos es un trabajo para el vinculador. Cuando su compilador procesa main.cpp , obtiene declaraciones para el código en class.cpp al incluir class.h . Solo necesita saber cómo se ven estas funciones o variables (que es lo que le da una declaración). Por lo tanto, compila su archivo main.cpp en algún archivo de objeto ( llámelo main.obj ). Del mismo modo, class.cpp se compila en un class.objexpediente. Para producir el ejecutable final, se invoca un vinculador para vincular esos dos archivos de objetos. Para cualquier variable o función externa no resuelta, el compilador colocará un código auxiliar donde ocurre el acceso. El enlazador luego tomará este código auxiliar y buscará el código o la variable en otro archivo de objeto listado, y si se encuentra, combina el código de los dos archivos de objeto en un archivo de salida y reemplaza el código auxiliar con la ubicación final de la función o variable. De esta manera, su código en main.cpp puede invocar funciones y usar variables en class.cpp SI Y SOLO SI SE DECLARAN EN class.h .

Espero que esto haya sido útil.

Justin Summerlin
fuente
Intenté entender .h y .cpp durante los últimos días. Esta respuesta me ahorró tiempo e interés para aprender C ++. Bien escrito. Gracias justin!
Rajkumar R
realmente lo explicaste genial! Quizás la respuesta sería bastante buena si fuera con imágenes
alamin
13

El CLASS_Hes un guardia incluido ; se usa para evitar que el mismo archivo de encabezado se incluya varias veces (a través de diferentes rutas) dentro del mismo archivo CPP (o, más exactamente, la misma unidad de traducción ), lo que conduciría a errores de definición múltiple.

No se necesitan incluir protectores en los archivos CPP porque, por definición, el contenido del archivo CPP solo se lee una vez.

Parece haber interpretado que los guardias de inclusión tienen la misma función que las importdeclaraciones en otros lenguajes (como Java); ese no es el caso, sin embargo. El #includesí mismo es más o menos equivalente al importde otros idiomas.

Martin B
fuente
2
"dentro del mismo archivo CPP" debería leer "dentro de la misma unidad de traducción".
dreamlax
@dreamlax: Buen punto: eso es lo que originalmente iba a escribir, pero luego pensé que alguien que no entiende incluir guardias solo se confundirá con el término "unidad de traducción". Editaré la respuesta para agregar "unidad de traducción" entre paréntesis, eso debería ser lo mejor de ambos mundos.
Martin B
6

No lo hace, al menos durante la fase de compilación.

La traducción de un programa c ++ del código fuente al código máquina se realiza en tres fases:

  1. Preprocesamiento : el preprocesador analiza todo el código fuente de las líneas que comienzan con # y ejecuta las directivas. En su caso, el contenido de su archivo class.hse inserta en lugar de la línea #include "class.h. Dado que puede estar incluido en su archivo de encabezado en varios lugares, las #ifndefcláusulas evitan errores de declaración duplicados, ya que la directiva del preprocesador no está definida solo la primera vez que se incluye el archivo de encabezado.
  2. Compilación : el compilador ahora traduce todos los archivos de código fuente preprocesados ​​a archivos de objetos binarios.
  3. Vinculación : el vinculador vincula (de ahí el nombre) a los archivos de objetos. Una referencia a su clase o uno de sus métodos (que debe declararse en class.h y definirse en class.cpp) se resuelve en el desplazamiento respectivo en uno de los archivos de objeto. Escribo 'uno de sus archivos de objetos' ya que su clase no necesita ser definida en un archivo llamado class.cpp, podría estar en una biblioteca que está vinculada a su proyecto.

En resumen, las declaraciones se pueden compartir a través de un archivo de encabezado, mientras que el mapeador de declaraciones a definiciones lo realiza el vinculador.

sum1stolemyname
fuente
4

Esa es la distinción entre declaración y definición. Los archivos de encabezado generalmente incluyen solo la declaración, y el archivo fuente contiene la definición.

Para usar algo solo necesitas saber que es declaración, no es definición. Solo el vinculador necesita conocer la definición.

Por eso, incluirá un archivo de encabezado dentro de uno o más archivos de origen, pero no incluirá un archivo de origen dentro de otro.

También te refieres #includey no importas.

Brian R. Bondy
fuente
3

Esto se hace para los archivos de encabezado para que el contenido solo aparezca una vez en cada archivo fuente preprocesado, incluso si se incluye más de una vez (generalmente porque se incluye desde otros archivos de encabezado). La primera vez que se incluye, el símbolo CLASS_H(conocido como protector de inclusión ) aún no se ha definido, por lo que se incluye todo el contenido del archivo. Hacer esto define el símbolo, por lo que si se incluye nuevamente, el contenido del archivo (dentro de #ifndef/#endif se omiten bloque ).

No es necesario hacer esto para el archivo fuente en sí, ya que (normalmente) no está incluido en ningún otro archivo.

Para su última pregunta, class.hdebe contener la definición de la clase y las declaraciones de todos sus miembros, funciones asociadas y cualquier otra cosa, para que cualquier archivo que lo incluya tenga suficiente información para usar la clase. Las implementaciones de las funciones pueden ir en un archivo fuente separado; solo necesitas las declaraciones para llamarlos.

Mike Seymour
fuente
2

main.cpp no tiene que saber qué hay en class.cpp . Solo tiene que conocer las declaraciones de las funciones / clases que va a usar, y estas declaraciones están en class.h .

El enlazador enlaza entre los lugares donde se usan las funciones / clases declaradas en class.h y sus implementaciones en class.cpp

Igor Oks
fuente
1

.cpplos archivos no se incluyen (usando #include) en otros archivos. Por lo tanto, no necesitan incluir vigilancia. Main.cppconocerá los nombres y las firmas de la clase en la que ha implementado class.cppsolo porque ha especificado todo eso class.h; este es el propósito de un archivo de encabezado. (Depende de usted asegurarse de que class.hdescribe con precisión el código en el que implementa class.cpp). El código ejecutable class.cppestará disponible para el código ejecutable main.cppgracias a los esfuerzos del vinculador.

Kate Gregory
fuente
1

En general, se espera que los módulos de código, como los .cpparchivos, se compilen una vez y se vinculen en múltiples proyectos, para evitar la compilación repetitiva innecesaria de lógica. Por ejemplo, g++ -o class.cppproduciría class.olo que luego podría vincular desde múltiples proyectos para usar g++ main.cpp class.o.

Podríamos usarlo #includecomo nuestro vinculador, como parece estar insinuando, pero eso sería una tontería cuando sepamos cómo vincularnos correctamente utilizando nuestro compilador con menos pulsaciones de teclas y una repetición de compilación menos derrochadora, en lugar de nuestro código con más pulsaciones de teclas y más derrochador repetición de compilación ...

Sin embargo, aún se requiere que los archivos de encabezado se incluyan en cada uno de los proyectos múltiples, ya que esto proporciona la interfaz para cada módulo. Sin estos encabezados, el compilador no conocería ninguno de los símbolos introducidos por los .oarchivos.

Es importante darse cuenta de que los archivos de encabezado son los que introducen las definiciones de símbolos para esos módulos; una vez que se haya realizado, entonces tiene sentido que las inclusiones múltiples puedan causar redefiniciones de símbolos (lo que provoca errores), por lo que utilizamos guardias de inclusión para evitar tales redefiniciones.

autista
fuente
0

debido a que Headerfiles define lo que contiene la clase (Miembros, estructuras de datos) y los archivos cpp lo implementan.

Y, por supuesto, la razón principal de esto es que podría incluir un archivo .h varias veces en otros archivos .h, pero esto daría como resultado múltiples definiciones de una clase, que no es válida.

Quonux
fuente