Une visite de C++ : les basiques.

Chapitre 2

La première chose que nous faisons, c'est de tuer tous les avocats du langage. - Henry VI, Part II

2.1 Introduction

Le but de ce chapitre et des trois suivants est de vous donner une idée de ce qu'est le C++, sans entrer dans les détails. Ce chapitre présente informellement la notation du C++, le modèle de mémoire et de calcul du C++, et les mécanismes basiques pour organiser le code dans un programme. Ce sont les aménagements du langage supportant les styles les plus souvent vus en C et parfois appelés programmation procédurale. Le chapitre 3 continue avec la présentation des méchanismes d'abstraction du C++. Les chapitres 4 et 5 donnent des exemples des infrastructures de la bibliothèque standard.

Le prérequis est que vous ayez déjà programmé avant. Si tel n'est pas le cas, veuillez considérer la lecture d'un manuel tel que Programming: Principles and Practices Using C++ [Stroustrup, 2009] avant de continuer ici. Même si vous avez déjà programmé avant, le langage que vous avez utilisé ou les applications que vous avez écrites peuvent être très différentes du style du C++ présenté ici. Si vous trouvez cette "visite éclair" confuse, passez jusqu'à la présentation plus systématique commençant au Chapitre 6.

Cette visite du C++ nous préserve d'une présentation strictement verticale du langage et des infrastructures des bibliothèques en nous autorisant l'utilisation d'un jeu riche en infrastructures même dans les premiers chapitres. Par exemple, les boucles ne sont pas discutées en détail avant le Chapitre 10, mais elles seront utilisées dans des sens évidents bien avant cela. Pareillement, la description détaillée des classes, templates, utilisation du stockage libre et de la bibliothèque standard sont dispersées sur de nombreux chapitres, mais les types de la bibliothèque standard, tels que vector, string, complex, map, unique_ptr et ostream sont utilisés librement où requis pour augmenter l'efficience des exemples de code.

Comme analogie, pensez à une courte visite touristique d'une cité telle que Copenhague ou New York. En quelques heures, on vous donne un rapide coup d'oeil sur les attractions majeures, on vous raconte quelques histoires fondatrices, et habituellement on vous donne quelques suggestions à propos de ce qu'il y a à voir ensuite. Vous ne connaissez pas la ville après une telle visite. Vous ne comprenez pas tout ce que vous avez vu et entendu. Pour vraiment connaître une ville, vous devez vivre dedans, souvent pendant des années. Néanmoins, avec un peu de chance, vous aurez gagné un petit aperçu, quelques notions de ce qui rend la ville spéciale, et quelques idées sur ce qui peut vous intéresser. Après la visite, l'exploration réelle peut commencer.

Cette visite présente le C++ comme un tout intégré, plutôt qu'un gâteau à plusieurs couches. En conséquence, cela n'identifie pas les caractéristiques du langage comme présentées en C, parties de C++98, ou nouvelles en C++11. De telles informations historiques peuvent se trouver au §1.4 et Chapitre 44.

2.2 Les Bases

C++ est un langage compilé. Pour qu'un programme puisse tourner, son texte source doit être analysé par un compilateur produisant des fichiers objet, qui sont eux-même combinés par un linker fournissant un programme exécutable. Un programme C++ consiste typiquement en une multitude de fichiers de code source (habituellement simplement appelés fichiers source).

Un programme exécutable est créé pour une combinaison de matériel/système d'exploitation spécifique; ce n'est pas portable, disons, d'un Mac à un PC Windows. Quand on parle de la portabilité des programmes C++, on pense habituellement à la portabilité du code source; C'est-à-dire que le code source peut être compilé avec succès et exécuté sur une variété de systèmes.

Les composants de la bibliothèque standardsont du code C++ parfaitement ordinaire fourni par toutes les impémentations de C++. C'est-à-dire que la bibliothèque standard du C++ peut être implantée en C++ lui-même (et l'est avec de très mineures utilisations de code machine pour des choses telles que le changement de contexte de thread). Ceci implique que le C++ est suffisamment expressif et efficace pour les tâches de programmation système les plus exigeantes.

