Transition du C au C++
Cet article propose une brève transition du C « classique » au C++. On suppose ici que les notions essentielles du premier langage sont maîtrisées, mais la plupart des notions abordées sont assez auxiliaires car les grandes avancées du C++ font l'objet d'articles à part entières.
Le b.a.-ba
Les fichiers d'en-tête
Le C++ présente une première différence notable avec son prédecesseur : les fichiers inclus ne présentent plus d'extension .h. Prenez-y garde : si vous ajoutez l'extension quand même, il se peut que le compilateur ne produise aucune erreur, mais dans ce cas il utilisera une ancienne version du langage (conservée dans un dossier backward de l'arborescence).
Pour utiliser les en-têtes connus du C, il vous faudra préfixer le nom de fichier par c et supprimer l'extension : par exemple, stdio.h devient cstdio.
Les en-têtes standards du C++ ne présentent pas d'extension non plus, et l'équivalent C++ de stdio.h est iostream.
Un grand classique
Rares sont ceux qui y ont échappé, nous allons donc commencer par un généreux Hello World. Souvenez-vous que les fonctions du C existent toujours en C++, mais qu'il est conseillé d'utiliser les fonctions de la Bibliothèque Standard du C++, aussi connue sous le nom de STL (pour Standard Toolkit Library).
#include <iostream> using namespace std; int main() { cout << "Hello World !!!" << endl; cin.ignore(); return 0; }
Une fois le fichier d'en-tête inclus, nous spécifions au programme que nous utilisons le namespace std, c'est à dire le jeu de fonctions standards. Cette ligne n'est pas toujours nécessaire au bon fonctionnement du programme, mais elle permet parfois de simplifier les expressions.
Vient ensuite la fonction principale : rien de nouveau par rapport au C, mais désormais la fonction cout (pour C-Out) remplace l'ancienne printf. La syntaxe de cette fonction est assez particulière, nous allons la voir en détail plus loin. Pour l'instant, remarquez que les constantes sont séparées par des opérateurs de décalage de bit : la chaîne de caractère, mais aussi la constante endl, qui est une équivalente à '\n'.
Déclarations
En C++, il est possible de déclarer des variables n'importe où dans le code, même entre deux instructions au beau milieu d'une fonction. Il faut cependant veiller à ne pas en abuser, le code pouvant rapidement devenir incompréhensible.
void feelShadock(int a) { cout << "Comment remplir une telle fonction ?" << endl << endl << "En citant les Shadocks !" << endl; int b = a * a; (b > a) ? cout << "Ga Bu !" << endl : cout << "Zo Meu !" << endl; int c = sqrt(b); (c == a) ? cout << "Zo Meu !" << endl : cout << "Ga Bu !" << endl; }
Ce code est un pur exemple : son inutilité, son inintelligibilité et sa stupidité ne regardent que l'auteur !
Gestion de la mémoire
Pointeurs et références
Le C++ hérite de la notion de pointeur du C, mais il y ajoute également la notion de référence. L'idée de la référence est de traiter l'objet comme s'il avait été déclaré dans le même bloc, ce qui s'avère particulièrement utile lors du passage par argument à une fonction. On rappelle qu'avec un pointeur, il faut procéder à quelques petites acrobaties pour écrire une valeur à l'adresse de l'objet.
Une référence est déclarée de la même façon qu'un pointeur mais en remplaçant l'étoile par une esperluette &.
En C, on aurait pu écrire ce qui suit :
void affecteHuit(int *a) { *a = 8; } void utilisation() { static int a = 0; affecteHuit(&a); }
On voit que cette syntaxe oblige à deux acrobaties : la première lors du passage de l'adresse de la variable (préfixe & ), puis l'affectation de la valeur dans la zone mémoire associée (préfixe * ). Ces complications, si elles sont parfois nécessaires, constituent également des sources d'erreur notables.
On peut reformuler l'exemple précédent en utilisant les références :
void affecteHuit(int &a) { a = 8; } void utilisation() { static int a = 0; affecteHuit(a); }
Les deux points délicats ont disparu, le passage par référence permettant le traitement de la variable comme si elle avait été déclarée directement dans le bloc.
Précisions
En termes d'occupation mémoire, le passage par référence a les mêmes avantages que l'utilisation des pointeurs : une référence a la dimension d'une adresse mémoire, comme un pointeur, et c'est le compilateur qui se charge généreusement du travail de conversion (et non plus le malheureux programmeur en détresse).
En-dehors des arguments de fonctions, vous pouvez très bien déclarer une référence dans un bloc. Il est cependant nécessaire d'initialiser celle-ci dès sa déclaration, alors que ce n'était pas le cas avec les pointeurs. Ainsi, le code qui suit est correct :
int reponse = 42; int &refReponse = reponse; refReponse *= 51;
Attention : ne confondez pas le & utilisé pour les références et l'opérateur d'adressage utilisé avec les pointeurs.
Dernière précision : comme c'est le cas avec les pointeurs, il n'existe aucune référence nulle.
Allocation de la mémoire
Les nouveaux opérateurs d'allocation du C++ sont new et new[]. Le premier sert à allouer de la mémoire pour une seule instance d'un type donné, tandis que le second est utilisé pour allouer un tableau d'éléments. On emploie la syntaxe objet = new type souvent plus intuitive que l'appel à la fonction malloc nécessaire en C.
Exemples
Pour allouer un entier en C, on peut procéder ainsi :
int *i = (int*)malloc(sizeof(int));
En voici un équivalent en C++ :
int *i = new int;
Par ailleurs, pour allouer un tableau en C, on doit faire un produit du type :
int *t = (int*)malloc(8 * sizeof(int));
Une version C++ sans doute plus explicite :
int *t = new int[8];
Pour libérer les variables allouées ci-dessus on utilise les opérateurs delete (associé à new) et delete[] (associé à new[]) suivant la syntaxe delete objet. Il y a ici peut de différences avec l'ancienne fonction free du C, si ce n'est l'absence de parenthèses. Ainsi, le code suivant libère les ressources allouées plus haut.
// En C free(i); free(t); // En C++ delete i; delete[] t;
Nous verrons que ces nouveaux opérateurs sont directement liés à la notion d'objet qu'apporte le C++.
Économie du préprocesseur
L'usage du préprocesseur est source de nombreux bugs depuis son avènement, notammant en ce qui concerne les macros d'instructions, ce qui a amené les concepteurs du C++ à définir de nouvelles normes pour déclarer des constantes et optimiser les fonctions brèves.
Constantes
Désormais, on déclare les constantes comme des variables, mais en précédant le type par le mot-clef const. Cette approche a l'avantage de typer les constantes, ce qui n'est pas le cas si l'on utilise le préprocesseur.
const int ANSWER = 42; const double PI = 3.14159;
Fonctions inline
Les macros du C se contentent de substituer le code tel quel à l'emplacement de l'appel, ce qui entraîne au moins deux problèmes : le recalcul des expressions à chaque évaluation, et les erreurs lorsque les expressions sont mal parenthésées. Pour y remédier, l'idée est d'implémenter des fonctions classiques en spécifiant au compilateur de les optimiser, évitant au passage les recalculs et les confusions.
Une fonction inline est donc une fonction dont le corps est substitué à son appel dans le code, ce qui évite de passer par la pile et de brancher le code en cours d'exécution au corps de la fonction à chaque fois (ce qui revient à perdre du temps pour rien avec les fonctions de petite taille). Exemple :
inline void inlineAire(int x, int y) { cout << x * y << endl; }
A noter que déclarer une fonction comme inline n'est qu'une demande faite au compilateur : pour des raisons technique celui-ci peut ne pas en tenir compte (comme, par exemple, lorsque la fonction est trop conséquente).
Entrées et sorties
Sortie standard
Nous allons nous intéresser à cette drôle d'instruction : cout. Les apparences sont trompeuses : il s'agit bien d'une instruction, car en C++ vous pouvez redéfinir les opérateurs pour manipuler de nouveaux objets. Peu importe pour l'instant, retenez simplement que cette syntaxe particulière permet un nombre variable d'arguments sans jouer avec des drapeaux compliqués comme c'était le cas avec printf. Par exemple :
int main(void) { int xA = 5, yA = 8, zA = 4; int xB = 4, yB = 8, zB = 5; cout << "Deux vecteurs :" << endl << endl << "A (" << xA << " , " << yA << " , " << zA << ")" << endl << endl << "B (" << xB << " , " << yB << " , " << zB << ")" << endl; return 0; }
Le résultat est le suivant :
Deux vecteurs :
A (5 , 8 , 4)
B (4 , 8 , 5)
Remarquez que l'opérateur << concatène les valeurs qu'il encadre en une seule chaîne de caractères qui est écrite en sortie. Cette syntaxe particulière est assez pratique : vous pourrez découper vos expressions comme bon vous semble pour rendre le code plus lisible, par exemple en l'écrivant sur plusieurs lignes comme c'est le cas ci-dessus.
Des conversions de type sont effectuées automatiquement sur les variables d'entrée : peu importe que celles-ci soient de type entier, chaîne ou nombre à virgule flottante, elles seront automatiquement affichées dans le format adéquat. Nous verrons plus tard comment revenir sur la mise en forme de la sortie avec cout.
Deux dérivés
Il existe également deux autres instructions d'écriture : cerr et clog. Elles s'utilisent exactement comme cout mais elles écrivent respectivement dans les fichiers stderr et stdio (!) ce qui les destinne plutôt aux rapports de bugs et autres messages de développement.
Entrée standard : cin
cin
lire C-IN) est l'équivalent de cout pour les valeurs lues en entrée. Elle s'utilise de la même façon, mais en utilisant le décalage de bits vers la droite pour représenter le déplacement de la valeur dans la variable. Ainsi, pour récupérer une chaîne de caractère entrée par l'utilisateur, on peut coder ce qui suit :
int main() { char reponse[42]; cout << "Entrez la reponse vitale, universelle et restielle :" << endl; cin >> reponse; cout << "Vous avez bien entre : " << reponse << "." << endl; return 0; }
Il en va de même pour tous les types de variables, cin se chargeant automatiquement de convertir les données entrées vers le type de la variable de destination.
Précision
Ce qui est est adressé à ceux qui veulent savoir ce qui se cache derrière la syntaxe particulière de cout et cin. En réalité, il s'agit de classes qui redéfinissent les opérateurs de décalage et disposent d'un ensemble de champs et de fonctions pour le formatage (c'est ainsi que, plus loin, nous renseigneront ces champs comme pour une structure).
Les fichiers
Utiliser les fichiers est aussi simple qu'utiliser cout et cin. Vous devrez inclure l'en-tête fstream pour les utiliser. Commençons par créer un objet de type fichier : en C, il n'existait que le type FILE ; en C++ il existe deux, ifstream et ofstream, respectivement pour les fichiers en lecture et en écriture.
Écrire dans un fichier : ofstream
Une fois qu'un objet de type ofstream est déclaré, on écrit dedans de la même façon qu'avec cout, mais en remplaçant cout par le nom de l'objet. Ainsi, on peut écrire du texte et une valeur dans un fichier pi.txt comme suit :
void ecrirePi() { ofstream fichier("adresse.txt"); if (!fichier) { cerr << "Impossible d'ouvrir le fichier!" << endl; return (-1); } fichier << "Texte a inserer :" << 3.1415926535 << endl; fichier.close(); }
La différence avec le C vient de ce que l'on appelle en C++ le constructeur du fichier, c'est à dire une fonction appelée à la création de l'objet, ce qui explique la déclaration du nom de fichier comme une fonction. Ici, notre objet fichier appelle dès sa création une méthode qui va l'initialiser. Celle-ci prend en argument l'adresse du fichier à créer.
Une fois le fichier ouvert, on peut l'utiliser aussi simplement que cout et y insérer variables et constantes. Une fois toutes les opérations terminées, il ne faut pas oublier de fermer l'accès avec la méthode close(). En cas d'oubli, celle-ci sera appelée automatiquement à la destruction de l'objet, mais mieux vaut expliciter le moment à partir duquel vous n'avez plus besoin d'écrire dans le fichier.
Lire un fichier : ifstream
On lit dans un fichier de la même façon qu'avec cin et en suivant le même processus d'initialisation et de clôture qu'avec ofstream :
void lireTableau() { ifstream fichier("adresse.txt"); if (!fichier) { cerr << "Impossible d'ouvrir le fichier!" << endl; return (-1); } char txt[1024]; fichier >> txt; cout << "Texte : " << endl << endl << txt << endl << endl; fichier.close(); }
Conclusion
Voilà qui complète notre tour d'horizon des nouvelles méthodes de gestion du flux d'entrée/sortie.