Mémo du langage C++

Mémo du langage C++
Crédit photo : Fotis Fotopoulos sur Unsplash

Sommaire

1. Types fondamentaux du C++

Avec taille minimum et taille exacte en octets.

1.1 Nombres en virgule flottante

  • float (4)
  • double (8)
  • long double (8)

1.2 Booléens

  • bool (1)

1.3Caractères

  • char (1/1)
  • wchar_t (1)
  • char8_t (C++20) (1)
  • char16_t (C++11) (2)
  • char32_t (C++11) (4)

1.4 Entiers

  • short (2)

  • int (2)

  • long (4)

  • long long (C++11) (8)

  • std::int8_t (C99)(1/1) et std::uint8_t (C99)(1/1) unsigned

  • std::int16_t (C99)(2/2) et std::uint16_t (C99)(2/2) unsigned

  • std::int32_t (C99)(4/4) et std::uint32_t (C99)(4/4) unsigned

  • std::int64_t (C99)(8/8) et std::uint64_t (C99)(8/8) unsigned

  • std::int_fast8_t (C99)(1) et std::uint_fast8_t (C99)(1) unsigned

  • std::int_fast16_t (C99)(2) et std::uint_fast16_t (C99)(2) unsigned

  • std::int_fast32_t (C99)(4) et std::uint_fast32_t (C99)(4) unsigned

  • std::int_fast64_t (C99)(8) et std::uint_fast64_t (C99)(8) unsigned

  • std::int_least8_t (C99)(1) et std::uint_least8_t (C99)(1) unsigned

  • std::int_least16_t (C99)(2) et std::uint_least16_t (C99)(2) unsigned

  • std::int_least32_t (C99)(4) et std::uint_least32_t (C99)(4) unsigned

  • std::int_least64_t (C99)(8) et std::uint_least64_t (C99)(8) unsigned

Note : les types std:: sont disponibles dans <cstdint>

1.5 Autres types

  • std::nullptr_t (C++11)
  • void
  • std::size_t (disponible dans <cstddef>)

1.6 Littéraux

Numériques : le type peut être précisé par suffixe :

  • u ou U : unsigned int
  • l ou L : long ou long double
  • ul, uL, Ul, UL, lu, lU, Lu, ou LU : unsigned long
  • ll ou LL : long long
  • ull, uLL, Ull, ULL, llu, llU, LLu, ou LLU : unsigned long long
  • f ou F : float

Préfixé par un 0, un littéral est interprété comme de l’octal.
Préfixé par un 0x, un littéral est interprété comme de l’hexadécimal.
Préfixé par un 0b, un littéral est interprété comme du binaire.

Chaînes : deux chaînes littérales sont automatiquement concaténées par le compilateur.

1.7 Types utilisateur

On peut définir ses propres types avec l’opérateur typedef ou avec using:

typedef double vitesse_t;
using vitesse_t = double;

Note 1 : par convention, on suffixe les noms des types avec _t.
Note 2 : la version avec using est plus lisible/compréhensible.

1.8 Typage automatique

Il est également possible de typer automatiquement une variable quand elle est définie avec le mot clé auto. Par exemple :

auto n = 5;
auto s{"une chaine"};
auto x{ new Complex(5.0,3.2) };

2. Entrées/sorties

Entête <iostream>

  • std::cin : entrée standard
  • std::cout : sortie standard
  • std::endl : retour à la ligne (équivalent à '\n')

Exemple d’utilisation

#include <iostream>
 
int main()
{
    int x{ 12 }; 
    std::cin >> x;
    std::cout << x << std::endl;
    return 0;
}

Pour spécifier la précision d’affichage d’un float, inclure <iomanip> et utiliser std::setprecision() :

std::cout << std::setprecision(16); // afficher avec une précision de 16 chiffres
std::cout << 3.33333333333333333333333333333333333333 <<'\n'; // affiche 3.333333333333333

Pour afficher un bool en lettre, utiliser std::boolalpha (dans <iostream>) :

std::cout << std::booltoalpha << true; // affiche true au lieu de 1

Pour afficher du binaire, utiliser le type std::bitset<n> fourni par <bitset>. Elle crée un champ de n bits affichable par std::cout :

std::cout << std::bitset<8> { 65 }; // affiche 01000001

3. Constantes

Une constante est définie par le mot clé const avant (ou après) sont type. Elle doit être initialisé lors de la déclaration (par affectation ou par copie) :