Le C++ est un langage à typage statique. Cela signifie que le type de chaque entité (i.e. objet, valeur, nom et expression) doit être connu du compilateur au moment où il l'utilise. Le type d'un objet détermine le jeu d'opérations qui lui est applicable.

2.2.1 Bonjour le monde !

Le programme C++ minimal est
int main() {} // le programme C++ minimal

Ceci définit une fonction appelée main, qui ne prend aucun argument et ne fait rien (§15.4).

Les accolades, { } expriment le regroupement en C++. Ici, elles indiquent le début et la fin du corps de la fonction. Le double slash, //, commence un commentaire qui s'étend jusqu'à la fin de la ligne. Un commentaire est pour un lecteur humain; le compilateur ignore les commentaires.

Tout programme C++ doit avoir exactement une fonction globale appelée main(). Le programme commence par exécuter cette fonction. La valeur int retournée par main(), s'il y en a, est la valeur de retour du programme au "système". Si aucune valeur n'est retournée, le système recevra une valeur indiquant une exécution réussie. Une valeur non nulle de main() indique un échec. Tous les systèmes d'exploitation et environnement d'exécution ne font pas usage de cette valeur de retour : les environnements basés sur Linux/Unix le font souvent mais les environnements basés sur Windows le font rarement.

Typiquement, un programme produit des sorties. Voici un programme qui écrit Bonjour le monde ! :


#include <iostream>

int main()
{
  std::cout << "Bonjour le monde !\n";
}

La ligne #include <iostream> demande au compilateur d'inclure les déclarations des infrastructures de flux d'E/S telles que définies dans iostream. Sans ces déclarations, l'expression
std::cout << "Bonjour le monde !\n"
n'aurait aucun sens. L'opérateur << (pousse vers) écrit son second argument dans son premier. Dans notre cas, la chaîne littérale "Bonjour le monde !\n" est écrite sur le flux de sortie standard std::cout. Une chaîne littérale est une séquence de caractères entourée de guillemets doubles. Dans une chaîne littérale, le caractère backslash \ suivi d'un autre caractère signifie un unique "caractère spécial". Dans notre cas, \n est le caractère de nouvelle ligne, pour que les caractères écrits soient Bonjour le monde ! suivis d'une nouvelle ligne.

Le std:: spécifie que le nom cout doit être trouvé dans l'espace de nom de la bibliothèque standard (§2.4.2, Chapitre 14). Je laisse généralement de côté le std:: quand je discute des fonctionnalités standards; §2.4.2 montre comment rendre visibles les noms d'un espace de noms sans qualification explicite.

Essentiellement, tout le code exécutable est placé dans des fonctions et est appelé directement ou indirectement depuis main(). Par exemple :
#include <iostream>
using namespace std; // rend les noms de std visibles sans std::(§2.4.2)

double carre(double x) // met un nombre à virgule flottante en double précision au carré
{
  return x*x;
}

void ecrit_carre(double x)
{
  cout << "le carré de " << x << " est " << carre(x) << "\n";
}

int main()
{
  ecrit_carre(1.234); // écrit : le carré de 1.234 est 1.52276
}

Un "type de retour" void signifie que la fonction ne retourne pas de valeur.

2.2.2 Types, Variables et Arithmétique

Chaque nom et chaque expression a un type qui détermine les opérations qui peuvent être appliquées dessus. Par exemple, la déclaration
int inch;
spécifie que inch est de type int; c'est-à-dire, inch est une variable entière.

Une déclaration est une assertion qui introduit un nom dans le programme. Cela spécifie un type pour l'entité nommée :

Le C++ offre une variété de types fondamentaux. Par exemple :
bool // Booléen, les valeurs possibles sont vrai ou faux
char // Caractères, par exemple 'a', 'z' et '9'
int // Entier, par exemple -213, 42 et 1066
double // Nombre à virgule flottante en double précision, par exemple 3.14 et 299793.0
Chaque type fondamental correspond directement à une infrastructure matérielle et a une taille fixe qui détermine l'intervalle des valeurs qui peuvent être stockées dedans : Une variable char est de la taille naturelle pour contenir un caractère sur une machine donnée (typiquement un octet), et les tailles des autres types sont mesurées en multiples de la taille d'un char. La taille d'un type dépend de l'implémentation (i.e. cela peut varier entre les machines) et peut être obtenue par l'opérateur sizeof; par exemple, sizeof(char) égal 1 et sizeof(int) est souvent 4.

