portaldacalheta.pt
  • Principal
  • Procédé De Design
  • Interface Web
  • Hausse Des Revenus
  • Processus Financiers
La Technologie

Fonctionnement de C ++: Comprendre la compilation



De Bjarne Stroustrup Le langage de programmation C ++ a un chapitre intitulé «A Tour of C ++: The Basics» —Standard C ++. Ce chapitre, en 2.2, mentionne en une demi-page le processus de compilation et de liaison en C ++. La compilation et la liaison sont deux processus très basiques qui se produisent tout le temps pendant le développement de logiciels C ++, mais curieusement, ils ne sont pas bien compris par de nombreux développeurs C ++.

Pourquoi le code source C ++ est-il divisé en fichiers d'en-tête et source? Comment chaque partie est-elle vue par le compilateur? Comment cela affecte-t-il la compilation et la liaison? Il y a beaucoup d'autres questions comme celles-ci auxquelles vous avez peut-être pensé mais que vous en êtes venu à accepter comme convention.



qu'est-ce qu'un jeton erc20

Que vous conceviez une application C ++, que vous implémentiez de nouvelles fonctionnalités, que vous essayiez de résoudre des bogues (en particulier certains bogues étranges) ou que vous essayiez de faire en sorte que le code C et C ++ fonctionne ensemble, savoir comment la compilation et la liaison fonctionnent vous fera gagner beaucoup de temps et rendre ces tâches beaucoup plus agréables. Dans cet article, vous apprendrez exactement cela.



L'article expliquera comment un compilateur C ++ fonctionne avec certaines des constructions de langage de base, répondra à certaines questions courantes liées à leurs processus et vous aidera à contourner certaines erreurs associées que les développeurs font souvent dans le développement C ++.



Remarque: cet article contient un exemple de code source qui peut être téléchargé à partir de https://bitbucket.org/danielmunoz/cpp-article

Les exemples ont été compilés dans une machine Linux CentOS:



$ uname -sr Linux 3.10.0-327.36.3.el7.x86_64

En utilisant la version g ++:

$ g++ --version g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-11)

Les fichiers source fournis doivent être portables vers d'autres systèmes d'exploitation, bien que les Makefiles qui les accompagnent pour le processus de construction automatisé ne doivent être portables que sur des systèmes de type Unix.



Le pipeline de construction: prétraiter, compiler et lier

Chaque fichier source C ++ doit être compilé dans un fichier objet. Les fichiers objets résultant de la compilation de plusieurs fichiers sources sont ensuite liés dans un exécutable, une bibliothèque partagée ou une bibliothèque statique (la dernière étant simplement une archive de fichiers objets). Les fichiers source C ++ ont généralement les suffixes d'extension .cpp, .cxx ou .cc.

Un fichier source C ++ peut inclure d'autres fichiers, appelés fichiers d'en-tête, avec le #include directif. Les fichiers d'en-tête ont des extensions comme .h, .hpp ou .hxx, ou n'ont aucune extension comme dans la bibliothèque standard C ++ et les fichiers d'en-tête d'autres bibliothèques (comme Qt). L'extension n'a pas d'importance pour le préprocesseur C ++, qui remplacera littéralement la ligne contenant le #include directive avec tout le contenu du fichier inclus.



La première étape que le compilateur effectuera sur un fichier source est d'exécuter le préprocesseur dessus. Seuls les fichiers source sont transmis au compilateur (pour le prétraiter et le compiler). Les fichiers d’en-tête ne sont pas transmis au compilateur. Au lieu de cela, ils sont inclus à partir des fichiers source.

Chaque fichier d'en-tête peut être ouvert plusieurs fois pendant la phase de prétraitement de tous les fichiers source, en fonction du nombre de fichiers source qui les incluent, ou du nombre d'autres fichiers d'en-tête inclus à partir des fichiers source les incluent également (il peut y avoir plusieurs niveaux d'indirection) . Les fichiers sources, par contre, ne sont ouverts qu'une seule fois par le compilateur (et le préprocesseur), lorsqu'ils lui sont transmis.



Pour chaque fichier source C ++, le préprocesseur construira une unité de traduction en y insérant du contenu lorsqu'il trouvera une directive #include en même temps qu'il supprimera le code du fichier source et des en-têtes lorsqu'il trouvera compilation conditionnelle blocs dont la directive vaut false. Cela fera aussi autres tâches comme les remplacements de macro.

Une fois que le préprocesseur a fini de créer cette (parfois énorme) unité de traduction, le compilateur démarre la phase de compilation et produit le fichier objet.



Pour obtenir cette unité de traduction (le code source prétraité), le -E L'option peut être passée au compilateur g ++, avec le -o pour spécifier le nom souhaité du fichier source prétraité.

Dans le cpp-article/hello-world répertoire, il existe un fichier d'exemple «hello-world.cpp»:

#include int main(int argc, char* argv[]) { std::cout << 'Hello world' << std::endl; return 0; }

Créez le fichier prétraité en:

