Quali sono le dichiarazioni a termine in C ++?


215

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

È menzionato quanto segue:

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;
}

Abbiamo usato una dichiarazione diretta in modo che il compilatore sapesse quale " add" era durante la compilazione main.cpp. Come accennato in precedenza, scrivere dichiarazioni anticipate per ogni funzione che si desidera utilizzare che risiede in un altro file può diventare noioso rapidamente.

Puoi spiegare ulteriormente la " dichiarazione anticipata "? Qual è il problema se lo usiamo nella main()funzione?


1
Una "dichiarazione anticipata" è in realtà solo una dichiarazione. Vedi (fine di) questa risposta: stackoverflow.com/questions/1410563/…
sbi

Risposte:


381

Perché forward-declare è necessario in C ++

Il compilatore desidera assicurarsi di non aver commesso errori di ortografia o di aver passato il numero errato di argomenti alla funzione. Quindi, insiste sul fatto che vede prima una dichiarazione di 'add' (o qualsiasi altro tipo, classe o funzione) prima che venga usata.

Questo in realtà consente al compilatore di fare un lavoro migliore nella convalida del codice e gli consente di riordinare le estremità libere in modo da poter produrre un file oggetto dall'aspetto pulito. Se non fosse necessario inoltrare dichiarazioni, il compilatore produrrebbe un file oggetto che dovrebbe contenere informazioni su tutte le possibili ipotesi su quale potrebbe essere la funzione 'aggiungi'. E il linker dovrebbe contenere una logica molto intelligente per provare a capire quale 'aggiungere' in realtà intendevi chiamare, quando la funzione 'aggiungi' potrebbe vivere in un diverso file oggetto il linker si unisce a quello che usa aggiungi per produrre una dll o exe. È possibile che il linker ottenga l'aggiunta errata. Supponiamo che tu voglia utilizzare int add (int a, float b), ma hai accidentalmente dimenticato di scriverlo, ma il linker ha trovato un int add già esistente (int a, int b) e ho pensato che fosse quello giusto e invece l'ho usato. Il codice verrà compilato, ma non farebbe ciò che ti aspettavi.

Quindi, solo per mantenere le cose esplicite ed evitare le supposizioni, ecc., Il compilatore insiste sul fatto che dichiari tutto prima di essere utilizzato.

Differenza tra dichiarazione e definizione

Per inciso, è importante conoscere la differenza tra una dichiarazione e una definizione. Una dichiarazione fornisce solo abbastanza codice per mostrare come appare qualcosa, quindi per una funzione, questo è il tipo restituito, che chiama la convenzione, il nome del metodo, gli argomenti e i loro tipi. Ma il codice per il metodo non è richiesto. Per una definizione, è necessaria la dichiarazione e quindi anche il codice per la funzione.

In che modo le dichiarazioni a termine possono ridurre significativamente i tempi di costruzione

Puoi ottenere la dichiarazione di una funzione nel tuo file .cpp o .h corrente # includendo l'intestazione che contiene già una dichiarazione della funzione. Tuttavia, questo può rallentare la compilazione, soprattutto se # includi un'intestazione in un .h invece di .cpp del tuo programma, poiché tutto ciò che # include il .h che stai scrivendo finirebbe # includendo tutte le intestazioni hai scritto anche #includes. Improvvisamente, il compilatore ha #incluso pagine e pagine di codice che deve compilare anche quando si desidera utilizzare solo una o due funzioni. Per evitare ciò, è possibile utilizzare una dichiarazione in avanti e digitare la dichiarazione della funzione da soli nella parte superiore del file. Se stai usando solo alcune funzioni, questo può davvero rendere le tue compilazioni più veloci rispetto al sempre #incluso l'intestazione. Per progetti davvero grandi,

Rompere i riferimenti ciclici in cui due definizioni si usano entrambe

Inoltre, le dichiarazioni anticipate possono aiutarti a interrompere i cicli. Qui è dove due funzioni cercano entrambe di usarsi. Quando ciò accade (ed è una cosa perfettamente valida da fare), puoi #includere un file di intestazione, ma quel file di intestazione cerca di #includere il file di intestazione che stai scrivendo .... che quindi # include l'altra intestazione , che # include quello che stai scrivendo. Sei bloccato in una situazione di pollo e uova con ogni file di intestazione che cerca di reincludere l'altro. Per risolvere questo problema, puoi inoltrare in anticipo le parti necessarie in uno dei file e lasciare #include da quel file.

Per esempio:

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 ... qui è richiesta la dichiarazione di Car in quanto Wheel ha un puntatore a una Car, ma Car.h non può essere incluso qui in quanto comporterebbe un errore del compilatore. Se Car.h fosse incluso, ciò proverebbe quindi a includere Wheel.h che includerebbe Car.h che includerebbe Wheel.h e questo continuerebbe per sempre, quindi il compilatore genera un errore. La soluzione è invece di dichiarare Car invece:

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

Se la classe Wheel avesse metodi che devono chiamare metodi di auto, tali metodi potrebbero essere definiti in Wheel.cpp e Wheel.cpp è ora in grado di includere Car.h senza causare un ciclo.


4
la dichiarazione in avanti è necessaria anche quando una funzione è amichevole per due o più classi
Barun,

1
Ehi Scott, a proposito dei tempi di costruzione: diresti che è una pratica comune / migliore inoltrare sempre dichiarazioni e includere le intestazioni secondo necessità nel file .cpp? Dal leggere la tua risposta sembrerebbe che dovrebbe essere così, ma mi chiedo se ci sono avvertenze?
Zepee,