Les opérateurs arithmétiques peuvent être utilisés dans des combinaisons de ces types :
x+y // plus
+x // plus unaire
x-y // moins
-x // moins unaire
x*y // multiplie
x/y // divise
x%y // reste (modulo) pour les entiers

Aussi peuvent l'être les opérateurs de comparaison :
x==y // egal
x!=y // non égal
x<y // plus petit
x>y // plus grand
x<=y // plus petit ou égal
x>y // plus grand ou égal

Dans les affectations et les opérations arithmétiques, le C++ effectue toutes les conversions sensées (§10.5.3) entre les types basiques pour qu'ils puissent être mélangés librement :
void une_fonction() // fonction qui ne retourne pas de valeur
{
  double d = 2.2; // initialise un nombre à virgule flottante
  int i = 7; // initialise un entier
  d = d+i; // affecte la somme à d
  i = d*i; // affecte le produit à i (tronque le double d*i vers un int)
}
Notez que = est l'opérateur d'affectation et que == teste l'égalité.

Le C++ offre une variété de notations pour exprimer l'initialisation, comme le = utilisé ci-dessus, et une forme universelle basée sur des listes d'initialisation délimitées par des accolades :
double d1 = 2.3; // initialise d1 avec 2.3
double d2 {2.3}; // initialise d2 avec 2.3

complex<double> z = 1; // un nombre complexe avec des scalaires en virgule flottante à précision double
complex<double> z2 {d1, d2};
complex<double> z3 = {1, 2}; // le = est optionnel avec {...}

vector<int> v {1,2,3,4,5,6}; // un vecteur de ints
La forme = est traditionnelle et remonte au C, mais dans le doute, utilisez la forme générale de liste {} (§6.3.5.2). Au moins, elle vous protège des conversions qui perdent de l'information (conversions rétrécissantes; §10.5) :
int i1 = 7.2; // i1 devient 7 (surprise ?)
int i2 {7.2}; // erreur : conversion de flottant vers entier
int i3 = {7.2}; // erreur : conversion de flottant vers entier (le = est redondant)
Une constante (§2.2.3) ne peut pas être laissée non initialisée et une variable ne doit être laissée non initialisée que dans des circonstances extrêmement rares. N'introduisez pas un nom tant que vous n'avez pas de valeur valable pour lui. Les types définis par l'utilisateur (tels que strnig, vector, Matrix, Motor_controller et Orc_warrior) peuvent être définis pour être implicitement initialisés (§3.2.1.1).

Quand vous définissez une variable, vous n'avez pas besoin de spécifier son type explicitement quand il peut être déduit de l'initialiseur :
  auto b = true; // un bool
  auto ch = 'x'; // un char
  auto i = 123; // un int
  auto d = 1.2; // un double
  auto z = sqrt(y); // z est du type de ce que sqrt(y) retourne
Avec auto, on utilise la syntaxe = parce qu'il n'y a pas de conversion de type qui puisse poser problème (§6.3.6.2).

On utilise auto quand on n'a pas de raison spécifique de mentionner le type explicitement. "raison spécifique" inclut :

En utilisant auto, on évite la redondance et l'écriture de longs noms de type. C'est spécialement important en programmation générique où le type exact d'un objet peut être difficile à savoir pour le programmeur et les noms de type peuvent être longs (§4.5.1).

En plus de l'arithmétique conventionnelle et des opérateurs logiques (§10.3), le C++ offre des opérations plus spécifiques pour modifier une variable :
x+=y // x = x+y
++x // incrément : x = x+1
x-=y // x = x-y
--x // décrément : x = x-1
x*=y // échelle : x = x*y
x/=y // échelle : x = x/y
x%=y // x = x%y
Ces opérateurs sont concis, pratiques et utilisés très fréquemment.

2.2.3 Constantes

