Éléments de syntaxe du C++

Cette page s'adresse aux étudiants de L3 de l'ISFA, promotion 2017-2018.
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.

Tous les cours avant le 29 mars.

Syntaxe de base jusqu'aux tableaux statiques.

Point culture

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.

Syntaxe de base

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

Importation de bibliothèques et chaines de caractères

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.

Lecture et écriture

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.

Boucles et structures de contrôle

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

Fonctions

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

Tableaux unidimensionnels

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.

Tableaux multidimensionnels

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.

Cours du 29 mars

Structures de données, pointeurs et allocation dynamique.

Le mot-clef struct

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).

Utilisation de la structure

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.

Adresse

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;

Pointeurs : principe

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.

Pointeurs : syntaxe

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.

Pointeurs : intérêt pour l'économie de mémoire

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

Taille des types en mémoire

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.

Allocation dynamique

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