Cette page s'adresse aux étudiants de L3 de l'ISFA, promotion 2016-2017.
Après avoir fait un semestre de Python, vous devez maintenant faire du C++. Cette page contient un résumé des concepts et de la syntaxe abordés en cours.
Syntaxe de base, déclaration de variables, types élémentaires, entrées-sorties.
Python est un langage interprété : un programme appelé un interpréteur lit chaque ligne et les exécute consécutivement.
C++ est quant à lui un langage compilé : un programme appelé un compilateur va transformer le code en un fichier exécutable, en langage machine, que l'ordinateur pourra directement exécuter.
Les fichiers de code C++ ont l'extension .cpp.
Pour indiquer la fin d'une instruction, au lieu d'aller à la ligne, vous devez mettre un point-virgule.
Pour séparer les blocs d'instruction, au lieu de faire des tabulations, vous devez mettre des accolades.
Le code C++ doit, afin d'être exécuté, se trouver dans une fonction main. La syntaxe est la suivante :
int main(void)
{
return 0;
}
Le code doit se situer avant le return.
Contrairement à Python, les variables doivent être déclarées avant d'être utilisées. Ceci est dû au fonctionnement interne du C++, qui va réserver de l'espace mémoire pour les variables en fonction de leur type. La syntaxe est la suivante :
type nom;
Il est également possible d'initialiser les variables dès leur déclaration :
type nom = valeur;
Le C++ a de multiples types de base :
int représente les entiers. Ceux-ci peuvent prendre des valeurs comprises entre -2 147 483 648 et 2 147 483 647. Exemples : int a=32; a=-8;
unsigned int représente les entiers positifs. Ceux-ci peuvent prendre des valeurs comprises entre 0 et 4 294 967 295. Exemples : unsigned int i=0; i=2345190981;
double représente les nombres réels. Exemples : double x=0.121; x=43.8990;
char représente les caractères simples. Exemples : char c='c'; c='w'; Notons que les caractères sont compris entre apostrophes.
bool représente les booléens. Exemples : bool b = true; b=false; Notons que true et false ne prennent pas de majuscules en C++.
Voici un exemple de code C++ effectuant l'addition de deux nombres :
int main(void)
{
int a,b;
a=8;
b=10;
int c=a+b;
return 0;
}
Afin d'importer des bibliothèques en C++, vous devez placer un #include<bibliotheque> tout en haut de votre fichier de code. De nombreuses bibliothèques existent et fournissent des fonctions et structures de données utiles.
La première de ces bibliothèques que nous allons utiliser est la bibliothèque string, qui permet d'utiliser les chaines de caractères.
Une fois que vous avez placé #include<string> en haut de votre fichier de code, vous pouvez créer des variables contenant des chaines de caractères. Le type des chaines de caractères en C++ est std::string et les chaines de caractères sont, comme en Python, utilisées avec des apostrophes. Par exemple :
std::string s = "Hello World !";
Notez bien que les types char et std::string sont complètement différents ! Une variable de type char ne peut en effet contenir qu'un seul caractère.
Pour lire et écrire sur la sortie standard, il nous faut utiliser la bibliothèque iostream : placez #include<iostream> en haut de votre fichier de code.
L'écriture s'effectue à l'aide d'une fonction appelée std::cout qui s'utilise avec la syntaxe suivante :
std::cout << sortie;
Par exemple, en reprenant le code précédent, si nous souhaitons afficher la valeur de c, nous pouvons écrire :
int main(void)
{
int a,b;
a=8;
b=10;
int c=a+b;
std::cout << c;
return 0;
}
Afin d'aller à la ligne, il est possible d'utiliser le caractère \n, ou bien d'écrire std::endl à la suite d'un std::cout :
int main(void)
{
int a,b;
a=8;
b=10;
int c=a+b;
std::cout << c << std::endl;
return 0;
}
Pour lire sur l'entrée standard, nous utilisont une fonction appelée std::cin qui s'utilise avec la syntaxe suivante :
std::cin >> entrée;
Par exemple, en reprenant le code précédent, si nous souhaitons demander à l'utilisateur les valeurs de a et b, nous pouvons écrire :
int main(void)
{
int a,b;
std::cin >> a >> b;
int c=a+b;
std::cout << c << std::endl;
return 0;
}
Assurez-vous toujours que vous avez déclaré les variables avant de lire sur l'entrée standard, et que leur type correspond à ce qui sera lu !
Notez que quand on utilise std::cin pour lire des chaines de caractère, on lit la chaine qui s'interrompt à la première espace. Afin de lire toute une ligne, on peut, si s est une std::string, effectuer getline(std::cin,s) afin que toute une ligne soit affectée dans s.
La boucle for a la syntaxe suivante :
for(initialisation ; condition ; modification) { instructions }
Cette boucle va effectuer, dans l'ordre : l'instruction initialisation, puis tant que la condition sera respectée, elle va exécuter instructions puis modification. Ainsi, afin d'afficher tous les entiers de 0 à 9, on peut effectuer :
int main(void)
{
for(unsigned int i = 0; i<10; ++i)
{
std::cout << i << std::endl;
}
return 0;
}
Dans l'exemple précédent, nous avons utilisé l'instruction ++i. En C++, si i est un entier, on a équivalence entre les instructions suivantes :
i=i+1
i+=1
i++
++i
On préférera généralement utiliser l'instruction ++i pour incrémenter la valeur de i (et de même, l'instruction --i pour la décrémenter).
La boucle while a la syntaxe suivante :
while(condition) { instructions }
Cette boucle va exécuter instructions tant que la condition est respectée. Ainsi, afin d'afficher tous les entiers de 0 à 9, on peut effectuer :
int main(void)
{
unsigned int i = O;
while(i<10)
{
std::cout << i << std::endl;
++i;
}
return 0;
}
Le C++ introduit également une autre type de boucle, la boucle do while :
do { instructions } while(condition);
La principale différence entre les boucles while et do while est que cette dernière sera exécutée au moins une fois avant que condition ne soit évaluée.
La structure de contrôle if a la syntaxe suivante :
if(condition) { instructions } else { instructions }
Notez que le else est facultatif, tout comme en Python. Notez également que elif n'existe pas en C++.
En exemple d'utilisation, affichons tous les nombres pairs entre 0 et 9 :
int main(void)
{
for(unsigned int i = 0; i<10; ++i)
{
if (i%2 == 0)
{
std::cout << i << std::endl;
}
}
return 0;
}
Afin d'éviter d'imbriquer des if et des else quand on peut avoir de nombreux cas, il existe une structure de contrôle appelée le switch qui a la syntaxe suivante :
switch(expression) {
case constante1:
instructions1
break;
...
case constantek:
instructionsk
break;
default:
instructions
break;
La variable expression va être évaluée. Si elle est égale à constante1, les instructions1 seront exécutés, et sinon on vérifie si elle est égale à constante2, ..., constantek. Le cas default correspond au cas où expression n'est égale à aucun des constante1, ..., constantek (il est facultatif). Ainsi, par exemple, si on souhaite afficher des choses différentes en fonction du modulo 4 des nombres entre 0 et 9, on peut faire ainsi :
int main(void)
{
for(unsigned int i = 0; i<10; ++i)
{
switch(i%4)
{
case 0:
std::cout << "Multiple de 4" << std::endl;
break;
case 1:
std::cout << "4k+1" << std::endl;
break;
case 2:
std::cout << "Pair mais pas multiple de 4" << std::endl;
break;
default:
std::cout << "4k+3" << std::endl;
break;
}
}
}
En C++, on définit les fonctions de la façon suivante :
type_retour nom(type1 arg1, ... , typek argk) { instructions }
La syntaxe est très similaire à celle du Python, à part que le type de chaque argument doit être précisé, et que la fonction doit avoir un type de retour. Vous devez donc faire un return d'une variable ou d'une valeur de type type_retour. Ainsi, par exemple, une fonction sum qui effectue la somme de deux entiers se définit ainsi :
int sum(int a, int b)
{
int c = a+b;
return c;
}
Notez qu'une fonction peut ne rien renvoyer, auquel cas on écrira void comme type de retour. Le type de retour void interdit d'avoir un return dans la fonction. Ainsi, par exemple, une fonction afficher qui affiche un entier se définit ainsi :
void afficher(int a)
{
std::cout << a << std::endl;
}
Syntaxe des tableaux statiques.
Les tableaux en C++ sont typés, au même titre que les variables. La syntaxe pour les déclarer est la suivante :
type nom[taille];
Ceci déclare un tableau de taille cases, qui sont indexées de 0 à taille-1.
Notez que le tableau est déclaré, mais pas initialisé. Vous devez donc le remplir par la suite.
Par la suite, on peut utiliser les tableaux comme en Python pour accéder et modifier des éléments :
int main(void)
{
int t[3];
t[0]=1;
std::cin >> t[1];
t[2]=t[0]-t[1];
}
Il existe un moyen d'initialiser le tableau dès la déclaration :
type nom[taille] = { elt1, elt2, ..., elttaille };
Cette syntaxe est utile pour initialiser des tableaux de petite taille. Une boucle for est souvent plus utile.
On peut définir des tableaux à deux dimensions :
type nom[taille1][taille2];
Bien entendu, on peut définir des tableaux avec autant de dimensions qu'on le souhaite.
Structures de données.
Parfois, on souhaite rassembler plusieurs données dans une seule structure. Pour cela, on utilise le mot-clef struct :
struct nom
{
type1 champ1;
...
typen champn;
};
Par la suite, on pourra utiliser nom comme n'importe quel type.
On peut utiliser n'importe quels types dans la structure. Ainsi, par exemple, on peut définir une structure etudiant de la façon suivante :
struct etudiant
{
std::string nom;
int numero;
int notes[5];
};
Cette structure crée donc un nouveau type etudiant qui rassemble trois données : le nom (représenté par une chaine de caractères), le numéro (représenté par un entier) et les notes (représentées par un tableau de cinq entiers).
Une fois la structure définie, on peut l'utiliser comme un type standard. Par exemple, on peut déclarer :
etudiant e;
On accède aux champs de la façon suivante :
e.nom = "Dailly";
std::cin >> e.numero;
Afin d'initialiser la structure, on peut soit remplir les champs comme montré précédemment, soit faire ainsi :
etudiant e = {"Dailly",12345678,{10,10,10,10,10}};
La syntaxe est donc la même que pour initialiser un tableau.
Les structures peuvent être utilisées comme des types quelconques ; on peut créer des tableaux de nos types structurés, et les utiliser comme type de retour ou de paramètre de fonctions. Par exemple, une fonction d'initialisation ressemblerait à :
etudiant creer(void)
{
etudiant e;
std::cin >> e.nom >> e.numero;
e.notes = {10,10,10,10,10};
return e;
}
Tandis qu'une fonction d'affichage serait :
void affiche(etudiant e) {
std::cout << "L'étudiant s'appelle " << e.nom << " et a pour numéro " << e.numero << ". Ses notes ce semestre sont :" << std::endl;
for(unsigned int i = 0; i < 5; ++i)
{
std::cout << e.notes[i] << std::endl;
}
}
Et une fonction modifiant une note de l'étudiant serait :
etudiant modifierNote(etudiant e, int i, int nouvelleNote)
{
e.notes[i] = nouvelleNote;
return e;
}
Pointeurs, allocation dynamique.
Lorsque l'on déclare une variable, elle est créée dans la mémoire à une case spécifique, appelée adresse. Pour accéder à l'adresse d'une variable a, on utilise la syntaxe &a. Exemple d'utilisation :
int a;
std::cout << &a << std::endl;
Les pointeurs sont des types de données spécifiques servant à contenir l'adresse de variables. De façon moins formelle, les pointeurs sont des variables qui permettent d'avoir un accès mémoire direct à d'autres variables.
Pourquoi utiliser les pointeurs ? Exemple pratique, si l'on souhaite échanger la valeur de deux variables entières a et b, le code est le suivant :
int tmp = b;
b = a;
a = tmp;
Cependant, si cet échange de valeur est fréquent dans le code, on voudra éviter de le recopier à chaque fois. Pour cela, on va créer une fonction echange qui effectuera cet échange, et que l'on appellera à chaque fois que nécessaire. Seulement, si l'on déclare la fonction de cette façon :
void echange(int a, int b)
{
int tmp = b;
b = a;
a = tmp;
}
Et que l'on appelle la fonction sur deux variables en effectuant echange(a,b), leurs valeurs ne seront pas changées ! En effet, lorsque l'on entre dans une fonction, les paramètres sont copiés en mémoire, la fonction travaille sur ces copies, puis les détruit lorsqu'elle se termine. Afin de modifier directement les variables originales, il faut donc connaître leur position dans la mémoire. C'est exactement ce que nous permettent de faire les adresses !
À partir de là, on peut définir un pointeur comme une variable contenant une adresse.
Si on déclare la variable suivante :
type a;
On déclare un pointeur pointant sur cette variable de la façon suivante :
type *ptr = &a;
Par la suite, on peut soit modifier le pointeur (pour qu'il pointe vers une variable différente du même type par exemple), soit modifier la variable pointée de la façon suivante :
*ptr = valeur;
Cette opération s'appelle le déréférencement : le programme va lire le contenu de la variable ptr, se rendre à l'adresse indiquée, et modifier la mémoire à ladite adresse.
Exemple concret avec l'échange vu plus haut :
void echange (int *ptr1, int *ptr2)
{
int tmp = *ptr2;
*ptr2 = *ptr1;
*ptr1 = tmp;
}
Cette fonction effectue donc les opérations suivantes :
1- Placer la valeur contenue dans la case pointée par ptr2 dans une variable tmp de type entier ;
2- Placer la valeur contenue dans la case pointée par ptr1 dans la case pointée par ptr2 ;
3- Placer la valeur contenue dans la variable tmp dans la case pointée par ptr1.
Si l'on appelle la fonction echange(&a,&b), on va donc effectivement modifier les valeurs de a et de b grâce à l'accès mémoire.
Pour initialiser un pointeur, on peut utiliser le mot-clef NULL. Ainsi, la ligne :
type *ptr = NULL;
déclare un pointeur ne pointant nulle part. Cela permet d'éviter les erreurs mémoire en testant, avant d'utiliser le pointeur, s'il est égal à NULL.
Les pointeurs sont l'une des notions les plus complexes de la programmation, mais également l'une des plus puissantes. N'hésitez pas à dessiner votre mémoire pour bien comprendre ce qui se passe.
Si vous êtes intéressés, il existe une arithmétique des pointeurs. Cependant, n'allez explorer cette notion que si vous êtes déjà bien à l'aise avec les pointeurs.
Dans le cours précédent, nous avions une structure etudiant composée de multiples types de données. Lorsque l'on appelle une fonction qui travaille sur un etudiant, on recopie donc la variable dans la mémoire. Si cette copie est effectuée souvent ou sur des structures contenant beaucoup de données, cela va devenir très coûteux.
Les pointeurs permettent de s'épargner la copie en mémoire, vu qu'ils permettent d'accéder directement à nos variables dans la mémoire.
Attention cependant, pour accéder à un champ depuis un pointeur sur une structure, on a deux syntaxes équivalentes :
type_structure x;
type_structure *ptr = &x;
(*ptr).champ = valeur;
ptr->champ = valeur;
Ainsi, pour réécrire la fonction de modification de notes de façon à ne pas recopier inutilement des données en mémoire, on peut procéder ainsi :
void modifierNote(etudiant *ptr, int i, int nouvelleNote)
{
e->notes[i] = nouvelleNote;
}
Quand est-il intéressant d'utiliser un pointeur plutôt que de recopier en mémoire ? Dans deux cas :
1- Quand on a besoin de l'accès mémoire direct (cas de la fonction echange développé ci-dessus) ;
2- Quand ce que l'on recopie prend plus d'espace mémoire qu'un pointeur.
Afin de connaître l'espace mémoire occupé par une variable d'un certain type, C++ dispose d'une fonction nommée sizeof. On l'utilise de la façon suivante :
std::cout << sizeof(type) << std::endl;
La fonction sizeof prend donc en paramètre un type de données (prédéfini comme int ou std::string, mais aussi les types structurés que vous pouvez créer), et renvoie un entier qui est le nombre d'octets que prendra une valeur dudit type.
Ainsi, un int occupe 4 octets, un double en occupe 8, une std::string en occupe au moins 32, et un pointeur quelconque en occupe 8.
En C, l'ancêtre du C++, il était impossible de définir des tableaux de taille non fixe (c'est encore le cas selon le compilateur C++). Pour contourner ceci a été créée l'allocation dynamique, qui consiste à réserver un espace mémoire, ce qui équivaut à créer un tableau.
La syntaxe pour réserver l'équivalent d'un tableau de taille éléments est la suivante :
type *t = (type*)malloc(taille*sizeof(type));
Derrière son apparence barbare, cette ligne est en fait très simple : malloc est une fonction permettant de réserver directement de l'espace mémoire, elle prend en paramètre la taille de l'espace nécessaire (qui est donc taille fois l'espace mémoire occupé par une variable du type souhaité). Comme malloc renvoie un pointeur générique, il faut convertir l'adresse renvoyée dans le bon type de pointeur.
Notez que malloc est censé renvoyer NULL si l'allocation a échoué.
Par la suite, la zone mémoire ainsi définie peut s'utiliser comme un tableau : on accède au ième élément de t en écrivant t[i]. Simplement, on peut utiliser t comme un pointeur, ce qui simplifie la syntaxe des fonctions. Ainsi, pour calculer la somme des éléments de t tableau de taille entiers, on peut définir la fonction suivante :
int sommeTableau (int *t, int taille)
{
int res = 0;
if (t != NULL)
{
for (unsigned int = 0; i < taille; ++i)
{
res+=t[i];
}
}
return res;
}
Notez que l'on peut ainsi créer des tableaux de n'importe quel type... y compris des tableaux de tableaux. Par exemple, pour allouer dynamiquement une matrice d'entiers de m lignes et n colonnes, on effectue le code suivant :
int **mat = (int**)malloc(m*sizeof(int*));
if (mat != NULL)
{
for (unsigned int i = 0; i < m; ++i)
{
mat[i]=(int*)malloc(n*sizeof(int));
}
}
On accède alors à une case comme pour une matrice classique : mat[i][j].
Toute mémoire allouée explicitement doit être désallouée explicitement. C'est l'un des principes les plus importants du langage C++. En pratique, cela peut se traduire par Tout malloc doit être free.
Afin de désallouer une zone mémoire t allouée via malloc, on utilise simplement la ligne suivante :
free(t);
Si on a utilisé l'allocation dynamique pour créer une matrice, on doit désallouer chacune de ses lignes avant de la désallouer elle-même. En pratique, cela donne le code suivant (en reprenant les mêmes notations que ci-dessus) :
for (unsigned int i = 0; i < m; ++i)
{
free(mat[i]);
}
free(mat);
La STL, les conteneurs, les itérateurs.
La STL (pour Standard Library, ou Bibliothèque Standard) est un ensemble de modules contenant des structures de données et des fonctions utilitaires pour C++.
Nous utilisons la STL depuis le début du cours : la bibliothèque iostream est un module de la STL.
Voici deux sites qui contiennent de nombreuses informations sur la STL :
CPPreference
CPlusPlus.com
Si vous cherchez des fonctions particulières, un type de données adapté à votre problème, ou bien que vous avez oublié la syntaxe dont vous avez besoin, n'hésitez surtout pas à vous en servir.
Cette page ne détaillera pas l'intégralité de la STL, et se contentera de vous donner quelques pistes d'exploration.
Une fois que vous aurez accès aux outils de la STL, vous aurez une immense versatilité pour répondre aux problèmes auxquels vous faites face. Il deviendra donc critique, pour vous, lorsque vous préparerez un algorithme répondant à un problème donné, de vous poser la question de la structure la plus adaptée pour l'implémenter. Ainsi, vous aurez intérêt à utiliser des structures semblables aux listes de Python si vous devez faire des opérations dessus (calcul de longueur, ajout/suppression d'élément...), tandis qu'il sera plus intéressant d'utiliser des tableaux statiques si vous devez simplement stocker un nombre connu de données.
La bibliothèque string que nous utilisons depuis le début sert à travailler de façon assez pointue sur des chaines de caractère.
Techniquement, on peut voir un std::string comme un tableau dynamique de char.
Vous trouverez ici un fichier de code décrivant certaines utilisations de cette structure de données.
Les conteneurs sont des structures servant à contenir des données. Il en existe plusieurs, ayant chacun leur intérêt algorithmique. Ici, nous présenterons surtout deux d'entre eux (vector et map), mais selon les cas il peut être intéressant d'utiliser des conteneurs adaptés à des données particulières.
Un vector est un tableau dynamique. Il s'agit de l'équivalent des listes de Python.
Afin d'utiliser vector, il faut ajouter #include<vector> en haut de son fichier. Par la suite, on déclare un vector de la façon suivante :
std::vector<type> nom;
Vous trouverez ici un fichier de code décrivant certaines fonctionnalités des vector.
Une map est l'équivalent des dictionnaires de Python : un tableau indexé par autre chose que des entiers de 0 à n-1.
Plus formellement, une map est un ensemble de paires (clef,valeur) où toutes les clefs sont uniques.
Vous trouverez ici un fichier de code décrivant certaines fonctionnalités des map.
Avec les map se pose une question : comment parcourir une structure de données qui n'est pas indexée par des entiers de 0 à n-1 ? En C++ ont été définis des types particuliers de pointeurs, appelés itérateurs, afin d'effectuer des parcours de structure.
Si on définit la structure suivante :
std::map<std::string,int> m;
Un itérateur sur cette structure se définira de la façon suivante :
std::map<std::string,int>::iterator it;
Une structure possède deux valeurs particulières pour les itérateurs : begin() (qui pointe sur le début de la structure) et end() (qui pointe après la fin de la structure). Ainsi, un parcours de la structure s'écrira de la façon suivante :
for (std::map<std::string,int>::iterator it = m.begin() ; it != m.end() ; ++it)
Par la suite, comme pour les pointeurs, on accédera à l'élément de la structure pointé par l'itérateur avec l'opérateur de déréférencement. Voyez le fichier de code sur les map pour des exemples pratiques.
La bibliothèque algorithm (utilisable en ajoutant la ligne #include<algorithm> en haut de votre fichier) contient un ensemble de fonctions prédéfinies permettant notamment de manipuler des structures de données. On a ainsi accès à des fonctions de tri, d'inversion, de mélange aléatoire...
Les algorithmes de cette bibliothèque fonctionnent grâce aux itérateurs, ce qui permet de les rendre génériques pour toutes les structures de données.
Vous trouverez ici un fichier de code décrivant plusieurs fonctions de la bibliothèque algorithm. Notez toujours l'utilisation d'itérateurs afin de les appeler.
Vous avez maintenant à votre disposition de très nombreux outils efficaces et puissants pour travailler comme en Python, et même plus efficacement.
Comme dit précédemment, interrogez-vous toujours sur la pertinence d'utiliser une certaine structure de données : vector ou tableau statique ? Pointeur ou type de base ? Algorithme puissant ou simple boucle ? La réponse dépendra du problème et du type de solution que vous mettrez en place.