Le C++ propose deux notions d'immuabilité (§7.5) :

Par exemple :
const int dmv = 17; // dmv est une constante nommée
int var = 17; // var n'est pas une constante
constexpr double max1 = 1.4*carre(dmv); // OK si carre(17) est une expression constante
constexpr double max2 = 1.4*carre(var); // erreur : var n'est pas une expression constante
const double max3 = 1.4*carre(var); // OK, peut être évalué à l'exécution
double somme(const vector&); // somme ne modifiera pas son argument (§2.2.5)
vector v {1.2, 3.4, 4.5}; // v n'est paas une constante
const double s1 = somme(v); // OK : évalué à l'exécution
constexpr double s2 = somme(v); // erreur : somme(v) n'est pas une expression constante
Pour qu'une fonction puisse être utilisable dans une expression constante, c'est-à-dire, dans une expression qui sera évaluée par le compilateur, elle doit être définie constexpr. Par exemple :
constexpr double carre(double x) { return x*x; }
Pour être constexpr, une fonction doit être plutôt simple : juste une assertion return calculant une valeur. Une fonction constexpr peut être utilisée pour des arguments non constants, mais quand cela est fait le résultat n'est pas une expression constante. On autorise une fonction constexpr à être appelée avec des arguments qui ne sont pas une expression constante dans les contextes qui ne requièrent pas des expressions constantes pour que nous n'ayons pas à définier la même fonction deux fois : une fois pour les expressions constantes et une fois pour les variables.