const int n{5};
int const n = 5;  // équivalent

Une constante n’a pas besoin de recevoir une valeur fixe à la compilation. Elle peut être déclarée et affectée par copie d’une autre variable. On parle alors de runtime constant. Si la valeur est déterminable à la compilation, il est préférable d’utiliser le mot-clé constexpr.


4. Opérateurs et précédence

Le niveau de précédence (de 1 à 17) indique dans quel ordre (croissant) sont évalués les opérateurs.
L’associativité (L>R ou L<R) indique si les opérateurs successifs sont associés de gauche à droite ou de droite à gauche (respectivement).

4.1 Opérateur de niveau 1

:: 	   Espace global (unaire)           ::nom
:: 	   Espace de nommage (binaire) 	    nom_de_classe::nom_de_membre

4.2 Opérateur de niveau 2 (L>R)

()                      Parentheses                              (exp)
()                      Appel de fonction                        nom_de_fonction(paramètres)
()                      Initialisation                           type nom(exp)
{}                      Initialisation uniforme (C++11)          type nom{exp}
type()                  Cast fonctionnel                         type(exp)
type{}                  Cast fonctionnel (C++11)                 type{exp}
[]                      Accès à un tableau                       pointeur[exp]
.                       Accès à un membre d'objet                objet.membre
->                      Accès à un membre de pointeur d'objet    pointeur_objet->membre
++                      Post-incrementation                      lvalue++
––                      Post-decrementation                      lvalue––
typeid                  Run-time type information                typeid(type) or typeid(exp)
const_cast              Cast away const                          const_cast<type>(exp)
dynamic_cast            Run-time type-checked cast               dynamic_cast<type>(exp)
reinterpret_cast        Cast inter type                          reinterpret_cast<type>(exp)
static_cast             Cast inter type à la compilation         static_cast<type>(exp) 

4.3 Opérateur de niveau 3 (L<R)

+                       Plus uniaire                             +exp
-                       Moins unaire                             -exp
++                      Pre-increment                            ++lvalue
––                      Pre-decrement                            ––lvalue
!                       NON logique                              !exp
~                       NON binaire                              ~exp
(type)                  Cast "façon C"                           (new_type)exp
sizeof                  Taille en oct.                           sizeof(type) or sizeof(exp)
&                       Adresse de                               &lvalue
*                       Déréferencement                          *exp
new                     Alloc. de mémoire dyn.                   new type
new[]                   Alloc. de tableau dyn.                   new type[exp]
delete                  Libération de memoire                    delete pointer
delete[]                Libération de tableau                    delete[] pointer 

4.4 Opérateur de niveau 4 (L>R)

->*                     Selecteur de membre de pointeur          object_pointer->*pointer_to_member
.*                      Selecteur de membre d'objet              object.*pointer_to_member 

4.5 Opérateur de niveau 5 (L>R)

*                       Multiplication                           exp * exp
/                       Division                                 exp / exp
%                       Modulo                                   exp % exp  

4.6 Opérateur de niveau 6 (L>R)

+                       Addition                                 exp + exp
-                       Soustraction                             exp - exp  

4.7 Opérateur de niveau 7 (L>R)

<<                      Décalage de bits à gauche                exp << exp
>>                      Décalage de bits à droite                exp >> exp  

4.8 Opérateur de niveau 8 (L>R)

<                       Inférieur                                exp < exp
<=                      Inférieur ou égal                        exp <= exp
>                       Supérieur                                exp > exp
>=                      Supérieur ou égal                        exp >= exp 

4.9 Opérateur de niveau 9 (L>R)

==                      Egalité                                  exp == exp
!=                      Inégalité                                exp != exp 

4.10 Opérateur de niveau 10 (L>R)

& 	                    ET binaire 	                             exp & exp 

4.11 Opérateur de niveau 11 (L>R)

^ 	                    OU exclusif binaire 	                 exp ^ exp 

4.12 Opérateur de niveau 12 (L>R)

| 	                    OU binaire 	                             exp | exp 

4.13 Opérateur de niveau 13 (L>R)

&& 	                    ET logique 	                             exp && exp 

4.14 Opérateur de niveau 14 (L>R)

|| 	                    OU logique 	                             exp || exp 

4.15 Opérateur de niveau 15 (L<R)

