Types

  • Your native language: with Chrome, right-click on the page and select « Translate into … »

  • English version:

En C++, pour créer une variable, il faut lui associer un type. Il ne faut pas confondre la notion de type et de variable :

  • Les variables ont une existence en mémoire

  • Les types sont des descriptions indiquant comment l’information est représentée dans la mémoire

Ainsi, il existe un seul type int mais il peut exister aucune, une ou plusieurs variables de type int.

Les types du C++ se divisent en deux catégories :

  • Les types fondamentaux

  • Les types composés (compound type) définis à partir des type fondamentaux

Les types fondamentaux

Présentation

Les types fondamentaux (fundamental type) sont spécifiés par la norme du langage C++ et sont intégrés au compilateur. Ils sont ainsi utilisables dans le code à tout moment sans avoir à inclure de fichier d’en-tête. Les types fondamentaux sont divisés en catégories :

  • Le type void : il sert par exemple à préciser qu’une fonction ne retourne pas de valeur.

  • Le type booléen : bool pouvant prendre que deux valeurs : true ou false.

  • Les types caractères : hors programme.

  • Les types numériques :
    • Les types entiers signés : short, int, long

    • Les types flottant : float, double…

Implémentation des types

A titre anecdotique, le standard du C++ ne fixe pas la taille exacte des types fondamentaux (int, long, short). Par exemple, le compilateur doit implémenter le type int comme un nombre entier signé dont la précision est au moins celle d’un short et au plus celle d’un long. En pratique, sur vos machines, un int sera implémenté comme un entier 32 bits, probablement 16 bits sur un microcontrôleur de machine à café et exceptionnellement 64 bits sur des plateformes exotiques.

L’implémentation variable du type int peut passer pour un détail, mais elle a une conséquence bien visible pour vous. En effet, les OS étant maintenant en 64 bits, si vous voulez parcourir un tableau de plus de 2 milliard d’éléments, il ne faudra pas choisir le type int comme index. Vous conclurez qu’il suffit alors d’utiliser un entier 64 bits, mais sur un système 32 bits, cela va doubler la taille de vos variables inutilement. Pour ce besoin précis, le langage C++ a mis en place un type spécifique nommé size_t. Son implémentation est garantie pour pouvoir adresser la totalité de l’espace mémoire de la machine cible. Ainsi, pas besoin de se poser de question sur l’architecture cible, le type size_t s’adapte automatiquement. Vous le croiserez frequemment dans les index de boucle :

for (size_t i = 0; i < array.size(); ++i)

Conversion des types numériques

Le transtypage ou la conversion ou le cast désigne l’action de transformer une valeur d’un type donné vers un autre type.

REGLE 1 : la conversion d’un nombre vers un autre type est valide et implicite en C++.

Le caractère valide signifie qu’aucun message d’erreur et qu’aucun message d’alerte ne se produit. Le caractère implicite signifie que le compilateur applique automatiquement une conversion sans que vous ayez à écrire du code (documentation sur les conversions implicites).

int x = 3.5;          // implicit conversion from double to int
cout << x;            // ===> 3

double d = 3;         // implicit conversion from int to double
cout << d;            // ===> 3.0000

La conversion implicite semble être une facilité à première vue, mais c’est aussi une source de problèmes. Dans le code ci-dessous, nous vous montrons un exemple d’une conversion implicite qui ne tourne pas forcément à notre avantage. En effet, nous appelons une fonction Print() ayant un paramètre de type int. Or, nous lui transmettons une valeur de type double. Ainsi, une conversion implicite est déclenchée par le compilateur et l’affichage final donne 3 au lieu de 3.5. Nous aurions préféré avoir un message d’alerte nous indiquant que nous n’avions pas de fonction adéquate pour traiter ce type de variable.

#include <iostream>


void Print(int v)     // ==> implicit conversion of the argument 3.5f
{
   std::cout << v;    // ==> 3
}

int main()
{
        double x = 3.5;
        Print(x);
        return 0;
}

Note

