Que sont les déclarations à terme en C ++?

215

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

Ce qui suit est mentionné:

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

Nous avons utilisé une déclaration avant pour que le compilateur sache ce qu'était " add" lors de la compilation main.cpp. Comme mentionné précédemment, l'écriture de déclarations avancées pour chaque fonction que vous souhaitez utiliser et qui réside dans un autre fichier peut devenir fastidieuse rapidement.

Pouvez-vous expliquer davantage la " déclaration à terme "? Quel est le problème si nous l'utilisons dans la main()fonction?

Simplicité
la source
1
Une "déclaration anticipée" n'est vraiment qu'une déclaration. Voir (la fin de) cette réponse: stackoverflow.com/questions/1410563/…
sbi

Réponses:

381

Pourquoi la déclaration directe est nécessaire en C ++

Le compilateur veut s'assurer que vous n'avez pas fait d'erreurs d'orthographe ou passé le mauvais nombre d'arguments à la fonction. Ainsi, il insiste sur le fait qu'il voit d'abord une déclaration «add» (ou tout autre type, classe ou fonction) avant d'être utilisé.

Cela permet simplement au compilateur de faire un meilleur travail de validation du code, et lui permet de ranger les extrémités lâches afin qu'il puisse produire un fichier objet soigné. Si vous n'aviez pas à transmettre de déclaration, le compilateur produirait un fichier objet qui devrait contenir des informations sur toutes les suppositions possibles quant à ce que la fonction «ajouter» pourrait être. Et l'éditeur de liens devrait contenir une logique très intelligente pour essayer de déterminer quel «ajout» vous aviez réellement l'intention d'appeler, lorsque la fonction «ajouter» peut vivre dans un fichier objet différent que l'éditeur de liens joint à celui qui utilise ajouter pour produire une DLL ou un exe. Il est possible que l'éditeur de liens obtienne le mauvais ajout. Supposons que vous vouliez utiliser int add (int a, float b), mais que vous avez accidentellement oublié de l'écrire, mais l'éditeur de liens a trouvé un int add déjà existant (int a, int b) et a pensé que c'était le bon et l'a utilisé à la place. Votre code compilerait, mais ne ferait pas ce que vous attendiez.

Donc, juste pour garder les choses explicites et éviter les suppositions, etc., le compilateur insiste pour que vous déclariez tout avant qu'il ne soit utilisé.

Différence entre déclaration et définition

En passant, il est important de connaître la différence entre une déclaration et une définition. Une déclaration donne juste assez de code pour montrer à quoi ressemble quelque chose, donc pour une fonction, c'est le type de retour, la convention d'appel, le nom de la méthode, les arguments et leurs types. Mais le code de la méthode n'est pas requis. Pour une définition, vous avez également besoin de la déclaration et du code de la fonction.

Comment les déclarations prévisionnelles peuvent réduire considérablement les temps de génération

Vous pouvez obtenir la déclaration d'une fonction dans votre fichier .cpp ou .h actuel en # incluant l'en-tête qui contient déjà une déclaration de la fonction. Cependant, cela peut ralentir votre compilation, surtout si vous # incluez un en-tête dans un .h au lieu de .cpp de votre programme, car tout ce qui #inclut le .h que vous écrivez finirait par # inclure tous les en-têtes vous avez également écrit #includes pour. Du coup, le compilateur a #inclus des pages et des pages de code dont il a besoin pour compiler même lorsque vous ne souhaitez utiliser qu'une ou deux fonctions. Pour éviter cela, vous pouvez utiliser une déclaration directe et tapez simplement la déclaration de la fonction vous-même en haut du fichier. Si vous n'utilisez que quelques fonctions, cela peut vraiment rendre vos compilations plus rapides que de toujours inclure # l'en-tête. Pour les très gros projets,

Briser les références cycliques où deux définitions utilisent les deux

