¿Qué son las declaraciones directas en C ++?

215

En: http://www.learncpp.com/cpp-tutorial/19-header-files/

Se menciona lo siguiente:

add.cpp:

int add(int x, int y)
{
    return x + y;
}

main.cpp:

#include <iostream>

int add(int x, int y); // forward declaration using function prototype

int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is " << add(3, 4) << endl;
    return 0;
}

Usamos una declaración directa para que el compilador supiera qué era " add" al compilar main.cpp. Como se mencionó anteriormente, escribir declaraciones de reenvío para cada función que desee usar que se encuentre en otro archivo puede volverse tedioso rápidamente.

¿Puedes explicar más la " declaración hacia adelante "? ¿Cuál es el problema si lo usamos en la main()función?

Sencillez
fuente
1
Una "declaración adelantada" realmente es solo una declaración. Vea (al final de) esta respuesta: stackoverflow.com/questions/1410563/…
sbi

Respuestas:

381

Por qué es necesaria la declaración directa en C ++

El compilador quiere asegurarse de que no haya cometido errores ortográficos o haya pasado un número incorrecto de argumentos a la función. Por lo tanto, insiste en que primero ve una declaración de 'agregar' (o cualquier otro tipo, clase o función) antes de usarlo.

Esto realmente solo permite que el compilador haga un mejor trabajo de validación del código, y le permite ordenar los cabos sueltos para que pueda producir un archivo de objeto ordenado. Si no tuviera que reenviar las declaraciones, el compilador produciría un archivo de objeto que tendría que contener información sobre todas las posibles suposiciones sobre cuál podría ser la función 'agregar'. Y el enlazador tendría que contener una lógica muy inteligente para tratar de determinar qué 'agregar' realmente pretendía llamar, cuando la función 'agregar' puede vivir en un archivo de objeto diferente, el enlazador se une con el que usa agregar para producir un dll o exe. Es posible que el enlazador obtenga el complemento incorrecto. Supongamos que desea usar int add (int a, float b), pero accidentalmente olvidó escribirlo, pero el vinculador encontró un int add ya existente (int a, int b) y pensé que era el correcto y usé eso en su lugar. Su código se compilaría, pero no estaría haciendo lo que esperaba.

Entonces, solo para mantener las cosas explícitas y evitar las conjeturas, etc., el compilador insiste en que declares todo antes de usarlo.

Diferencia entre declaración y definición

Por otro lado, es importante saber la diferencia entre una declaración y una definición. Una declaración solo proporciona suficiente código para mostrar cómo se ve algo, así que para una función, este es el tipo de retorno, la convención de llamada, el nombre del método, los argumentos y sus tipos. Pero el código para el método no es obligatorio. Para una definición, necesita la declaración y también el código para la función también.

Cómo las declaraciones directas pueden reducir significativamente los tiempos de compilación

Puede obtener la declaración de una función en su archivo actual .cpp o .h # incluyendo el encabezado que ya contiene una declaración de la función. Sin embargo, esto puede ralentizar su compilación, especialmente si #incluye un encabezado en un .h en lugar de .cpp de su programa, ya que todo lo que # incluye el .h que está escribiendo terminaría # incluyendo todos los encabezados escribiste #incluye también. De repente, el compilador tiene #incluidas páginas y páginas de código que necesita compilar incluso cuando solo desea utilizar una o dos funciones. Para evitar esto, puede usar una declaración directa y simplemente escribir la declaración de la función usted mismo en la parte superior del archivo. Si solo usa algunas funciones, esto realmente puede hacer que sus compilaciones sean más rápidas en comparación con #incluir siempre el encabezado. Para proyectos realmente grandes,

Romper referencias cíclicas donde dos definiciones se usan

Además, las declaraciones directas pueden ayudarlo a romper los ciclos. Aquí es donde dos funciones intentan usarse entre sí. Cuando esto sucede (y es una cosa perfectamente válida), puede #incluir un archivo de encabezado, pero ese archivo de encabezado intenta # incluir el archivo de encabezado que está escribiendo actualmente ... que luego # incluye el otro encabezado , que incluye el que estás escribiendo. Estás atrapado en una situación de huevo y gallina con cada archivo de encabezado intentando #incluir el otro. Para resolver esto, puede declarar hacia adelante las partes que necesita en uno de los archivos y dejar el #include fuera de ese archivo.

P.ej:

File Car.h

#include "Wheel.h"  // Include Wheel's definition so it can be used in Car.
#include <vector>

class Car
{
    std::vector<Wheel> wheels;
};

File Wheel.h

Hmm ... aquí se requiere la declaración de Car ya que Wheel tiene un puntero a un Car, pero Car.h no se puede incluir aquí ya que daría lugar a un error del compilador. Si se incluye Car.h, eso trataría de incluir Wheel.h, que incluiría Car.h, que incluiría Wheel.h, y esto continuaría para siempre, por lo que el compilador genera un error. La solución es reenviar declarar Car en su lugar:

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

Si la clase Wheel tuviera métodos que necesitaran llamar métodos de auto, esos métodos podrían definirse en Wheel.cpp y Wheel.cpp ahora puede incluir Car.h sin causar un ciclo.