En quelques endroits, les expressions constantes sont requises par les règles du langage (i.e. les limites de tableaux (§2.2.5, §7.3), les étiquettes de case (§2.2.4, §9.4.2), certains arguments template (§25.2) et les constantes déclarées avec constexpr). Dans les autres cas, l'évaluation à la compilation est importante en termes de performance. Indépendamment des soucis de performance, la notion d'immuabilité (d'un objet avec un état non modifiable) est un problème de design important (§10.4).

2.2.4 Tests et Boucles

Le C++ fournit un jeu conventionnel d'assertions pour exprimer la sélection et les boucles. Par exemple, voici une simple fonction qui invite l'utilisateur à entrer un caractère et qui retourne un Booléen indiquant la réponse :
bool accepter()
{
  cout << "Voulez-vous continuer (o ou n) ?\n";  // écrit la question

  char reponse = 0;
  cin >> reponse;  // lit la réponse
  
  if (reponse == 'o') return true;
  return false;
}
Pour correspondre à l'opérateur de sortie << ("pousse vers"), l'opérateur >> ("obtenir depuis") est utilisé comme entrée; cin est le flux d'entrée standard. L'opérande à droite de >> est la cible de l'opération d'entrée et le type de cet opérande détermine ce que >> accepte. le caractère \n à la fin de la chaîne de sortie représente une nouvelle ligne (§2.2.1).

L'exemple pourrait être amélioré en prenant un n (pour "non") en considération :
bool accepter2()
{

  cout << "Voulez-vous continuer (o ou n) ?\n";  // écrit la question

  char reponse = 0;
  cin >> reponse;  // lit la réponse
  
  switch (reponse) {
  case 'o':
    return true;
  case 'n':
    return false;
  default:
    cout << "Je vais prendre cela comme un non.\n";
    return false;
  }
}

Une assertion switch teste une valeur avec un jeu de constantes. Ces constantes doivent être distinctes, et si la valeur testée ne correspond à aucune d'entre elles, le default est choisi. Si aucun default n'est fourni, aucune action n'est effectuée si la valeur ne correspond à aucune constante de choix.

Peu de programmes sont écrits sans boucle. Par exemple, on peut souhaiter donner à l'utilisateur quelques essais pour produire une entrée acceptable :
bool accept3()
{
  int essais = 1;
  while (essais<4) {
    cout << "Voulez-vous continuer (o ou n) ?\n"; // écrit la question
    char reponse = 0;
    cin >> reponse; // lit la réponse
    
    switch (reponse) {
    case 'o':
      return true;
    case 'n':
      return false;
    default:
      cout << "Désolé, je ne comprends pas cela.\n";
      ++essais; // incrémente
  }
  cout << "Je vais prendre cela pour un non.\n";
  return false;
}
L'assertion while s'exécute jusqu'à ce que sa condition devienne false.

2.2.5 Pointeurs, tableaux et Boucles

Un tableau d'éléments de type char peut être déclaré ainsi :
char v[6]; // tableau de 6 caractères

Similairement, un pointeur peut être déclaré ainsi :
char* p; // pointeur sur un caractère

Dans les déclarations, [ ] signifie "tableau de " et * signifie "pointeur vers". Tous les tableaux ont 0 pour borne inférieure, donc v a six éléments, de v[0] à v[5]. La taille d'un tableau doit être une expression constante (§2.2.3). Une variable pointeur peut contenir l'adresse d'un objet du type approprié :
char* p = &v[3]; // p pointe sur le quatrième élément de v
char x = *p; // *p est l'objet vers lequel p pointe
Dans une expression, le préfixe unaire * signifie "le contenu de" et le préfixe unaire & signifie "l'adresse de". On peut représenter le résultat de cette définition initialisée graphiquement :

Considérons la copie de dix éléments d'un tableau à un autre :
void copier_fct()
{
 &emspint v1[10] = {0,1,2,3,4,5,6,7,8,9};
  int v2[10]; // destiné à être la copie de v1

  for (auto i=0; i!=10; i++);emsp;// copie les éléments
    v2[i]=v1[i];
  // ...
}
Cette déclaration for peut être lue comme "mettre i à zéro; tant que i n'est pas 10, copier le ième élément et incrémenter i". Quand appliqué à une variable entière, l'opérateur d'incrémentation ++ ajoute simplement 1. Le C++ offre aussi une déclaration for plus simple, appelée une déclaration for à portée, pour les boucles qui traversent une séquence de la façon la plus simple :
void imprimer()
{
  int v[] = {0,1,2,3,4,5,6,7,8,9};

  for (auto x : v) // pour chaque x dans v
    cout << x << '\n';

  for (auto x : {10, 21, 32, 43, 54, 65})
    cout << x << '\n';
  // ...
}
La première déclaration à portée de for peut être lue comme "pour chaque élément de v, du premier au dernier, placer une copie dans x et l'imprimer". Notez que nous n'avons pas à spécifier une limite de tableau quand on l'initialise avec une liste. La déclaration à portée de for peut être utilisée pour n'importe quelle séquence d'élément (§3.4.1).

Si nous ne voulons pas copier les valeurs de v dans la variable x, mais plutôt avoir x qui se réfère à un élément, on peut écrire :
void incrementer()
{
  int v[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

  for (auto& x : v)
    ++x;
  // ...
}
Dans une déclaration, le suffixe unaire & signifie "référence à". Une référence est similaire à un pointeur, excepté que vous n'avez pas à utiliser un préfixe * pour accéder à la valeur référée par la référence. Aussi, une référence ne peut pas référencer un objet différent après son iitialisation. Quand ils sont utilisés dans des déclarations, les opérateurs (tels que &, * et [ ]) sont appelés opérateurs déclarants" :
T a[n];  // T[n]: tableau de n Ts (§7.3)
T* p;  // T*: pointeur vers T (§7.2)
T& r; // T&: référence vers T (§7.7)
T f(A); // T(A): fonction prenant un argument de type A retournant un résultat de type T (§2.2.1)
On essaie de s'assurer qu'un pointeur pointe toujours sur un objet, pour que le déréférencement de celui-ci soit valide. Quand on n'a pas d'objet à pointer ou si on a besoin de représenter la notion de "pas d'objet disponible", (i.e. pour la fin d'une liste), on donne au pointeur la valeur nullptr ("le pointeur null"). Il n'y a qu'un nullptr partagé par tous les types de pointeur :
double* pd = nullptr;
Lien<Enregistrement>* lst = nullptr; // pointeur sur un Lien vers un Enregistrement
int x = nullptr; // erreur : nullptr est un pointeur et non un entier
Il est souvent sage de vérifier qu'un argument pointeur qui est supposé pointer vers quelque chose, pointe véritablement sur quelque chose :