De plus, les déclarations à terme peuvent vous aider à rompre les cycles. C'est là que deux fonctions essaient toutes deux de s'utiliser. Lorsque cela se produit (et c'est une chose parfaitement valable à faire), vous pouvez #inclure un fichier d'en-tête, mais ce fichier d'en-tête essaie de #inclure le fichier d'en-tête que vous écrivez actuellement .... qui #inclut ensuite l'autre en-tête , qui #inclut celui que vous écrivez. Vous êtes coincé dans une situation de poulet et d'oeuf avec chaque fichier d'en-tête essayant de ré-inclure l'autre. Pour résoudre ce problème, vous pouvez déclarer les pièces dont vous avez besoin dans l'un des fichiers et laisser le #include hors de ce fichier.

Par exemple:

Fichier 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 ... la déclaration de Car est requise ici car Wheel a un pointeur sur Car, mais Car.h ne peut pas être inclus ici car cela entraînerait une erreur de compilation. Si Car.h était inclus, cela essaierait alors d'inclure Wheel.h qui inclurait Car.h qui inclurait Wheel.h et cela continuerait pour toujours, donc à la place le compilateur soulève une erreur. La solution est de transmettre à la place déclarer Car:

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

Si la classe Wheel avait des méthodes qui doivent appeler des méthodes de car, ces méthodes pourraient être définies dans Wheel.cpp et Wheel.cpp est maintenant capable d'inclure Car.h sans provoquer de cycle.

Scott Langham
la source
4
une déclaration directe est également nécessaire lorsqu'une fonction est compatible avec deux ou plusieurs classes
Barun
1
Hey Scott, sur votre point sur les temps de construction: Diriez-vous que c'est une pratique courante / meilleure de toujours transmettre déclarer et inclure les en-têtes selon les besoins dans le fichier .cpp? À la lecture de votre réponse, il semblerait qu'il devrait en être ainsi, mais je me demande s'il y a des mises en garde?
Zepee
5
@Zepee C'est un équilibre. Pour les builds rapides, je dirais que c'est une bonne pratique et je recommande de l'essayer. Cependant, cela peut prendre un certain effort et des lignes de code supplémentaires qui devront peut-être être maintenues et mises à jour si les noms de type, etc. sont toujours modifiés (bien que les outils s'améliorent pour renommer automatiquement les choses). Il y a donc un compromis. J'ai vu des bases de code où personne ne dérange. Si vous vous retrouvez à répéter les mêmes définitions avancées, vous pouvez toujours les placer dans un fichier d'en-tête distinct et l'inclure, quelque chose comme: stackoverflow.com/questions/4300696/what-is-the-iosfwd-header
Scott Langham
des déclarations aval sont requises lorsque les fichiers d'en-tête se réfèrent les uns aux autres: ie stackoverflow.com/questions/396084/…
Nicholas Hamilton
1
Je peux voir que cela permet aux autres développeurs de mon équipe d'être de très mauvais citoyens de la base de code. Si vous n'avez pas besoin d'un commentaire avec la déclaration vers l'avant, // From Car.hvous pouvez créer des situations velues en essayant de trouver une définition sur la route, c'est garanti.
Dagrooms
25

Le compilateur recherche que chaque symbole utilisé dans l'unité de traduction actuelle est précédemment déclaré ou non dans l'unité actuelle. C'est juste une question de style fournissant toutes les signatures de méthode au début d'un fichier source tandis que les définitions sont fournies plus tard. Son utilisation importante est lorsque vous utilisez un pointeur vers une classe en tant que variable membre d'une autre 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"

Donc, utilisez des déclarations directes dans les classes lorsque cela est possible. Si votre programme n'a que des fonctions (avec des fichiers d'en-tête ho), fournir des prototypes au début n'est qu'une question de style. Ce serait de toute façon le cas si le fichier d'en-tête était présent dans un programme normal avec un en-tête qui n'a que des fonctions.

Mahesh
la source
12