Scott Langham
fuente
44
declaración adelantada también es necesario cuando una función es amigable con dos o muchas clases
Barun
1
Hola Scott, en tu punto sobre los tiempos de compilación: ¿Dirías que es una práctica común / mejor siempre reenviar declarar e incluir encabezados según sea necesario en el archivo .cpp? Al leer su respuesta, parece que debería ser así, pero me pregunto si hay alguna advertencia.
Zepee
55
@ Zepee Es un equilibrio. Para compilaciones rápidas, diría que es una buena práctica y recomiendo probarlo. Sin embargo, puede tomar un poco de esfuerzo y líneas de código adicionales que pueden necesitar ser mantenidas y actualizadas si los nombres de tipo, etc., aún se cambian (aunque las herramientas mejoran al cambiar el nombre de las cosas automáticamente). Entonces hay una compensación. He visto bases de códigos donde nadie se molesta. Si se encuentra repitiendo las mismas definiciones de reenvío, siempre puede colocarlas en un archivo de encabezado separado e incluir eso, algo como: stackoverflow.com/questions/4300696/what-is-the-iosfwd-header
Scott Langham
se requieren declaraciones de reenvío cuando los archivos de encabezado se refieren entre sí: es decir, stackoverflow.com/questions/396084/…
Nicholas Hamilton
1
Puedo ver esto permitiendo que los otros desarrolladores de mi equipo sean realmente malos ciudadanos de la base de código. Si no necesita un comentario con la declaración de avance, // From Car.hpuede crear situaciones difíciles tratando de encontrar una definición en el futuro , garantizado.
Dagrooms
25

El compilador busca cada símbolo que se utiliza en la unidad de traducción actual que se haya declarado previamente o no en la unidad actual. Es solo una cuestión de estilo proporcionar todas las firmas de métodos al comienzo de un archivo fuente, mientras que las definiciones se proporcionan más adelante. Su uso significativo es cuando usa un puntero a una clase como variable miembro de otra clase.

//foo.h
class bar;    // This is useful
class foo
{
    bar* obj; // Pointer or even a reference.
};

// foo.cpp
#include "bar.h"
#include "foo.h"

Por lo tanto, use declaraciones directas en las clases siempre que sea posible. Si su programa solo tiene funciones (con archivos de encabezado ho), proporcionar prototipos al principio es solo una cuestión de estilo. De todos modos, este sería el caso si el archivo de encabezado estuviera presente en un programa normal con encabezado que solo tiene funciones.

Mahesh
fuente
12

Debido a que C ++ se analiza de arriba hacia abajo, el compilador necesita saber sobre las cosas antes de usarlas. Entonces, cuando hace referencia a:

int add( int x, int y )

en la función principal, el compilador necesita saber que existe. Para probar esto, intente moverlo debajo de la función principal y obtendrá un error de compilación.

Entonces, una ' Declaración de avance ' es justo lo que dice en la lata. Está declarando algo antes de su uso.

En general, incluiría declaraciones de reenvío en un archivo de encabezado y luego incluiría ese archivo de encabezado de la misma manera que se incluye iostream .

Mella
fuente
12

El término " declaración directa " en C ++ se usa principalmente para declaraciones de clase . Vea (al final de) esta respuesta para saber por qué una "declaración directa" de una clase es realmente una simple declaración de clase con un nombre elegante.

En otras palabras, el "reenvío" simplemente agrega lastre al término, ya que cualquier declaración puede verse como reenviada en la medida en que declara algún identificador antes de ser utilizado.

(En cuanto a qué es una declaración en lugar de una definición , nuevamente vea ¿Cuál es la diferencia entre una definición y una declaración? )

sbi
fuente
2

Cuando el compilador ve add(3, 4)que necesita saber qué significa eso. Con la declaración de reenvío, básicamente le dice al compilador que addes una función que toma dos entradas y devuelve un int. Esta es información importante para el compilador porque necesita poner 4 y 5 en la representación correcta en la pila y necesita saber de qué tipo es la cosa que devuelve add.

En ese momento, el compilador no está preocupado por la actual implementación de add, es decir, donde está (o si no es incluso uno) y si se compila. Esto aparece más tarde, después de compilar los archivos de origen cuando se invoca el vinculador.

René Nyffenegger
fuente
1
int add(int x, int y); // forward declaration using function prototype

¿Puedes explicar más la "declaración hacia adelante"? ¿Cuál es el problema si lo usamos en la función main ()?

Es lo mismo que #include"add.h" . Si lo sabe, el preprocesador expande el archivo que menciona #includeen el archivo .cpp donde escribe la #includedirectiva. Eso significa que, si escribes #include"add.h", obtienes lo mismo, es como si estuvieras haciendo una "declaración directa".

Supongo que add.htiene esta línea:

int add(int x, int y); 
Nawaz
fuente
1

Un apéndice rápido con respecto a: por lo general, coloca esas referencias en un archivo de encabezado que pertenece al archivo .c (pp) donde se implementa la función / variable, etc. en su ejemplo se vería así: add.h:

extern int add (int a, int b);

la palabra clave extern establece que la función se declara realmente en un archivo externo (también podría ser una biblioteca, etc.). su main.c se vería así:

#incluir 
#include "add.h"

int main ()
{
.
.
.

Jack
fuente
Pero, ¿no solo ponemos las declaraciones en el archivo de encabezado? Creo que es por eso que la función se define en "add.cpp" y, por lo tanto, utiliza declaraciones directas. Gracias.
Simplicidad
0

Un problema es que el compilador no sabe qué tipo de valor entrega su función; se supone que la función devuelve un inten este caso, pero esto puede ser tan correcto como incorrecto. Otro problema es que el compilador no sabe qué tipo de argumentos espera su función y no puede advertirle si está pasando valores del tipo incorrecto. Hay reglas especiales de "promoción", que se aplican al pasar, digamos valores de punto flotante a una función no declarada (el compilador tiene que ampliarlas para escribir double), que a menudo no es lo que la función realmente espera, lo que lleva a errores difíciles de encontrar en tiempo de ejecución.

Puñal
fuente