Les types
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 unique type int mais il peut exister aucune, une seule 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
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…
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 entre valeurs numériques 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 a = 3.5; // conversion implicite de double en int
cout << a; // ===> 3
double f = 3; // conversion implicite int vers double
cout << f; // ===> 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 Affichage() ayant un paramètre de type int. Hors, 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 Aff(int v) // ==> conversion implicite de l'argument 3.5f
{
std::cout << v; // ==> 3
}
int main()
{
double a = 3.5;
Aff(a);
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 a = 4;
double b = 7.5;
// => a est d'abord converti en double
std::cout << a + b; // ==>> 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) :
|
|
|
|
|
|
|
|
|
Pour chacune des expressions, donnez son type et sa valeur :
Expression |
Type |
Valeur |
---|---|---|
|
||
|
||
|
||
|
||
|
||
|
||
|
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.
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 magicien
{
int PV;
int PMagie;
int PiecesOR;
};
Avertissement
Dans le langage C, la syntaxe des struct est différente. La version que nous présentons ici est la version simplifiée 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 Ecran { Accueil, Jeu, Options, HighScores };
int main()
{
Ecran EcranCourant = Ecran::Accueil;
}
Ainsi, le nom Ecran 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 est un nouveau nom désignant le type tableau d'entiers de taille 5x5
int main()
{
T55 T2; // création d'un tableau d'entiers de taille 5x5
}
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); // le compilateur va choisir min(int,int);
std::cout << min(2.0,1.0); // le compilateur va choisir min(doule, double);
std::cout << min(3.0,5); // cas indécidable : 2 fonctions compatibles impliquant une conversion implicite chacune
}
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;}
La surcharge de fonctions permet d’avoir des fonctions de même nom avec des paramètres de types différents.
Si des fonctions sont disponibles, la résolution de surcharge trouve toujours une fonction adéquate.
La surcharge de fonctions utilise le type de retour pour savoir quelle fonctionner appeler.
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) |