Culture générale : une opération entre deux types identiques ne donnent pas forcément un résultat du même type ! Ainsi, l’addition de deux variables de type short (16 bits) donne un résultat de type int. Cependant, pour les types courants : int, float, double, le résultat d’une opération arithmétique entre deux variables de même type donne un résultat de type identique.

REGLE 2 : une opération arithmétique entre deux types numériques différents implique la conversion implicite du type le plus faible vers le type le plus fort.

Par exemple, le type double est considéré comme plus fort que le type entier. Ainsi, l’addition entre un entier et un double entraîne la conversion implicite de la valeur entière vers le type double. Voici un exemple :

#include <iostream>

int main()
{
        int    x = 4;
        double y = 7.5;

        // => x is first converted to double
        std::cout << x + y;     // ==>> 11.5
        return 0;
}

Pour chaque initialisation, indiquez si elle déclenche une conversion implicite et la valeur finale (en notant xxx.0 pour les doubles) :

double d = 1.0;

int i = 3.7;

double d = 2;

int i = 1;

int i = 0*sin(u);

Pour chacune des expressions, donnez son type et sa valeur :

Expression

Type

Valeur

2 + 3 / 2

2 + ( 5 % 2 )

2.0 + 3 / 2

2 + 3.0 / 2

5 <= 3 + 2

1.5 + 2

1.5 + 2.0

Les types composés

Les types composés (compound type) sont définis à partir des type fondamentaux.

Le type fonction

Le type fonction est défini à partir du type de chacun des paramètres ainsi que du type de la valeur de retour. Ainsi dans l’exemple suivant :

double Test(int a, int b) ...

Le nom Test est associé au type : fonction prenant deux entiers en paramètres et retournant un double.

Le type référence

Une référence crée un alias vers une variable déjà existante, nous en reparlerons plus tard. Le type référence se crée en ajoutant un symbole & (et commercial) après un type. Ainsi :

  • Le type int & se lit : référence vers un int.

  • Le type double & se lit : référence vers un double.

Le type pointeur

Le type pointeur vers un type T correspond à une adresse mémoire indiquant la position en mémoire d’une entité de type T.

Avertissement

Dans ce cours, appelé « Smart C++ », nous n’utiliserons pas de pointeur classique avec la notation *. Il faudra faire en sorte de ne jamais les utiliser dans votre code. Ce cours introduit le C++ moderne.

Le type structure

Une structure permet de regrouper plusieurs paramètres dans une sorte de container. C’est l’exemple même du type composé à partir de plusieurs types de base. Prenons l’exemple dans un jeu de rôle, nous avons un magicien qui dispose de plusieurs caractéristiques : points de vie, points de magie, pièces d’or.

struct Wizard
{
        int HP;        // Health Points
        int MP;        // Magic Points
        int Gold;      // Gold coins
};

Avertissement

Dans le langage C, la syntaxe des struct est différente. La version que nous présentons ici est la version propre au C++.

Le type enumeration

Une énumération répond à un besoin pratique. Prenons l’exemple d’un jeu où plusieurs écrans sont disponibles :

  • Ecran d’accueil

  • Partie en cours

  • Options de jeu

  • HighScores

Une variable entière peut permettre d’indiquer l’écran actif, en choisissant la valeur 1 pour représenter l’écran d’accueil, 2 pour la partie en cours et ainsi de suite. Cependant, ce choix n’est pas une bonne pratique de programmation car on va trouver dans le code des constantes 1, 2, 8… sans signification claire. Une approche plus explicite consiste à nommer chaque élément d’une liste de constante :

enum class Screen { Home, Game, Options, HighScores };

int main()
{
        Screen currentScreen = Screen::Home;
}

Ainsi, le nom Screen est de type enumeration.

Note

La présence du mot clef class n’a ici rien à voir avec les classes de la POO.

Pour chacune des affirmations suivantes, indiquez si elles sont vraies ou fausses :

  • En C++, le type fonction est donné uniquement par les paramètres de la fonction.

  • « Fonction recevant un entier et retournant un entier » correspond à un type de fonction.

  • Le type référence se construit en utilisant un signe *.

  • En C++ il existe les types composés et les types fondamentaux.

  • Le type struct est un type fondamental.

  • Une énumération permet d’éviter des constantes entières sans signification.

  • Une structure permet de regrouper plusieurs informations.