$ g++ -E hello-world.cpp -o hello-world.ii

Et voyez le nombre de lignes:

$ wc -l hello-world.ii 17558 hello-world.ii

Il a 17 588 lignes dans ma machine. Vous pouvez également simplement exécuter make dans ce répertoire et il effectuera ces étapes pour vous.

Nous pouvons voir que le compilateur doit compiler un fichier beaucoup plus volumineux que le simple fichier source que nous voyons. Cela est dû aux en-têtes inclus. Et dans notre exemple, nous n'avons inclus qu'un seul en-tête. L'unité de traduction devient de plus en plus grande à mesure que nous continuons à inclure les en-têtes.

Ce processus de prétraitement et de compilation est similaire pour le langage C. Il suit les règles C pour la compilation, et la façon dont il inclut les fichiers d'en-tête et produit le code objet est presque la même.

Comment les fichiers source importent et exportent les symboles

Voyons maintenant les fichiers dans cpp-article/symbols/c-vs-cpp-names annuaire.

Comment les fonctions sont traitées.

Il existe un simple fichier source C (et non C ++) nommé sum.c qui exporte deux fonctions, une pour ajouter deux entiers et une pour ajouter deux flottants:

int sumI(int a, int b) { return a + b; } float sumF(float a, float b) { return a + b; }