Parce que C ++ est analysé de haut en bas, le compilateur doit connaître les choses avant de les utiliser. Ainsi, lorsque vous faites référence:

int add( int x, int y )

dans la fonction principale, le compilateur doit savoir qu'il existe. Pour le prouver, essayez de le déplacer en dessous de la fonction principale et vous obtiendrez une erreur de compilation.

Donc, une « déclaration prospective » est exactement ce qu'elle dit sur l'étain. Il déclare quelque chose avant son utilisation.

En règle générale, vous incluez des déclarations directes dans un fichier d'en-tête, puis vous incluez ce fichier d'en-tête de la même manière que iostream est inclus.

pseudo
la source
12

Le terme " déclaration avant " en C ++ est principalement utilisé uniquement pour les déclarations de classe . Voir (la fin de) cette réponse pour savoir pourquoi une "déclaration avant" d'une classe est vraiment juste une simple déclaration de classe avec un nom de fantaisie.

En d'autres termes, le "forward" ajoute simplement du lest au terme, car toute déclaration peut être vue comme étant en avant dans la mesure où elle déclare un identifiant avant d' être utilisée.

(En ce qui concerne ce qu'est une déclaration par opposition à une définition , voir à nouveau Quelle est la différence entre une définition et une déclaration? )

sbi
la source
2

Lorsque le compilateur le voit, add(3, 4)il doit savoir ce que cela signifie. Avec la déclaration directe, vous dites au compilateur qu'il adds'agit d'une fonction qui prend deux entrées et retourne un entier. Il s'agit d'informations importantes pour le compilateur car il doit mettre 4 et 5 dans la représentation correcte sur la pile et doit savoir de quel type est la chose retournée par add.

À ce moment - là, le compilateur n'est pas inquiet au sujet de la réelle mise en œuvre add, à savoir où il est (ou s'il est encore un) et si elle compile. Cela apparaît plus tard, après avoir compilé les fichiers source lorsque l'éditeur de liens est appelé.

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

Pouvez-vous expliquer davantage la "déclaration à terme"? Quel est le problème si nous l'utilisons dans la fonction main ()?

C'est la même chose que #include"add.h". Si vous le savez, le préprocesseur développe le fichier que vous mentionnez #includedans le fichier .cpp où vous écrivez la #includedirective. Cela signifie que si vous écrivez #include"add.h", vous obtenez la même chose, c'est comme si vous faisiez une "déclaration avant".

Je suppose que add.hcette ligne a:

int add(int x, int y); 
Nawaz
la source
1

un petit addenda concernant: habituellement vous mettez ces références avancées dans un fichier d'en-tête appartenant au fichier .c (pp) où la fonction / variable etc. est implémentée. dans votre exemple, cela ressemblerait à ceci: add.h:

extern int add (int a, int b);

le mot-clé extern indique que la fonction est effectivement déclarée dans un fichier externe (peut également être une bibliothèque, etc.). votre main.c ressemblerait à ceci:

#comprendre 
#include "add.h"

int main()
{
.
.
.

jack
la source
Mais, ne mettons-nous pas seulement les déclarations dans le fichier d'en-tête? Je pense que c'est pourquoi la fonction est définie dans "add.cpp", et donc en utilisant des déclarations avancées? Merci.
Simplicity
0

Un problème est que le compilateur ne sait pas quel type de valeur est fourni par votre fonction; est suppose que la fonction renvoie un intdans ce cas, mais cela peut être aussi correct que faux. Un autre problème est que le compilateur ne sait pas quel type d'arguments votre fonction attend et ne peut pas vous avertir si vous transmettez des valeurs de mauvais type. Il existe des règles spéciales de "promotion", qui s'appliquent lors du passage, par exemple, de valeurs à virgule flottante à une fonction non déclarée (le compilateur doit les élargir pour taper double), ce qui n'est souvent pas ce que la fonction attend réellement, ce qui conduit à des bogues difficiles à trouver lors de l'exécution.

Poignard
la source