Nommer des types

Il est possible de donner un nom à un type grâce au mot-clef using pour simplifier notamment l’écriture de type long ou complexe.

Note

Pour la syntaxe, il faut penser à la définition d’une variable du type en question puis écrire cette définition sans donner le nom de la variable. Ainsi, pour créer un tableau T de 10 entiers, la syntaxe est : int T[10]. Ainsi, en retirant le nom T on obtient comme syntaxe pour le type tableau de 10 entiers : int[10].

SYNTAXE - Associer un nom à un type

using NouveauNomDeType = Type;

Prenons l’exemple d’un tableau d’entiers de taille 5x5 :

using T55 = int[5][5];   // T55 is a new name designating the type: 5x5 array of integers

int main()
{
        T55 arr;              // creation of a 5x5 array of integers
}

Pour chacune des affirmations suivantes, indiquez si elles sont vraies ou fausses :

  • Il est possible de nommer un type.

  • La syntaxe : using monint int; est correcte.

Surcharge de fonctions

Il existe un mécanisme en C++ permettant d’avoir plusieurs fonctions portant le même nom mais avec des paramètres de types différents.

Voici un exemple :

int    min(int a, int b) {...}
double min(double a, double b) {...}

Il semble naturel de nommer de manière identique des fonctions effectuant un traitement similaire même si elles acceptent des types différents. Dans cette configuration, on parle de surcharge de fonctions (function overloading) ou plus rarement de polymorphisme ad hoc (sans lien avec le polymorphisme d’héritage).

Le compilateur C++ utilise pour cela un mécanisme de résolution de surcharge (overload resolution). Pour résumer, lorsque le compilateur rencontre un appel de fonction, il liste l’ensemble des fonctions portant ce nom avec un nombre de paramètres compatibles. Ensuite, il utilise des règles précises pour prendre une décision. Voici un résumé de sa logique interne :

  • Si le compilateur trouve une fonction avec les types adéquats, il la choisit

  • Sinon, il liste toutes les fonctions compatibles quitte à effectuer des conversions implicites
    • Si le compilateur en trouve une seule, il la choisit

    • S’il en trouve plusieurs, le compilateur conserve les fonctions effectuant le moins de conversions implicites
      • S’il reste une seule fonction, le compilateur la choisit

      • Sinon il émet un message d’erreur car la situation est ambiguë

Voici un exemple :

#include <iostream>
int    min(int a, int b)            { return 1;   }
double min(double a, double b)      { return 1.2; }

int main()
{
        std::cout << min(3,4);      // the compiler will choose min(int,int);
        std::cout << min(2.0,1.0);  // the compiler will choose min(double,double);
        std::cout << min(3.0,5);    // undecidable case: 2 compatible functions, each requiring an implicit conversion
}

Pour le 3ème appel, l’erreur obtenue est : call of overloaded ‘min(double, int)’ is ambiguous.

Avertissement

Dans ce mécanisme, seuls les paramètres sont utilisés et non le type de retour. Par conséquent, l’exemple suivant ne correspond pas à une surcharge de fonction et il produit une erreur :

int    test(int a)      { return a;  }
double test(int a)      { return a + 1.0;}
  1. La surcharge de fonctions permet d’avoir des fonctions de même nom avec des paramètres de types différents.

  2. Si des fonctions sont disponibles, la résolution de surcharge trouve toujours une fonction adéquate.

  3. La surcharge de fonctions utilise le type de retour pour savoir quelle fonctionner appeler.

  4. La surcharge de fonctions peut déclencher des conversions implicites.

Soit le programme suivant:

1 int    F(int a)               {  return a; }
2 double F(double a)            {  return a; }
3 double F(int i, int j)        {  return i; }
4 double F(double i, int j)     {  return i; }

Pour chaque chaque appel de fonction, indiquez quelle définition de F() est utilisée ou Erreur si l’appel n’est pas valide.

Appel de fonction

Définition utilisée

F(1.0)

F(2)

F(2.0,5)

F(1, 2.0)

F(1.0, 2)

F(1.0, 2.0)