Compilez-le (ou exécutez make et toutes les étapes pour créer les deux exemples d'applications à exécuter) pour créer le fichier objet sum.o:

$ gcc -c sum.c

Regardez maintenant les symboles exportés et importés par ce fichier objet:

$ nm sum.o 0000000000000014 T sumF 0000000000000000 T sumI

Aucun symbole n'est importé et deux symboles sont exportés: sumF et sumI. Ces symboles sont exportés dans le cadre du segment .text (T), donc ce sont des noms de fonctions, du code exécutable.

Si d'autres fichiers sources (C ou C ++) veulent appeler ces fonctions, ils doivent les déclarer avant de les appeler.

La manière standard de le faire est de créer un fichier d'en-tête qui les déclare et les inclut dans le fichier source que nous voulons les appeler. L'en-tête peut avoir n'importe quel nom et extension. J'ai choisi sum.h:

#ifdef __cplusplus extern 'C' { #endif int sumI(int a, int b); float sumF(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif

Que sont ces ifdef / endif blocs de compilation conditionnelle? Si j'inclus cet en-tête à partir d'un fichier source C, je veux qu'il devienne:

int sumI(int a, int b); float sumF(float a, float b);

Mais si je les inclut à partir d'un fichier source C ++, je veux qu'il devienne:

extern 'C' { int sumI(int a, int b); float sumF(float a, float b); } // end extern 'C'

Le langage C ne sait rien de la extern 'C' directif , mais C ++ le fait, et il faut que cette directive soit appliquée aux déclarations de fonction C. Ceci est dû au fait C ++ mangles les noms des fonctions (et méthodes) car il prend en charge la surcharge de fonctions / méthodes, contrairement à C.

Cela peut être vu dans le fichier source C ++ nommé print.cpp:

#include // std::cout, std::endl #include 'sum.h' // sumI, sumF void printSum(int a, int b) { std::cout << a << ' + ' << b << ' = ' << sumI(a, b) << std::endl; } void printSum(float a, float b) { std::cout << a << ' + ' << b << ' = ' << sumF(a, b) << std::endl; } extern 'C' void printSumInt(int a, int b) { printSum(a, b); } extern 'C' void printSumFloat(float a, float b) { printSum(a, b); }

Il existe deux fonctions avec le même nom (printSum) qui ne diffèrent que par le type de leurs paramètres: int ou float. La surcharge de fonctions est une fonctionnalité C ++ qui n’est pas présent dans C. Pour implémenter cette fonctionnalité et différencier ces fonctions, C ++ déforme le nom de la fonction, comme nous pouvons le voir dans leur nom de symbole exporté (je ne choisirai que ce qui est pertinent dans la sortie de nm):

$ g++ -c print.cpp $ nm print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T _Z8printSumff 0000000000000000 T _Z8printSumii U _ZSt4cout

Ces fonctions sont exportées (dans mon système) sous la forme _Z8printSumff pour la version flottante et _Z8printSumii pour la version int. Chaque nom de fonction en C ++ est mutilé sauf s'il est déclaré comme extern 'C'. Il y a deux fonctions qui ont été déclarées avec un lien C dans print.cpp: printSumInt et printSumFloat.

Par conséquent, ils ne peuvent pas être surchargés, ou leurs noms exportés seraient les mêmes car ils ne sont pas mutilés. J'ai dû les différencier les uns des autres en postfixant un Int ou un Float à la fin de leurs noms.

Comme ils ne sont pas mutilés, ils peuvent être appelés à partir du code C, comme nous le verrons bientôt.

Pour voir les noms mutilés comme nous les verrions dans le code source C ++, nous pouvons utiliser le -C (démangle) dans le nm commander. Encore une fois, je ne copierai que la même partie pertinente du résultat:

$ nm -C print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T printSum(float, float) 0000000000000000 T printSum(int, int) U std::cout

Avec cette option, au lieu de _Z8printSumff on voit printSum(float, float), et au lieu de _ZSt4cout nous voyons std :: cout, qui sont des noms plus conviviaux.

Nous voyons également que notre code C ++ appelle le code C: print.cpp appelle sumI et sumF, qui sont des fonctions C déclarées comme ayant une liaison C dans sum.h. Cela peut être vu dans la sortie nm de print.o ci-dessus, qui informe de certains symboles non définis (U): sumF, sumI et std::cout. Ces symboles non définis sont censés être fournis dans l'un des fichiers objets (ou bibliothèques) qui seront liés avec cette sortie de fichier objet dans la phase de liaison.

Jusqu'à présent, nous venons de compiler le code source en code objet, nous n'avons pas encore lié. Si nous ne lions pas le fichier objet contenant les définitions de ces symboles importés avec ce fichier objet, l'éditeur de liens s'arrêtera avec une erreur 'symbole manquant'.

Notez également que puisque print.cpp est un fichier source C ++, compilé avec un compilateur C ++ (g ++), tout le code qu'il contient est compilé sous forme de code C ++. Fonctions avec liaison C comme printSumInt et printSumFloat sont également des fonctions C ++ qui peuvent utiliser les fonctionnalités C ++. Seuls les noms des symboles sont compatibles avec C, mais le code est C ++, ce qui peut être vu par le fait que les deux fonctions appellent une fonction surchargée (printSum), ce qui ne pourrait pas arriver si printSumInt ou printSumFloat ont été compilés en C.

créer votre propre jeton ethereum

Voyons maintenant print.hpp, un fichier d'en-tête qui peut être inclus à la fois à partir de fichiers source C ou C ++, ce qui permettra printSumInt et printSumFloat à appeler à la fois à partir de C et à partir de C ++, et printSum à appeler depuis C ++:

#ifdef __cplusplus void printSum(int a, int b); void printSum(float a, float b); extern 'C' { #endif void printSumInt(int a, int b); void printSumFloat(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif

Si nous l'incluons à partir d'un fichier source C, nous voulons juste voir:

void printSumInt(int a, int b); void printSumFloat(float a, float b);

printSum ne peut pas être vu à partir du code C car son nom est mutilé, nous n'avons donc pas de moyen (standard et portable) de le déclarer pour le code C. Oui, je peux les déclarer comme:

void _Z8printSumii(int a, int b); void _Z8printSumff(float a, float b);

Et l'éditeur de liens ne se plaindra pas puisque c'est le nom exact que mon compilateur actuellement installé a inventé pour lui, mais je ne sais pas si cela fonctionnera pour votre éditeur de liens (si votre compilateur génère un nom mutilé différent), ou même pour le prochaine version de mon éditeur de liens. Je ne sais même pas si l'appel fonctionnera comme prévu en raison de l'existence de conventions d'appel (comment les paramètres sont passés et les valeurs de retour sont renvoyées) qui sont spécifiques au compilateur et peuvent être différents pour les appels C et C ++ (en particulier pour les fonctions C ++ qui sont des fonctions membres et reçoivent le pointeur this en tant que paramètre).

Votre compilateur peut potentiellement utiliser une convention d'appel pour les fonctions C ++ normales et une autre si elles sont déclarées comme ayant une liaison externe «C». Ainsi, tromper le compilateur en disant qu'une fonction utilise la convention d'appel C alors qu'elle utilise réellement C ++ car elle peut donner des résultats inattendus si les conventions utilisées pour chacune se trouvent être différentes dans votre chaîne d'outils de compilation.

Il y a méthodes standard pour mélanger C et C ++ code et un moyen standard d'appeler des fonctions surchargées C ++ à partir de C est de les envelopper dans des fonctions avec la liaison C comme nous l'avons fait en enveloppant printSum avec printSumInt et printSumFloat.

Si nous incluons print.hpp à partir d'un fichier source C ++, le __cplusplus la macro du préprocesseur sera définie et le fichier sera vu comme:

void printSum(int a, int b); void printSum(float a, float b); extern 'C' { void printSumInt(int a, int b); void printSumFloat(float a, float b); } // end extern 'C'

Cela permettra au code C ++ d'appeler la fonction surchargée printSum ou ses wrappers printSumInt et printSumFloat.

Créons maintenant un fichier source C contenant la fonction principale, qui est le point d’entrée d’un programme. Cette fonction principale C appellera printSumInt et printSumFloat, c'est-à-dire appeler les deux fonctions C ++ avec liaison C. Souvenez-vous que ce sont des fonctions C ++ (leurs corps de fonction exécutent du code C ++) qui n'ont que des noms déformés en C ++. Le fichier est nommé c-main.c:

#include 'print.hpp' int main(int argc, char* argv[]) { printSumInt(1, 2); printSumFloat(1.5f, 2.5f); return 0; }

Compilez-le pour générer le fichier objet:

$ gcc -c c-main.c

Et voir les symboles importés / exportés:

$ nm c-main.o 0000000000000000 T main U printSumFloat U printSumInt

Elle exporte le principal et importe printSumFloat et printSumInt, comme prévu.

Pour tout lier dans un fichier exécutable, nous devons utiliser l'éditeur de liens C ++ (g ++), car au moins un fichier que nous allons lier, print.o, a été compilé en C ++:

$ g++ -o c-app sum.o print.o c-main.o

L'exécution produit le résultat attendu:

$ ./c-app 1 + 2 = 3 1.5 + 2.5 = 4

Essayons maintenant avec un fichier principal C ++, nommé cpp-main.cpp:

#include 'print.hpp' int main(int argc, char* argv[]) { printSum(1, 2); printSum(1.5f, 2.5f); printSumInt(3, 4); printSumFloat(3.5f, 4.5f); return 0; }

Compilez et visualisez les symboles importés / exportés de cpp-main.o fichier objet:

$ g++ -c cpp-main.cpp $ nm -C cpp-main.o 0000000000000000 T main U printSumFloat U printSumInt U printSum(float, float) U printSum(int, int)

Il exporte le principal et importe le lien C printSumFloat et printSumInt, et les deux versions mutilées de printSum.

Vous vous demandez peut-être pourquoi le symbole principal n’est pas exporté en tant que symbole mutilé comme main(int, char**) à partir de cette source C ++ puisqu'il s'agit d'un fichier source C ++ et qu'il n'est pas défini comme extern 'C'. Eh bien, main est une fonction définie par une implémentation spéciale et mon implémentation semble avoir choisi d'utiliser le lien C pour cela, qu'il soit défini dans un fichier source C ou C ++.

Lier et exécuter le programme donne le résultat attendu:

$ g++ -o cpp-app sum.o print.o cpp-main.o $ ./cpp-app 1 + 2 = 3 1.5 + 2.5 = 4 3 + 4 = 7 3.5 + 4.5 = 8

Fonctionnement des protections d'en-tête

Jusqu'à présent, j'ai pris soin de ne pas inclure mes en-têtes deux fois, directement ou indirectement, à partir du même fichier source. Mais comme un en-tête peut inclure d'autres en-têtes, le même en-tête peut indirectement être inclus plusieurs fois. Et comme le contenu de l'en-tête est simplement inséré à l'endroit où il a été inclus, il est facile de terminer avec des déclarations dupliquées.

Voir les fichiers d'exemple dans cpp-article/header-guards.

// unguarded.hpp class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; // guarded.hpp: #ifndef __GUARDED_HPP #define __GUARDED_HPP class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; #endif // __GUARDED_HPP

La différence est que, dans guarded.hpp, nous entourons l'en-tête entier d'un conditionnel qui ne sera inclus que si __GUARDED_HPP la macro de préprocesseur n’est pas définie. La première fois que le préprocesseur inclut ce fichier, il ne sera pas défini. Mais, puisque la macro est définie à l'intérieur de ce code protégé, la prochaine fois qu'elle sera incluse (à partir du même fichier source, directement ou indirectement), le préprocesseur verra les lignes entre le #ifndef et le #endif et rejettera tout le code entre leur.

Notez que ce processus se produit pour chaque fichier source que nous compilons. Cela signifie que ce fichier d'en-tête peut être inclus une et une seule fois pour chaque fichier source. Le fait qu'il ait été inclus à partir d'un fichier source ne l'empêche pas d'être inclus à partir d'un fichier source différent lors de la compilation de ce fichier source. Cela l'empêchera simplement d'être inclus plusieurs fois à partir du même fichier source.

Le fichier d'exemple main-guarded.cpp comprend guarded.hpp deux fois:

#include 'guarded.hpp' #include 'guarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

Mais la sortie prétraitée n'affiche qu'une seule définition de classe A:

$ g++ -E main-guarded.cpp # 1 'main-guarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-guarded.cpp' # 1 'guarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-guarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

Par conséquent, il peut être compilé sans problème:

$ g++ -o guarded main-guarded.cpp

Mais le main-unguarded.cpp le fichier comprend unguarded.hpp deux fois:

#include 'unguarded.hpp' #include 'unguarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

Et la sortie prétraitée montre deux définitions de la classe A:

$ g++ -E main-unguarded.cpp # 1 'main-unguarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-unguarded.cpp' # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-unguarded.cpp' 2 # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 3 'main-unguarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }

Cela posera des problèmes lors de la compilation:

$ g++ -o unguarded main-unguarded.cpp

Dans le fichier inclus de main-unguarded.cpp:2:0:

unguarded.hpp:1:7: error: redefinition of 'class A' class A { ^ In file included from main-unguarded.cpp:1:0: unguarded.hpp:1:7: error: previous definition of 'class A' class A { ^

Par souci de concision, je n’utiliserai pas d’en-têtes protégés dans cet article si ce n’est pas nécessaire, car la plupart sont de courts exemples. Mais gardez toujours vos fichiers d'en-tête. Pas vos fichiers sources, qui ne seront inclus de nulle part. Juste des fichiers d'en-tête.

Passer par valeur et constance des paramètres

Regardez by-value.cpp fichier dans cpp-article/symbols/pass-by:

#include #include #include // std::vector, std::accumulate, std::cout, std::endl using namespace std; int sum(int a, const int b) { cout << 'sum(int, const int)' << endl; const int c = a + b; ++a; // Possible, not const // ++b; // Not possible, this would result in a compilation error return c; } float sum(const float a, float b) { cout << 'sum(const float, float)' << endl; return a + b; } int sum(vector v) { cout << 'sum(vector)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const vector v) { cout << 'sum(const vector)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }

Depuis que j'utilise le using namespace std directive, je n’ai pas à qualifier les noms des symboles (fonctions ou classes) dans l’espace de noms std dans le reste de l’unité de traduction, qui dans mon cas est le reste du fichier source. S'il s'agissait d'un fichier d'en-tête, je n'aurais pas dû insérer cette directive car un fichier d'en-tête est censé être inclus à partir de plusieurs fichiers source; cette directive apporterait à la portée globale de chaque fichier source tout l'espace de noms std à partir du point où ils incluent mon en-tête.

un budget d'investissement est un :

Même les en-têtes inclus après le mien dans ces fichiers auront ces symboles dans la portée. Cela peut produire des conflits de noms car ils ne s'attendaient pas à ce que cela se produise. Par conséquent, n’utilisez pas cette directive dans les en-têtes. Utilisez-le uniquement dans les fichiers source si vous le souhaitez, et uniquement après avoir inclus tous les en-têtes.

Notez comment certains paramètres sont const. Cela signifie qu'ils ne peuvent pas être modifiés dans le corps de la fonction si nous essayons de le faire. Cela donnerait une erreur de compilation. Notez également que tous les paramètres de ce fichier source sont passés par valeur et non par référence (&) ou par pointeur (*). Cela signifie que l'appelant en fera une copie et passera à la fonction. Donc, peu importe pour l'appelant qu'ils soient const ou non, car si nous les modifions dans le corps de la fonction, nous ne modifierons que la copie, pas la valeur d'origine que l'appelant a passée à la fonction.

Étant donné que la constance d'un paramètre passé par valeur (copie) n'a pas d'importance pour l'appelant, elle n'est pas altérée dans la signature de la fonction, comme on peut le voir après la compilation et l'inspection du code objet (uniquement la sortie pertinente):

$ g++ -c by-value.cpp $ nm -C by-value.o 000000000000001e T sum(float, float) 0000000000000000 T sum(int, int) 0000000000000087 T sum(std::vector) 0000000000000048 T sum(std::vector )

Les signatures n'expriment pas si les paramètres copiés sont const ou non dans le corps de la fonction. Cela n’a pas d’importance. Il importait uniquement pour la définition de la fonction, de montrer en un coup d'œil au lecteur du corps de la fonction si ces valeurs changeront un jour. Dans l'exemple, seule la moitié des paramètres sont déclarés const, donc nous pouvons voir le contraste, mais si nous voulons être const-correct ils devraient tous avoir été déclarés ainsi car aucun d’entre eux n’est modifié dans le corps de la fonction (et ils ne devraient pas).

Comme cela n'a pas d'importance pour la déclaration de fonction qui est ce que voit l'appelant, nous pouvons créer le by-value.hpp en-tête comme ceci:

#include int sum(int a, int b); float sum(float a, float b); int sum(std::vector v); int sum(std::vector v);

L'ajout des qualificatifs const ici est autorisé (vous pouvez même les qualifier comme des variables const qui ne sont pas const dans la définition et cela fonctionnera), mais ce n'est pas nécessaire et cela ne fera que des déclarations inutilement verbeuses.

Passer par référence

Voyons voir by-reference.cpp:

#include #include #include using namespace std; int sum(const int& a, int& b) { cout << 'sum(const int&, int&)' << endl; const int c = a + b; ++b; // Will modify caller variable // ++a; // Not allowed, but would also modify caller variable return c; } float sum(float& a, const float& b) { cout << 'sum(float&, const float&)' << endl; return a + b; } int sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }

La constance lors du passage par référence est importante pour l'appelant, car elle indiquera à l'appelant si son argument sera modifié ou non par l'appelé. Par conséquent, les symboles sont exportés avec leur constance:

$ g++ -c by-reference.cpp $ nm -C by-reference.o 0000000000000051 T sum(float&, float const&) 0000000000000000 T sum(int const&, int&) 00000000000000fe T sum(std::vector const&) 00000000000000a3 T sum(std::vector const&)

Cela devrait également être reflété dans l'en-tête que les appelants utiliseront:

#include int sum(const int&, int&); float sum(float&, const float&); int sum(const std::vector&); float sum(const std::vector&);

Notez que je n’ai pas écrit le nom des variables dans les déclarations (dans l’en-tête) comme je l’avais fait jusqu’à présent. Ceci est également légal, pour cet exemple et pour les précédents. Les noms de variable ne sont pas obligatoires dans la déclaration, car l'appelant n'a pas besoin de savoir comment vous voulez nommer votre variable. Mais les noms de paramètres sont généralement souhaitables dans les déclarations afin que l'utilisateur puisse savoir en un coup d'œil ce que chaque paramètre signifie et donc ce qu'il doit envoyer dans l'appel.

Étonnamment, les noms de variables ne sont pas non plus nécessaires dans la définition d’une fonction. Ils ne sont nécessaires que si vous utilisez réellement le paramètre dans la fonction. Mais si vous ne l'utilisez jamais, vous pouvez laisser le paramètre avec le type mais sans le nom. Pourquoi une fonction déclarerait-elle un paramètre qu’elle n’utiliserait jamais? Parfois, les fonctions (ou méthodes) font simplement partie d'une interface, comme une interface de rappel, qui définit certains paramètres qui sont passés à l'observateur. L'observateur doit créer un rappel avec tous les paramètres spécifiés par l'interface, car ils seront tous envoyés par l'appelant. Mais l'observateur peut ne pas être intéressé par tous, donc au lieu de recevoir un avertissement du compilateur concernant un «paramètre inutilisé», la définition de la fonction peut simplement le laisser sans nom.

Passer par le pointeur

// by-pointer.cpp: #include #include #include using namespace std; int sum(int const * a, int const * const b) { cout << 'sum(int const *, int const * const)' << endl; const int c = *a+ *b; // *a = 4; // Can't change. The value pointed to is const. // *b = 4; // Can't change. The value pointed to is const. a = b; // I can make a point to another const int // b = a; // Can't change where b points because the pointer itself is const. return c; } float sum(float * const a, float * b) { cout << 'sum(int const * const, float const *)' << endl; return *a + *b; } int sum(const std::vector* v) { cout << 'sum(std::vector const *)' begin(), v->end(), 0); v = NULL; // I can make v point to somewhere else return c; } float sum(const std::vector * const v) { cout << 'sum(std::vector const * const)' begin(), v->end(), 0.0f); }

Pour déclarer un pointeur vers un élément const (int dans l'exemple), vous pouvez déclarer le type comme suit:

int const * const int *

Si vous voulez également que le pointeur lui-même soit const, c'est-à-dire que le pointeur ne peut pas être changé pour pointer vers autre chose, vous ajoutez un const après l'étoile:

int const * const const int * const

Si vous voulez que le pointeur lui-même soit const, mais pas l'élément pointé par celui-ci:

int * const

Comparez les signatures de fonction avec l'inspection démêlée du fichier objet:

$ g++ -c by-pointer.cpp $ nm -C by-pointer.o 000000000000004a T sum(float*, float*) 0000000000000000 T sum(int const*, int const*) 0000000000000105 T sum(std::vector const*) 000000000000009c T sum(std::vector const*)

Comme vous le voyez, le nm L'outil utilise la première notation (const après le type). Notez également que la seule constance qui est exportée, et qui importe pour l'appelant, est de savoir si la fonction modifiera l'élément pointé par le pointeur ou non. La constance du pointeur lui-même n'est pas pertinente pour l'appelant car le pointeur lui-même est toujours passé en tant que copie. La fonction ne peut faire que sa propre copie du pointeur pour pointer vers un autre endroit, ce qui n'est pas pertinent pour l'appelant.

Ainsi, un fichier d'en-tête peut être créé comme:

#include int sum(int const* a, int const* b); float sum(float* a, float* b); int sum(std::vector* const); float sum(std::vector* const);

Passer par pointeur est comme passer par référence. Une différence est que lorsque vous passez par référence, l'appelant est censé et supposé avoir passé la référence d'un élément valide, ne pointant pas vers NULL ou une autre adresse non valide, alors qu'un pointeur peut pointer vers NULL par exemple. Les pointeurs peuvent être utilisés à la place des références lorsque le passage de NULL a une signification particulière.

Étant donné que les valeurs C ++ 11 peuvent également être transmises avec déplacer la sémantique . Ce sujet ne sera pas traité dans cet article mais pourra être étudié dans d'autres articles comme Argument passant en C ++ .

Un autre sujet connexe qui ne sera pas traité ici est comment appeler toutes ces fonctions. Si tous ces en-têtes sont inclus à partir d'un fichier source mais ne sont pas appelés, la compilation et la liaison réussiront. Mais si vous souhaitez appeler toutes les fonctions, il y aura des erreurs car certains appels seront ambigus. Le compilateur pourra choisir plus d'une version de sum pour certains arguments, notamment lors du choix de passer par copie ou par référence (ou référence const). Cette analyse sort du cadre de cet article.

Compiler avec différents drapeaux

Voyons maintenant une situation réelle liée à ce sujet où des bogues difficiles à trouver peuvent apparaître.

Aller au répertoire cpp-article/diff-flags et regardez Counters.hpp:

class Counters { public: Counters() : #ifndef NDEBUG // Enabled in debug builds m_debugAllCounters(0), #endif m_counter1(0), m_counter2(0) { } #ifndef NDEBUG // Enabled in debug build #endif void inc1() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter1; } void inc2() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter2; } #ifndef NDEBUG // Enabled in debug build int getDebugAllCounters() { return m_debugAllCounters; } #endif int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: #ifndef NDEBUG // Enabled in debug builds int m_debugAllCounters; #endif int m_counter1; int m_counter2; };

Cette classe a deux compteurs, qui commencent par zéro et peuvent être incrémentés ou lus. Pour les builds de débogage, c'est ainsi que j'appellerai les builds où NDEBUG macro n’est pas définie, j’ajoute également un troisième compteur, qui sera incrémenté chaque fois que l’un des deux autres compteurs est incrémenté. Ce sera une sorte d'aide au débogage pour cette classe. De nombreuses classes de bibliothèques tierces ou même des en-têtes C ++ intégrés (selon le compilateur) utilisent des astuces comme celle-ci pour autoriser différents niveaux de débogage. Cela permet aux versions de débogage de détecter les itérateurs hors de portée et d'autres choses intéressantes auxquelles le créateur de la bibliothèque pourrait penser. J'appellerai release builds 'builds où le NDEBUG macro est définie. »

qu'est-ce que la crise de la dette grecque

Pour les versions de version, l'en-tête précompilé ressemble à (j'utilise grep pour supprimer les lignes vides):

$ g++ -E -DNDEBUG Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_counter1(0), m_counter2(0) { } void inc1() { ++m_counter1; } void inc2() { ++m_counter2; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_counter1; int m_counter2; };

Alors que pour les versions de débogage, cela ressemblera à:

$ g++ -E Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_debugAllCounters(0), m_counter1(0), m_counter2(0) { } void inc1() { ++m_debugAllCounters; ++m_counter1; } void inc2() { ++m_debugAllCounters; ++m_counter2; } int getDebugAllCounters() { return m_debugAllCounters; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_debugAllCounters; int m_counter1; int m_counter2; };

Il y a un autre compteur dans les versions de débogage, comme je l'ai expliqué plus tôt.

J'ai également créé des fichiers d'aide.

// increment1.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment1(Counters&); // increment1.cpp: #include 'Counters.hpp' void increment1(Counters& c) { c.inc1(); } // increment2.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment2(Counters&); // increment2.cpp: #include 'Counters.hpp' void increment2(Counters& c) { c.inc2(); } // main.cpp: #include #include 'Counters.hpp' #include 'increment1.hpp' #include 'increment2.hpp' using namespace std; int main(int argc, char* argv[]) { Counters c; increment1(c); // 3 times increment1(c); increment1(c); increment2(c); // 4 times increment2(c); increment2(c); increment2(c); cout << 'c.get1(): ' << c.get1() << endl; // Should be 3 cout << 'c.get2(): ' << c.get2() << endl; // Should be 4 #ifndef NDEBUG // For debug builds cout << 'c.getDebugAllCounters(): ' << c.getDebugAllCounters() << endl; // Should be 3 + 4 = 7 #endif return 0; }

Et un Makefile qui peut personnaliser les indicateurs du compilateur pour increment2.cpp seulement:

all: main.o increment1.o increment2.o g++ -o diff-flags main.o increment1.o increment2.o main.o: main.cpp increment1.hpp increment2.hpp Counters.hpp g++ -c -O2 main.cpp increment1.o: increment1.cpp Counters.hpp g++ -c $(CFLAGS) -O2 increment1.cpp increment2.o: increment2.cpp Counters.hpp g++ -c -O2 increment2.cpp clean: rm -f *.o diff-flags

Alors, compilons tout en mode débogage, sans définir NDEBUG:

$ CFLAGS='' make g++ -c -O2 main.cpp g++ -c -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o

Maintenant, exécutez:

$ ./diff-flags c.get1(): 3 c.get2(): 4 c.getDebugAllCounters(): 7

La sortie est exactement comme prévu. Maintenant, compilons un seul des fichiers avec NDEBUG défini, qui serait le mode de libération, et voyez ce qui se passe:

$ make clean rm -f *.o diff-flags $ CFLAGS='-DNDEBUG' make g++ -c -O2 main.cpp g++ -c -DNDEBUG -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o $ ./diff-flags c.get1(): 0 c.get2(): 4 c.getDebugAllCounters(): 7

La sortie n'est pas celle attendue. increment1 function a vu une version finale de la classe Counters, dans laquelle il n'y a que deux champs membres int. Donc, il a incrémenté le premier champ, pensant que c'était m_counter1, et n'a rien incrémenté d'autre car il ne sait rien sur le m_debugAllCounters champ. Je dis que increment1 a incrémenté le compteur car la méthode inc1 dans Counter est en ligne, donc il a été incorporé dans increment1 corps de fonction, pas appelé à partir de lui. Le compilateur a probablement décidé de l'inclure car le -O2 L'indicateur de niveau d'optimisation a été utilisé.

Donc, m_counter1 n'a jamais été incrémenté et m_debugAllCounters a été incrémenté à sa place par erreur dans increment1. C’est pourquoi nous voyons 0 pour m_counter1 mais nous voyons toujours 7 pour m_debugAllCounters.

En travaillant dans un projet où nous avions des tonnes de fichiers sources, regroupés dans de nombreuses bibliothèques statiques, il est arrivé que certaines de ces bibliothèques aient été compilées sans options de débogage pour std::vector, et d'autres ont été compilées avec ces options.

Probablement à un moment donné, toutes les bibliothèques utilisaient les mêmes indicateurs, mais avec le temps, de nouvelles bibliothèques ont été ajoutées sans prendre en compte ces indicateurs (ce n'étaient pas des indicateurs par défaut, ils avaient été ajoutés à la main). Nous avons utilisé un IDE pour compiler, donc pour voir les drapeaux pour chaque bibliothèque, vous deviez fouiller dans les onglets et les fenêtres, ayant différents (et multiples) drapeaux pour différents modes de compilation (release, debug, profile ...), donc c'était encore plus difficile à noter que les drapeaux n'étaient pas cohérents.

Cela provoquait cela dans les rares occasions où un fichier objet, compilé avec un ensemble d'indicateurs, passait un std::vector dans un fichier objet compilé avec un ensemble différent d'indicateurs, qui effectuait certaines opérations sur ce vecteur, l'application s'est plantée. Imaginez que le débogage n’ait pas été facile puisque le plantage a été signalé dans la version finale, et qu’il ne s’est pas produit dans la version de débogage (du moins pas dans les mêmes situations que celles signalées).

Le débogueur a également fait des choses folles car il déboguait du code très optimisé. Les plantages se produisaient dans un code correct et trivial.

Le compilateur fait beaucoup plus que vous ne le pensez

Dans cet article, vous avez découvert certaines des constructions de langage de base de C ++ et comment le compilateur fonctionne avec elles, en commençant par l'étape de traitement jusqu'à l'étape de liaison. Savoir comment cela fonctionne peut vous aider à voir l'ensemble du processus différemment et vous donner plus d'informations sur ces processus que nous tenons pour acquis dans le développement C ++.

D'un processus de compilation en trois étapes à la manipulation des noms de fonctions et à la production de signatures de fonctions différentes dans différentes situations, le compilateur fait beaucoup de travail pour offrir la puissance de C ++ en tant que langage de programmation compilé.

J'espère que vous trouverez les connaissances de cet article utiles dans vos projets C ++.

En relation: Comment apprendre les langages C et C ++: la liste ultime

Premiers pas avec les microservices: un didacticiel Dropwizard

Science Des Données Et Bases De Données

Premiers pas avec les microservices: un didacticiel Dropwizard
Les 10 erreurs les plus courantes commises par les développeurs WordPress

Les 10 erreurs les plus courantes commises par les développeurs WordPress

Back-End

Articles Populaires
Comment créer une culture dans des équipes distantes
Comment créer une culture dans des équipes distantes
Guide du développeur sur les licences Open Source
Guide du développeur sur les licences Open Source
Comment organiser une conférence technique réussie: l'événement CordobaJS
Comment organiser une conférence technique réussie: l'événement CordobaJS
Astuces et astuces avancées pour les présentations PowerPoint
Astuces et astuces avancées pour les présentations PowerPoint
Un didacticiel pour les futurs développeurs Google Glass: créer votre première application Glass
Un didacticiel pour les futurs développeurs Google Glass: créer votre première application Glass
 
Vol d'identité des pigistes: ça m'est arrivé - voici ce que vous devez savoir
Vol d'identité des pigistes: ça m'est arrivé - voici ce que vous devez savoir
Les 9 erreurs les plus courantes commises par les développeurs ioniques
Les 9 erreurs les plus courantes commises par les développeurs ioniques
Ray Dalio de Bridgewater: pionnier silencieux du Big Data, du Machine Learning et de la Fintech
Ray Dalio de Bridgewater: pionnier silencieux du Big Data, du Machine Learning et de la Fintech
Le guide ultime pour créer un plugin WordPress
Le guide ultime pour créer un plugin WordPress
Reconnaissance des numéros d'apprentissage automatique - De zéro à l'application
Reconnaissance des numéros d'apprentissage automatique - De zéro à l'application
Articles Populaires
  • créer un fonds de capital-investissement
  • comment structurer un gain
  • comment utiliser la directive dans angularjs
  • responsabilité du directeur financier
  • différence entre s et c corp llc
  • comment construire des modèles financiers
Catégories
  • Procédé De Design
  • Interface Web
  • Hausse Des Revenus
  • Processus Financiers
  • © 2022 | Tous Les Droits Sont Réservés

    portaldacalheta.pt