5
@Zepee È un equilibrio. Per build veloci, direi che è una buona pratica e consiglio di provarlo. Tuttavia, possono essere necessari alcuni sforzi e ulteriori righe di codice che potrebbero dover essere mantenute e aggiornate se i nomi dei tipi ecc. Vengono ancora modificati (sebbene gli strumenti stiano migliorando nel rinominare automaticamente le cose). Quindi c'è un compromesso. Ho visto basi di codice in cui nessuno si preoccupa. Se ti ritrovi a ripetere le stesse definizioni forward, puoi sempre metterle in un file di intestazione separato e includerlo, qualcosa del tipo: stackoverflow.com/questions/4300696/what-is-the-iosfwd-header
Scott Langham

sono richieste dichiarazioni anticipate quando i file di intestazione si riferiscono l'un l'altro: ie stackoverflow.com/questions/396084/…
Nicholas Hamilton

1
Vedo questo che consente agli altri sviluppatori della mia squadra di essere davvero cattivi cittadini della base di codice. Se non hai bisogno di un commento con la dichiarazione in avanti, come // From Car.hallora puoi creare alcune situazioni pelose cercando di trovare una definizione lungo la strada, garantito.
Dagrooms,

25

Il compilatore cerca che ciascun simbolo utilizzato nell'unità di traduzione corrente sia precedentemente dichiarato o meno nell'unità corrente. È solo una questione di stile che fornisce tutte le firme dei metodi all'inizio di un file sorgente mentre le definizioni vengono fornite in seguito. Il suo uso significativo è quando si utilizza un puntatore a una classe come variabile membro di un'altra classe.

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

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

Quindi, quando possibile, usa le dichiarazioni forward nelle classi. Se il tuo programma ha solo funzioni (con i file di intestazione ho), fornire prototipi all'inizio è solo una questione di stile. Questo sarebbe comunque il caso se il file header fosse presente in un normale programma con header che ha solo funzioni.


12

Poiché il C ++ viene analizzato dall'alto verso il basso, il compilatore deve conoscere le cose prima che vengano utilizzate. Quindi, quando fai riferimento:

int add( int x, int y )

nella funzione principale il compilatore deve sapere che esiste. Per provarlo prova a spostarlo sotto la funzione principale e otterrai un errore del compilatore.

Quindi una " Dichiarazione a termine " è esattamente ciò che dice sulla scatola. Sta dichiarando qualcosa in anticipo rispetto al suo utilizzo.

In generale si dovrebbe includere dichiarazioni a termine in un file di intestazione e quindi includere il file di intestazione nello stesso modo in cui iostream è incluso.


12

Il termine " dichiarazione anticipata " in C ++ viene utilizzato principalmente per le dichiarazioni di classe . Vedi (fine di) questa risposta per capire perché una "dichiarazione anticipata" di una classe è in realtà solo una semplice dichiarazione di classe con un nome stravagante.

In altre parole, "forward" aggiunge semplicemente zavorra al termine, poiché qualsiasi dichiarazione può essere vista come in avanti in quanto dichiara un identificatore prima di essere utilizzato.

(Per quanto riguarda ciò che è una dichiarazione al contrario di una definizione , vedi di nuovo Qual è la differenza tra una definizione e una dichiarazione? )


2

Quando il compilatore vede add(3, 4), deve sapere cosa significa. Con la dichiarazione forward in sostanza si dice al compilatore che addè una funzione che accetta due ints e restituisce un int. Questa è un'informazione importante per il compilatore perché deve mettere 4 e 5 nella rappresentazione corretta nello stack e deve sapere che tipo è la cosa restituita da add.

A quel tempo, il compilatore non è preoccupato per l' effettiva attuazione add, vale a dire dove si trova (o se ci sia anche una sola) e se si compila. Ciò verrà visualizzato in seguito, dopo aver compilato i file di origine quando viene richiamato il linker.


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

Potete spiegare ulteriormente la "dichiarazione anticipata"? Qual è il problema se lo usiamo nella funzione main ()?

È lo stesso di #include"add.h". Se lo sai, il preprocessore espande il file in cui fai riferimento nel #includefile .cpp in cui scrivi la #includedirettiva. Ciò significa che, se scrivi #include"add.h", ottieni la stessa cosa, è come se stessi facendo una "dichiarazione anticipata".

Suppongo che add.habbia questa linea:

int add(int x, int y); 

1

un breve addendum riguardante: di solito metti quei riferimenti in avanti in un file di intestazione appartenente al file .c (pp) in cui è implementata la funzione / variabile ecc. nel tuo esempio sarebbe simile al seguente: add.h:

extern int add (int a, int b);

la parola chiave extern afferma che la funzione è effettivamente dichiarata in un file esterno (potrebbe anche essere una libreria ecc.). il tuo main.c assomiglierebbe a questo:

#includere 
#include "add.h"

int main ()
{
.
.
.


Ma non inseriamo solo le dichiarazioni nel file di intestazione? Penso che questo sia il motivo per cui la funzione è definita in "add.cpp", e quindi usando dichiarazioni forward? Grazie.
Semplicità

0

Un problema è che il compilatore non sa quale tipo di valore viene fornito dalla tua funzione; si presume che la funzione restituisca un intin questo caso, ma ciò può essere tanto corretto quanto errato. Un altro problema è che il compilatore non sa quale tipo di argomenti si aspetta la tua funzione e non può avvisarti se stai passando valori del tipo sbagliato. Esistono regole speciali di "promozione", che si applicano quando si passa, ad esempio valori in virgola mobile a una funzione non dichiarata (il compilatore deve allargarli per digitare double), che spesso non è ciò che la funzione si aspetta effettivamente, portando a trovare bug difficili in fase di esecuzione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.