Ce cours s'adresse aux personnes ayant une bonne connaissance du langage C ainsi que des notions concernant le modèle objet. Il présente les notions de base du langage C++ ; une série d'exercices lui est associée dans la partie travaux pratiques.
Dans un premier temps, et bien que la syntaxe du C++ reprenne dans les grandes lignes celle du C, nous allons étudier quelques unes des principales différences.
Il y a deux types de commentaires en C++ : les commentaires mono ligne et les blocs de commentaire. Les commentaires mono ligne commencent par les symboles // et permettent de placer en commentaire la fin d'une ligne comme le montre le fragment de code suivant :
int i; // fin de la ligne en commentaire
Les commentaires mono ligne s'avèrent particulièrement utiles et agréables à utiliser au point que l'on aurait ensuite tendance à vouloir les incorporer dans du code C ! ce qui est naturellement interdit mais néanmoins autorisé par certains compilateurs peu scrupuleux.
Les blocs de commentaires sont eux compatibles avec le langage C (ils en sont directement issus) et permettent de banaliser de plus larges portions de code en le plaçant entre les paires /* et */ comme le montre le fragment de code suivant :
/* code sur plusieurs lignes en commentaire */
Ce type de commentaire a toutefois une particularité fâcheuse : ils ne peuvent être imbriqués, ce qui peut conduire à des erreurs de compilation lorsque, par mégarde on veut mettre en commentaire une portion de code qui contient déjà un tel commentaire !
Considérez, par exemple, le code éronné suivant où l'on tente d'imbriquer deux commentaires :
/* debut de commentaire externe ... texte en commentaire /* debut de commentaire imbrique ... Fin de commentaire imbrique ... et fin effective du commentaire externe*/ ... texte compilé alors qu'on souhaitait le voir placé en commentaire Fin souhaitée du commentaire externe*/
Placez vous dorénavant dans la peau du compilateur C++ (c'est un peu étriqué, j'en conviens). Vous voyez le début du premier bloc de commentaire, vous savez donc que, dorénavant, tout le code avant un signe */ doit être considéré comme étant mis en commentaire. Aussi, vous ne détecterez pas la présence de la balise /* indiquant le début d'un commentaire mais vous arrêterez au premier signe */ présent. Aussi, toute la partie de code placée entre les deux balises */ n'est pas considérée comme étant mise en commentaire, ce qui, dans le meilleur des cas se traduira par une erreur de compilation éveillant vos soupçons, et, dans le pire des cas, par un programme avec un comportement erronné du fait de lignes de code non prévues.
Aussi, il faut utiliser autant que possible les commentaires mono ligne qui, eux, peuvent être imbriqués sans danger dans un bloc de commentaire !
Signalons pour finir, une astuce (un peu moisie) qui utilise le préprocesseur de macros du C++ :
#if 0 code en commentaire #endif
Le bloc de code compris entre #if et #endif ne sera compilé que si l'expression suivant immédiatement #if est différente de 0. Comme ici, nous plaçons directement 0 après #if nous sommes certains que ce bloc ne sera pas compilé. Ce genre de technique n'est décrit ici qu'à but documentaire et doit être absolument proscrit ! Vous pouvez néanmoins le trouver dans de nombreux codes déjà existants.
Revenons sur les fonctions du langage C. Contrairement à la plupart des langages de programmation modernes, ce dernier n'admet qu'un seul mode de passage des paramètres : le passage par valeur. Supposons désormais qu'un sous programme ait besoin de modifier la valeur de l'un de ses paramètres. Comme seul le passage par valeur est supporté, il ne reste plus qu'à passer un pointeur sur l'objet à modifier, ce qui s'avère peu pratique et source d'erreurs. Le C++ autorise quand à lui le passage par référence, à l'instar du Pascal ou de l'ADA. Examinons le code permettant d'échanger le contenu de deux variables entières en C puis en C++ :
/* En C d'abord */ void swapEntiers(int *a, int *b) { int c = *a; *a = *b; *b = c; } int main(int argc, char *argv[]) { int i=5; int j=6; swapEntiers(&i, &j); return 0; }
On ne peut pas dire que l'utilisation des "&" soit très intuitive ...
// Et maintenant en C++ void swapEntiers(int &a, int &b) { int c=a; a = b; b = c; } int main(int, char **) { int i=5; int j=6; swapEntiers(i,j); return 0; }
Le code est beaucoup plus intuitif et moins sujet aux erreurs. La syntaxe est néanmoins trompeuse car le signe de reference "&" est le même que celui de la prise d'adresse. Il faut comprendre une référence comme étant un alias pour une variable. Examinons le code suivant :
#include <iostream> int main(int, char **) { int i=5; int j=10; int &r=i; cout << i << endl; // Affiche 5 cout << r << endl; // Affiche 5 r++; cout << r << endl; // Affiche 6 cout << i << endl; // Affiche 6 r=j; r++; cout << r << endl; // Affiche 11 cout << j << endl; // Affiche 10 cout << i << endl; // Affiche 11 ! return 0; }
L'on voit que toute manipulation sur r est immédiatement transmise sur i. En outre, l'affectation sur r ne change pas la variable à laquelle r fait référence mais bien la valeur de la variable référencée ! Après l'instruction r=j, c'est i qui se voit affecter la valeur de j et non pas r qui pointe sur j comme le prouve le code ci-dessus.
Avant de terminer avec les références, signalons de suite une différence fondamentale entre les pointeurs et les références : une référence est toujours associée à une variable. Il ne peut y avoir de référence nulle.
En outre, ce fragment de code utilise deux nouvelles fonctionnalités du C++ :
Imaginez une fonction où certains paramètres sont appelés la plupart du temps avec la même valeur. Il serait agréable de n'avoir à spécifier ces arguments que lorsque leur valeur diffère du défaut. C'est ce que vous propose les valeurs par défaut.
Ceux-ci ne doivent être spécifiés que lors de la déclaration de la fonction et ne doivent pas être rappelés lors de définition de la fonction.
Supposons, par exemple, que l'on écrive une fonction destinée à afficher une fenêtre à l'écran. Comme le montre le prototype suivant, deux paramètres sont nécessaires :
int ShowWindow(Window *p, unsigned int modeOuverture);
Valeur | Signification | Fréquence |
---|---|---|
SHOW_MAXIMIZED | Ouverture en plein écran | rare |
SHOW_MINIMIZED | Ouverture iconifiée | rare |
SHOW_CREATION | Ouverture à la taille prévue lors de la création | la pluspart du temps |
Conférons dorénavant la valeur par défaut au second paramètre, nous obtenons le prototype :
int ShowWindow(Window *p, unsigned int modeOuverture=SHOW_CREATION);
La définition ne devra pas répéter cette valeur par défaut :
int ShowWindow(Window *p, unsigned int modeOuverture=SHOW_CREATION) { // On ne rappelle pas la valeur par défaut dans la définition // Code omis pour simplification (moi aussi je me prends pour Microsoft) }
Nous montrons ici 3 appels différents de cette fonction, le premier utilise la valeur par défaut, le second et le troisième non :
int main(int, char **) { Window *p; ShowWindow(p); // Ouvre la fenêtre avec la valeur par défaut // du second paramètre : taille de création ShowWindow(p, SHOW_MAXIMIZED); // Ouvre la fenêtre en mode plein // écran => nécessaire d'utiliser // le second paramètre ShowWindow(p, SHOW_CREATION); // Rien ne vous interdit de spécifier // la valeur par défaut return 0; }
Le nombre des paramètres avec des valeurs par défaut n'est pas limité. En fait, tous les paramètres peuvent prendre une valeur par défaut. Toutefois, seuls les derniers paramètres de la fonction peuvent avoir une valeur par défaut. Par exemple, le code suivant est illégal :
void f1(int i, double angle=0.0, double longueur);
De même, considérez la fonction suivante :
void f2(double angle=0.0, double longueur=50.0);
Soudain, vous avez envie d'appeler f2 avec la valeur par défaut pour angle, mais pas pour longueur, l'appel suivant est illégal :
f2(,35.0);
Vous ne pouvez omettre que les valeurs des derniers paramètres. Sans vouloir entrer dans les détails, il vous faut savoir que cela tient au mécanisme de passage des paramètres sur la pile lors d'appel des fonctions !
Alors, une proposition pour la future norme du C++ ? il serait sans doute intéressant de proposer un mécanisme d'appel par nommage des paramètres permettant de placer n'importe où les paramètres avec valeur par défaut.
Par exemple, on pourrait appeler la fonction f1 et f2 comme suit (avec la syntaxe de l'ADA) :
f1( longueur => 12.0, i=> 3);
f2( longueur => 35.0);
Afin de comprendre l'intérêt des arguments de fonction anonymes, commençons par les mettre en usage sur un exemple simple.
A l'instar du C, le programme principal s'inscrit dans une fonction nommée main. Toutefois, en C++, seuls deux prototypes sont autorisés par la norme ANSI :
Rappelons pour la bonne bouche les significations de ces arguments dont les noms ne sont absolument pas imposés par la norme, mais sont devenus un standard de facto:
#include <iostream> using namespace std; // Pour eviter de prefixer avec std:: // les variables et constantes // d'entrees / sorties int main(int argc, char *argv[], char *env[]) { // argv[0] est le nom de l'executable cout << "Nom d'invocation de l'executable " << argv[0] << endl; // Examen de la ligne de commande if (argc == 1) { cout << "Pas d'arguments de ligne de commande" << endl; } else { cout << "Vous avez passe " << argc << " arguments" << endl; for (int compteur=1; compteur < argc ; compteur++) { cout << " Parametre de numero " << compteur << " a pour valeur : " << argv[compteur] << endl; } } // Passage en revue des variables d'environnement cout << "Examen de l'environnement d'execution" << endl; char **exaEnv=env; // On s'arrete des que l'on trouve le pointeur nul // de fin d'environnement while (*exaEnv) { cout << *exaEnv << endl; exaEnv++; } return 0; // Le programme s'est termine normalement }
Ce programme très simple permet d'afficher la liste des arguments de la ligne de commande et les variables d'environnement. La seule difficulté provient de l'utilisation de la nouvelle bibliothèque d'entrées / sorties qui sera examinée dans un prochain chapitre. Pour l'heure, il vous suffit de savoir que :
cout << expression;
Affiche expression à l'écran et que les différents appels de l'opérateur << sont empilables. Finalement, la constante endl signifie "saut de ligne".
C'est très beau tout ça, mais une fois de plus, je me suis éloigné de mon objectif : les arguments (ou paramètres) anonymes. Comme vous l'aurez remarqué, la fonction main prend obligatoirement 2 paramètres qui sont argc et argv qui devront donc impérativement apparaître dans la déclaration de votre fonction main que vous en ayez besoin ou non. Toutefois, il est navrant de recevoir un warning de votre compilateur indiquant que vous n'utilisez pas des paramètres qui vous ont été imposés. Aussi, il est possible d'indiquer au compilateur que le paramètre va être effectivement passé par l'appelant mais qu'il ne vous intéresse pas en laissant son nom en blanc.
Ainsi, si vous n'avez pas besoin des arguments de main, vous pourrez l'écrire ainsi :
int main(int , char **)
Le type reste présent (sa taille est importante pour la gestion de la pile) mais, comme il est anonyme, le compilateur ne pourra pas vous insulter pour non utilisation !
Le cas de main est un peu excessif mais vous voudrez régulièrement utiliser des paramètres anonymes, par exemple, dans les cas suivants :
Un point essentiel : si les paramètres à valeur par défaut doivent impérativement être les derniers, cette restriction ne s'applique absolument pas aux paramètres anonymes, ainsi une fonction pourrait très bien être définie ainsi :
int fonction(int a, char *b, int /* param anonyme */, double d) { }
Hormis le cas extrème de main, je recommande de toujours documenter la raison de l'anonymat d'un paramètre. Cela évitera à l'utilisateur de votre fonction de se poser trop de questions.
La surcharge est un mécanisme qui permet de donner différentes signatures d'arguments à une même fonction. Plus succinctement peut être, vous pouvez nommer de la même façon des fonctions de prototypes différents.
L'intérêt est de pouvoir nommer de la même manière des fonctions réalisant la même opération à partir de paramètres différents. Supposons, par exemple, que vous travailliez sur un système de gestion d'interface utilisateur à base de fenêtres. Chaque fenêtre est repérée par 2 systèmes différents :
Votre but est d'écrire 2 fonctions qui permettent de récupérer un pointeur sur une fenêtre en utilisant soit l'identificateur numérique, soit le nom. Avec un système sans surcharge, il vous faudrait utiliser 2 noms de fonctions différents, par exemple :
Window *GetWindowIdent(unsigned long identificateurNumerique); Window *GetWindowName(const char *nom);
Avec le mécanisme de surcharge, vous pouvez utilisez le même nom :
Window *GetWindow(unsigned long identificateurNumerique); Window *GetWindow(const char *nom);
Comment le compilateur fait-il pour s'y retrouver ? C'est en fait très simple : le nom « interne » de la fonction contient la liste des paramètres. De la sorte, à la compilation, en consultant la liste des paramètres effectifs, le compilateur établit quelle est la forme de la fonction à appeler.
Une limitation à la surcharge : il est impossible d'avoir des fonctions qui ne diffèrent que par leur type de retour. En effet, le compilateur ne peut pas les distinguer lorsque l'on omet de récupérer la valeur retournée.
Joie ! contrairement au C, il y a de vraies constantes en C++ ! Vous vous en souvenez surement, en C, lorsque l'on voulait utiliser une constante, il fallait utiliser le préprocesseur. Par exemple, pour définir une constante entière égale à 5 et nommée TAILLE, il fallait déclarer :
#define TAILLE 5
.. et ne surtout pas oublier de ne PAS mettre de ";" à la fin ! Maintenant cette époque est révolue car nous disposons de vraies constantes. Par exemple, pour spécifier la taille d'un tableau, vous pouvez faire :
const int TAILLE=5; int tab[TAILLE];
Bien entendu, il vous est possible de définir n'importe quel type de données constantes :
const double PI=3.0; // Largement suffisant pour la plupart des applications (si si !) const double EXP_1=2.718; const char MOI[]="Ours Blanc des Carpathes";
De fait, je ne veux plus jamais voir de #define pour définir des constantes vous avez suivi au fond ?
En parlant de constantes, êtes vous surs de bien savoir comment l'on utiliser les pointeurs constants ? par exemple, la différence entre un const char * et char const *
Le tableau suivant exprime les différences subtiles sur ces 2 notions en indiquant si des opérations de modification (par exemple, l'application de l'opérateur ++) sont possibles sur le pointeur ou l'objet pointé :
Déclaration | « Décodage » | Pointeur modifiable ? (p++) |
( (*p)++ ) |
---|---|---|---|
const int *p | p est un pointeur sur entier constant | oui | non |
int const *p | p est un pointeur constant sur un entier | non | oui |
const int * const p | p est un pointeur constant sur un entier constant | non | non |
Donc, par extension, const char * est un type très utilisé car il s'agit d'un pointeur vers une chaîne de caractère constante ! c'est donc le type de choix lorsque vous souhaitez passer une chaîne de caractères à une fonction qui n'est pas sensée la modifier !
Oubliés les odieux printf et scanf, voici venu le temps des fonctionnalités d'entrées / sorties orientées objet. Ces dernières reposent sur la notion de flux.
Les flux standard
Trois objets de type flux sont proposés en standard. Le tableau suivant récapitule leurs caractéristiques principales :
Objet Flux | Périphérique "C" | Description |
---|---|---|
cout | stdout | Flux associé à la sortie standard (écran) |
cin | stdin | Flux associé à l'entrée standard (clavier) |
cerr | stderr | Flux associé à la sortie des erreurs (écran) |
Leur utilisation se fait principalement au travers des opérateurs de redirection en sortie "<<" et en entrée ">>". Ainsi, l'écriture à l'écran d'une expression se traduit par le code suivant :
Sans entrer dans les détails, il vous faut savoir que cela sous entend la surcharge de l'opérateur << pour le type de donnée correspondant à expression. Ce travail a été réalisé pour la plupart des types atomiques du C++. Il vous appartiendra de le faire pour vos propres classes, comme nous le verrons un peu plus tard.
Point intéressant : les opérateurs << sont prévus pour être
Quelques exemples impliquant cout :
Nous avons deux variables entières i et j et nous souhaitons les imprimer à l'écran :
Code résultant
et seront exposées ici d'ici la fin de la semaine prochaine ! (notez la vilaine répétition du mot « ici » )