?:                      Ternaire                                 exp ? exp : exp
=                       Assignement                              lvalue = exp
*=                      Multiplication et assignement            lvalue *= exp
/=                      Division et assignement                  lvalue /= exp
%=                      Modulo et assignement                    lvalue %= exp
+=                      Addition et assignement                  lvalue += exp
-=                      Soustraction et assignement              lvalue -= exp
<<=                     Décalage gauche et assignement           lvalue <<= exp
>>=                     Décalage droite et assignement           lvalue >>= exp
&=                      ET binaire et assignement                lvalue &= exp
|=                      OU binaire et assignement                lvalue |= exp
^=                      OU exclusif binaire et assignement       lvalue ^= exp 

4.16 Opérateur de niveau 16 (L<R)

throw 	                Lancé d'expression 	                     throw exp 

4.17 Opérateur de niveau 17 (L>R)

, 	                    Opérateur virgule 	                     exp, exp 

5. Champs de bits

Il faut inclure <bitset> pour déclarer des variables de type std::bitset<N> où N représente le nombre de bits (minimum).

Ensuite, on les manipule avec test(), set(), flip() et reset() de la façon suivante :

Les bits sont numérotés de droite à gauche en commençant à 0.

std::bitset<8> bits{ 0b0000'0101 }; // déclare un champ de 8 bits initialisés avec les valeurs 0000 0101
bits.set(3); // mets le troisième bits à 1
bits.flip(5); // inverse le 5ème bit
bits.reset(4); // met le 4eme bit à 0
std::cout << bits.test(2); // retourne true si le 2ème bit est à 1

6. Espaces de nommage

6.1 Déclaration et définition

Les espaces de nommage permettent de séparer plusieurs variables/fonctions/classes… de même nom.

Définition d’un espace de nommage :

namespace nom_de_l_espace {

// tout ce qui est défini ici est dans l'espace de nommage

}

Plusieurs espaces de nommage peuvent être déclarés dans le même fichier.
On peut imbriquer les espaces de nommage.
Un espace de nommage peut être déclaré dans plusieurs fichiers, les contenus seront fusionnés.
Il faut déclarer l’espace de nommage dans le fichier d’entête et dans le fichier de source.

On accède à un élément qui est dans un espace de nommage avec l’opérateur :: de la façon suivante : nom_de_l_espace::element.
L’opérateur :: sans préciser le nom d’un espace de nommage avant permet de spécifier explicitement l’espace global.
On peut déclarer une variable de type namespace pour créer un alias d’un espace de nommage (pour simplifier son nom par exemple) comme ceci :

namespace cx = math::artihmetic::complex_numbers;

// cx::add est maintenant équivalent à math::artihmetic::complex_numbers::add

6.2 Directives using

Le mot clé using permet d’indiquer dans un bloc qu’on va se référer à une fonction spécifique d’un espace de nommage. Par exemple :

using std::cout;
cout << "un texte" << std:endl;  // On n'a plus besoin de préfixer cout mais c'est toujours nécessaire pour endl

On peut aussi utiliser using namespace sur un espace de nommage complet :

using namespace std;
cout << "un texte" << endl; // On a "tiré" tout l'espace de nommage std dans le bloc en cours

6.3 Espace de nommage anonyme

Si l’espace de nommage est déclaré sans nom, ses éléments sont automatiquement accessibles dans l’espace de nommage parent sans prefixe :

#include <iostream>

namespace // espace de nommage anonyme
{
    void afficheDeux() // cette fonction ne peut être appelée que dans ce fichier
    {
        std::cout << 2;
    }
}

int main()
{
    afficheDeux(); // il n'y a pas besoin de préfixe d'espace de nommage
    return 0;
}

6.4 Espace de nommage inline

Quand un espace de nommage est déclaré inline, ses fonctions sont utilisées quand le nom de l’espace de nommage n’est pas précisé :

#include <iostream>

inline namespace espaceChiffre
{
    void afficheDeux() // can only be accessed in this file
    {
        std::cout << 2;
    }
}

namespace espaceLettre
{
    void afficheDeux() // can only be accessed in this file
    {
        std::cout << "Deux";
    }
}

int main()
{
    espaceChiffre::afficheDeux();
    espaceLettre::afficheDeux();
    afficheDeux();

    return 0;
}

7. Portée, durée et liaison des variables

7.1 Portée

La portée d’une variable désigne les endroits où elle peut être accédée.

Les variables peuvent être :

  • locales : elles ne sont accessibles que dans le bloc où elles ont été déclarées (variables locales, paramètres de fonctions, types définis par l’utilisateur comme des énumérations ou des classes s’ils ont été définis dans un bloc).
  • globales : elles sont accessibles n’importe où dans le fichier (variables globales, fonctions, types définis par l’utilisateur s’ils n’ont pas été définis dans un bloc.

7.2 Durée

La durée d’une variable détermine quand elle est créée et détruite. Il y a trois durées possibles :

  • automatique : les variables sont créées là où elles sont définies et détruites à la fin du bloc . Ce sont les variables locales et les paramètres.
  • statiques : elles sont créées et détruites en même temps que le programme. Ce sont les variables globales et les variables déclarées avec le mot clé static.
  • dynamiques : elles sont crées et détruites à la demande par les opérateurs d’allocation/libération de mémoire.

7.3 Liaison

La liaison détermine si plusieurs déclaration de la même variable référencent ou non le même emplacement mémoire. Un identificateur peut être :

  • non lié : sa définition est indépendante de toute autre. C’est le cas des variables locales et des types définis par l’utilisateur comme des énumérations ou des classes s’ils ont été définis dans un bloc.
  • à liaison interne : l’emplacement mémoire accédé est le même partout dans le fichier où l’identificateur est déclaré. C’est le cas des variables globales statiques, des fonctions statiques, des variables globales constantes et des fonctions(ou des types utilisateur) déclarés dans un espace de nommage anonyme.
  • à liaison externe : l’emplacement mémoire peut être accédé partout dans le fichier ou dans un autre fichier (avec une déclaration forward).

Les fonctions sont par défaut à liaison externe (sauf si elles sont définies avec le mot clé static).

7.4 Spécificateurs de classes de stockage

Ils définissent la durée et la liaison d’une variable. C++ supporte les spécificateurs suivants :

  • extern
  • static
  • thread_local (C++11)
  • mutable
  • auto (déprécié en C++11)
  • register (déprécié en C++17)

8. Conversions de types

8.1 Conversions implicites (coercion)

  • promotion numérique : c’est la conversion automatique d’un char en int ou d’un float en double par exemple.
  • conversion numérique : c’est la transformation d’un float en int (ou inversement)

Exemples :

int n{2}; // char -> int (promotion)
double d{3}; // int -> double (conversion)

8.2 Conversion explicites (casts)

Il y a 5 types de conversions explicites en C++ :

  • static_cast : essaie de transformer un type en un autre
  • const_cast : retire l’attribut const (à utiliser avec précaution)
  • dynamic_cast : transtypage sûr entre des classes (requière RTTI)
  • reinterpret_cast : convertit n’importe quoi en n’importe quoi (ce n’est pas vraiment un cast, cet opérateur indique juste au compilateur d’interpréter différement le contenu en mémoire).
  • et enfin le cast “dans le style du C” qui se comporte de la façon suivante :
    • const_cast si possible
    • sinon static_cast
    • sinon static_cast suivi de const_cast
    • sinon reinterpret_cast
    • sinon reinterpret_cast suivi de const_cast

De façon générale, static_cast et dynamic_cast sont d’un usage sûr. const_cast est à employer avec beaucoup de précautions. reinterpret_cast encore plus et le cast “dans le style du C” devrait être évité autant que possible.

Les casts s’emploient de la façon suivante :

double d{2.6};
int n = static_cast<int>(d);  // conversion du double en int

9. Enumérations

9.1 Enumérations

Définition par le mot clé enum.
Les éléments de l’énumération sont dans le même espace de nommage que l’enum elle-même, on ne peut donc pas avoir deux éléments de même nom dans deux énums différentes du même espace de nommage.
Si on n’affecte pas explicitement une valeur aux éléments, le premier vaut automatiquement 0. Les suivants sont des entiers incrémentés.

Exemple :

enum Couleur   // Définition d'une enum
{
CLR_NOIR = 1,
CLR_ROUGE,  // vaut 2
CLR_VERT,  // vaut 3, etc.
CLR_BLEU,
CLR_BLANC
};

// Définition d'une variable
Couleur peinture {CLR_BLANC};

// Conversion en int
int n = CLR_NOIR;
Couleur c = 2; // interdit : provoque une erreur
Couleur cc = static_cast<Couleur>(2);  // OK mais non recommandé

9.2 Classes d’énumérations

Définies par le mot clé enum class.
Fonctionne de la même manière à une différence près : l’énumération crée un espace de nommage qui contient ses éléments.
De cette façon, on ne peut pas accidentellement comparer des éléments de deux énum différentes.

Exemple :

enum class Etat
{
OUVERT,
FERME,
INCONNU
};

Etat e{Etat::FERME};  // Utilise le nom complet

10. Structures

Les structures définissent des regroupement de variables. Elles se déclarent avec le mot clé struct.
On peut les initialiser avec l’opérateur { } en séparant les valeurs d’init par une virgule.
On peut mettre une valeur par défaut dans la définition.

Exemple :

struct Identite
{
   id id { 0 };  // valeur par défaut
   int age;
};

Identite toto { 5, 12 };

11. Contrôle de flux

11.1 Sortie rapide

#include <cstdlib>

std::exit(int codesortie);

11.2 Branchements conditionnels

11.2.1 if

if (condition) 
{
    // code exécuté si la condition est vraie
}
else
{
    // code exécuté si la condition est fausse
}

11.2.2 switch

switch (expression)
{
    case valeur1:
        // code exécuté si expression == valeur1
   case valeur2:
        // code exécuté si expression == valeur1 ou valeur2
        break;  // interrompt le switch
    case valeur 3:
        // code exécuté si expression == valeur3 (seulement)
       break;
    default:
       // code exécuté par défaut        
}

11.3 Boucles

11.3.1 while / do .. while

while (condition)
{
    // code exécuté tant que la condition est vraie
}

do
{
    // code exécuté tant que la condition est vraie
}
while (condition)

11.3.2 for

for ( init ; condition ; fin )
{
}

Init est exécuté en premier puis tant que la condition est vérifiée, le code dans le bloc est exécuté suivi de fin.

11.3.3 foreach (pour itérer les tableaux)

for ( declaration d'élément : tableau )
{
}

int tab[] { 0, 2, 3, 4 };
for (int n : tab)
{
   std::cout << n;
}

11.4 Sauts

11.4.1 goto

label:      // défintion d'un label


goto label;    // continue l'exécution en allant à label

Note : on ne peut pas faire de goto en avant en sautant par dessus une déclaration de variable.

11.4.2 break

L’instruction break permet de sortir du bloc en cours (switch ou boucle).

11.4.3 return

L’instruction return sort de la fonction en cours.

11.4.4 continue

L’instruction continue passe immédiatement à l’itération suivante d’une boucle.


12. Tableaux

12.1 Tableaux statiques

  • éléments de n’importe quel type
  • indices de types entier (tous y compris bool)
  • taille fixée à la compilation
  • les tableaux sont passés par adresse aux fonctions
  • sizeof d’un tableau retourne le nombre d’éléments multiplié par la taille d’un élément (l’occupation mémoire totale)
  • nombre d’éléments d’un tableau :
    • avant C++17 : sizeof(array) / sizeof(element)
    • en C++17 : std:size(array) en ayant inclus <iterator> (retourne un unsigned int)
    • en C++20 : std:ssize(array) en ayant inclus <iterator> (retourne un signed int)

Exemple :

double mesuresTemperature[31];  // tableau de 31 doubles indicés de 0 à 30

mesuresTemperature[0] = 12.5;

int premiers[5]  { 2, 3, 5, 7  };  // initialisation, si la liste est plus courte que le tableau, les éléments restants sont initialisés à 0

int nombres[] { 0, 1, 2, 3 }; // avec une liste d'init, il n'est pas nécessaire de préciser la taille du tableau

int entiers[3][5]   // tableau multidimensions
{
  { 1, 2, 3, 4, 5 }, // ligne 0
  { 6, 7, 8, 9, 10 }, // ligne 1
  { 11, 12, 13, 14, 15 } // ligne 2
};

12.2 Tableaux dynamiques

  • alloués avec new [taille]
  • libérés avec delete []

Exemple :

int *array{ new int[5]{ 9, 7, 5, 3, 1 } };

delete [] array;

13. Pointeurs et références

13.1 Pointeurs

  • Obtenir l’adresse d’une variable : &
  • Déréférencer un pointeur (accéder à l’élément situé à l’adresse pointée) : *
  • Pointeur nul :
    • #include <cstddef> pour accéder à la directive NULL du préprocesseur (déconseillé)
    • utiliser le mot clé nullptr (C++11)
  • Allocation de mémoire avec l’opérateur new
  • Libération de mémoire allouée dynamiquement avec delete

Exemple :

int *ptr { new int };

delete ptr;
  • Un pointeur peut être déclaré constant, c’est à dire que sa valeur ne peut pas changer après initialisation. Attention de ne pas confondre avec un pointeur sur une valeur constante (c’est à dire qu’on ne peut pas changer la valeur après déréférencement).

Exemple :

int n {10};
const int * p { 5 };  // la valeur pointée est constante
*p = 6; // illégal
p = &n; // légal

int * const p2 { &n };  // le pointeur est constant
*p2 = 12; // légal
p2 = p; // illégal

const int * const p3 { &n }; // le pointeur et la valeur pointée sont constants, rien n'est modifiable
  • Pour accéder à un membre d’une structure via un pointeur sur cette structure, il existe deux solutions :
(*pstructure).membre
pstructure->membre

13.2 Références

  • Une référence est une variable qui adresse la même zone mémoire qu’une autre. C’est une sorte d’alias.
  • Elle se déclare avec le symbole &
  • Une référence doit obligatoirement être initialisée
  • Une référence ne peut pas être modifiée (c’est l’élément référencé qui est modifié)
  • Les références peuvent servir à passer des variables par adresses à des fonctions (si en plus elles sont const elles évitent des copies inutiles pourles types non fondamentaux)

Exemple :

int n {5};
int &r { n }; // référence sur n

r = 6;
std::cout << n; // affiche 6

14. Fonctions

14.1 Passage de paramètres

  • Par valeur : void maFonc( int n ); : maFonc reçoit une copie de n et ne peut pas la modifier (sauf localement)
  • Par référence : void maFonc( int& n ); : maFonc reçoit une référence sur n et peut donc le modifier
  • Par adresse : void maFonc( int* p ); : maFonc reçoit un pointeur et peut donc modifier l’élément pointé

14.2 Retour de fonction

Ils peuvent aussi se faire par valeur, adresse ou référence.

ATTENTION : il ne faut pas retourner par référence une variable locale à la fonction sinon elle sera détruite à la fin de la fonction et l’appelant récupérera une référence invalide.

Pour retourner plusieurs éléments, on peut utiliser une structure ou un std::tuple.

14.3 Fonctions inline

Quand une fonction est déclarée inline, chaque appel est remplacé par le corps complet de la fonction par le compilateur. Note : les compilateurs modernes détectent très bien quand il est bénéfique ou non d’inliner une fonction. Ce mot clé est généralement inutile.

14.4 Valeur d’argument par défaut

Une valeur par défaut peut être spécifiée avec un = : void maFonc(int n = 5);

Quand un paramètre a une valeur par défaut, tous les paramètres qui le suivent doivent aussi avoir une valeur par défaut. Si la valeur par défaut est déclarée dans une déclaration en avance (dans un .h par exemple), il ne faut pas la répéter dans la définition de la fonction.

14.5 Surcharge de fonction

On peut déclarer plusieurs fois une fonction avec le même nom tant que les paramètres sont différents.

Note : la valeur de retour peut également être différente mais ce n’est pas un critère d’unicité. Il en est de même pour les paramètres qui ont des valeurs par défaut.

14.6 Pointeur de fonction

Syntaxe générale : type_retour (* nom_pointeur)(types des paramètres).

Exemple :

int MaFonc(int n);

// Syntaxe 1
int (* maFoncPtr)(int);
maFoncPtr = MaFonc;   // maFoncPtr est un pointeur sur MaFonc

// Syntaxe 2
int (* maFoncPtr)(int) {MaFonc};

// Syntaxe 3 (C++11)
std::function<int(int)> maFoncPtr {MaFonc};

// Appels
(*maFoncPtr)(5);
maFoncPtr(5);

Un pointeur de fonction peut être rendu plus lisible grâce à un typedef : typedef int(* maFoncPtrType)(int);

Et (C++11) encore plus lisible : using maFoncPtrType = int(*)(int);
ou using maFoncPtrType = std::function<int(int)>;

14.7 Inférence de type

Le mot clé auto et l’inférence de type sont utilisables sur les pointeurs de fonctions.


15. Assertions

15.1 Vérifications à l’exécution

Les assertions sont des directives du préprocesseur destinées à vérifier des conditions à l’exécution.
Pour utiliser la macro assert, il faut inclure <cassert>.
Les directives assert terminent immédiatement le programme.
Elles sont désactivées si le symbole NDEBUG est défini.

15.2 Vérification à la compilation

Pour faire des assertions lors de la compilation, il faut utiliser static_assert.
Exemple : static_assert(sizeof(int)==4,"Les int doivent faire 4 octets.").
Note : le commentaire est apparu en C++11 et est devenu optionnel en C++17.


16. Fonctions lambdas et closures

16.1 Définition

Une lambda (ou closure) est une fonction anonyme définie à l’intérieur d’une autre fonction. La syntaxe générale de définition d’une lambda est :

[ capture ] ( paramètres ) -> type_de_retour
{
    corps de la lambda
}

La clause de capture et les paramètres peuvent être vides s’ils ne sont pas nécessaires (sans toutefois omettre les crochets ou les parenthèses). Le type de retour peut ne pas être spécifié (on ne met pas la flèche non plus alors), il sera automatiquement déduit.

16.2 Variables lambdas

Les lambda peuvent être stockées dans des variables std::function et leur type peut être inférés si la variable qui les contient est immédiatement initialisée. Elles peuvent aussi être conservées dans des pointeurs de fonction si leur clause de capture est vide. Exemple :

// Pointeur de fonction
double (*addition)(double, double){
  [](double a, double b) {
    return (a + b);
    }
};
 
// Avec std::function
std::function addition{  // note: avant C++17, il faut mettre  std::function<double(double, double)>
  [](double a, double b) {
    return (a + b);
  }
};
 
// Inférence du type
auto addition{
  [](double a, double b) {
    return (a + b);
  }
};

16.3 Capture

La clause de capture permet de rendre accessible à la lambda des variables définies dans le code englobant. Par défaut les variables capturées sont des copies constantes de la variable réelle. Il est possible de retirer le qualificateur const en déclarant la lambda mutable (mais la capture reste faite par copie donc la variable extérieure n’est toujours pas modifiable par la lamda.
Si la variable capturée est précédée de &, la capture se fait par référence (c’est donc directement la variable extérieure qui est accédée et elle est par défaut modifiable par la lambda).

On peut capturer plusieurs variables en les séparant par des virgules.

On peut capturer toutes les variables qui sont mentionnées dans la lambda automatiquement, par valeur avec l’opérateur = ou par référence avec l’opérateur & dans les crochets de capture. Exemple : [=](){ return l*w; } va capturer automatiquement par valeur les variables l et w du contexte appelant.

Attention lors d’une capture par référence à ce que la variable référencée ne meurt pas avant que la lambda soit utilisée.

17 Classes

17.1 Déclaration d’une classe

Une classe est une structure de données regroupant à la fois des données (comme une struct), des fonctions (les méthodes) et des types locaux. Les types locaux sont des alias d’autres types, déclarés avec le mot-clé using.

Elle se déclare avec le mot-clé class.

Exemple :

class Calculatrice // déclaration d'une classe
{
public:
  using nombre_t = int; // type local à la classe
 
  std::vector<nombre_t> m_historiqueResultat{}; // membre de la classe (souvent préfixé par m_)
 
  nombre_t addition(nombre_t a, nombre_t b)  // méthode de la classe
  {
    nombre_t resultat{ a + b };
 
    m_historiqueResultat.push_back(resultat);
 
    return resultat;
  }
};  // Note : ne pas oublier le ; à la fin de la déclaration, comme pour une struct

Les classes peuvent être déclarées dans les fichiers d’entête (.h) et leurs méthodes définies dans les fichiers sources (.cpp).

17.2 Spécification d’accès

La déclaration d’une classe peut contenir des spécificateurs d’accès. Ces mots clés déterminent d’où il est possible d’accéder au membres (et aux méthodes) de la classe :

  • public : l’élément est accessible à l’intérieur et à l’extérieur de la classe
  • private : l’élément n’est accessible qu’à l’intérieur de la classe
  • protected : l’élément est accessible à l’intérieur de la classe et des classes qui en dérivent

Par défaut, les membres d’une classe sont privés.

17.3 Constructeurs

Les constructeurs sont un genre particulier de méthode. Ils servent à initialiser (= construire) les instances de la classe. Remarque : un constructeur ne crée pas l’objet (au sens où il n’alloue pas la mémoire contenant les membres), il se contente de les initialiser (ou pas).

Un constructeur se déclare comme une méthode qui a le même nom que la classe et qui n’a pas de type de retour (même pas void).

Un constructeur qui n’a aucun paramètre ou dont tous les paramètres ont des valeurs par défaut est appelé le constructeur par défaut.

Si une classe n’a aucun constructeur déclaré, le compilateur génère un constructeur implicite (qui ne fait aucune initialisation).

Quand un membre d’une classe est lui-même une classe, sont constructeur sera appelée avant celui de la classe qui le contient.

Un constructeur peut comporter une liste d’initialisation des membres de la classe. Cela permet d’éviter d’appeler le constructeur par défaut puis de copier un membre de type classe.

Un membre peut avoir une valeur d’initialisation par défaut déclaré. Ainsi, il n’est pas nécessaire de l’initialiser dans chaque constructeur.

Exemple :

class ClasseA
{
    int m_valeur{ 0 };  // Valeur par défaut si le membre n'est pas initialisé par le constructeur

    // Constructeur par dafaut
    ClasseA() { m_valeur = 0; }

    // Constructeur avec une affectation
    ClasseA( double dValeurInitiale ) 
    { 
        std::cout << m_nValeur;  // l'affichage n'est pas défini
        m_nValeur = static_cast<int>(nValeurInitiale); 
    }

    // Constructeur avec liste d'initialisation
    // L'initialisation respecte les mêmes règles que l'initialisation de variables
    ClasseA( int nValeurInitiale ) : m_nValeur{nValeurInitiale} 
    {
        std::cout << m_nValeur;  // l'affectation a lieu avant l'affichage
    }
};

Attention : quand un constructeur utilise une liste d’initialisation, les initialisations ne sont pas faite dans l’ordre de la liste mais dans l’ordre de déclaration des membres dans la classe !

Deux constructeurs peuvent être chaînés en plaçant le premier dans la liste d’initialisation du second.

Exemple :

class ClasseA
{
    int m_nValeur;

    // Le constructeur par défaut est chaîné après le constructeur prenant un int en paramètre
    ClasseA() : ClasseA(0) {}  

    ClasseA( int nValeurInitiale ) : m_nValeur{nValeurInitiale} {}

};

17.4 Destructeurs

Les destructeurs sont un autre genre particulier de méthodes. Ils sont appelés quand l’objet est détruit (= il sort du scope ou la mémoire allouée est libérée).

Les destructeurs servent généralement à libérer les ressources non automatiques possédées par la classe (mémoire allouée, ressources système…).

Un destructeur se déclare comme une méthode portant le nom de la classe précédé du symbole ~ (tilde).

Un destructeur ne prend pas de paramètre (il n’y a donc qu’un seul destructeur par classe).

Attention : l’utilisation de la fonction exit() termine le programme sans exécuter aucun destructeur.

17.5 Le pointeur spécial this

this est un pointeur spécial (et un mot clé du langage) qui référence l’objet en cours pendant l’exécution d’une méthode.

La construction return *this permet à une méthode de retourner une référence sur l’objet en cours et ainsi de faire des méthodes chaînables (ex : la surcharge de l’opérateur << de cout).

17.6 Classes et méthodes constantes

Une instance de classe peut être déclarée constante (avec le mot clé const). Dans ce cas, elle doit être initialisée par son constructeur. Toute affectation d’un membre par la suite provoquera une erreur de compilation.

De plus, les méthodes d’une classe peuvent être déclarées elles aussi constantes. On ne peut appeler que les méthodes constantes sur une instance constante.

Remarque : lorsqu’on passe en paramètre une instance de classe à une fonction, il est plus efficace de passer une référence constante qu’une copie.

Enfin, il est possible de surcharger une méthode pour en avoir une version constante et une version non constante. (Un exemple d’utilisation est de permettre de renvoyer des valeurs constantes ou non dans les deux varianates).

17.7 Membres et méthodes statiques

Le mot clé static appliqué à un membre rend ce membre commun à toutes les instances de la classe. Un membre statique peut être initialisé à sa déclaration.

Le mot clé static appliqué à une méthode rend cette méthode indépendante des instances de la classe. Elle ne possède donc pas de pointeur this et ne peut manipuler que des membres statiques.

17.8 Fonctions, classes et méthodes amies

Le mot clé friend permet de rendre amis une classe et une fonction, une autre classe ou une méthode d’une autre classe. Un élément ami est autorisé à accéder aux membres privés de la classe comme s’il en faisait lui-même partie.

Attentiion : c’est bien la classe qui donne accés à ses membres en déclarant son amitié.

Plusieurs classes peuvent déclarer leur amitié à la même fonction.

Dans le cas d’une fonction ou d’une méthode, il faut bien sûr qu’elles acceptent dans leurs paramètres une instance (ou une référence ou un pointeur) de la classe qui déclare son amitié.