Rappels : les types de base
Rappelons que l'opérateur
sizeof appliqué à un
type ou une variable donne la taille de l'espace occupé en mémoire exprimée en nombre d'octets.
Le nombre associé est de type
long int.
Le traitement de tous les
sizeof est réalisé dès la compilation :
cela impose que les types utilisés soient suffisamment définis pour que cette taille soit
calculable. L'utilisation de
sizeof ne donne lieu à aucun appel lors de
l'exécution du programme.
Il est donc possible d'utiliser
sizeof dans des (utilisables dans un
switch) ou pour définir un tableau.
Rappel sur les types scalaires de base du langage C :
- le type
char : il s'agit du type caractère.
En C les caractères sont codés sur 8 bits (code ASCII)
(donc sizeof(char) est égal à 1).
Selon les machines ils correspondent à des entiers signés (compris entre
-128 et +127) ou non signés (compris entre 0 et 255).
C'est pourquoi, on peut adjoindre au type char les qualificatifs signed
et unsigned.
- le type entier
int, qui peut être qualifié d'une part
short ou long et
d'autre part signed (qualificatif par défaut) ou unsigned.
Avec les premiers qualificatifs, int peut être omis.
Ainsi, unsigned short int, long int, long ou signed long sont des types acceptables.
Il existe par ailleurs maintenant le type long long .
La norme ne dit rien sur la taille de ces types, d'où l'intérêt d'utiliser l'opérateur
sizeof lors de la rélisateur de certaines opérations pour garantir
une portabilité maximale (par exemple lors d'opérations de décalage).
La seule chose qu'on sait est que
sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long).
Ce que l'on peut considérer comme les valeurs les plus courantes :
| |
short
| int
| long
| long long
|
| taille |
2 |
4 |
4 |
8 |
- les types de nombre en virgule flottante type
float, double et long double.
Les nombres non signés de ce type ne sont généralement pas supportés.
La taille des valeurs correspondantes dépend des machines et systèmes.
- Les types
float et double
sont généralement de taille respective 4 et 8;
- pour le type
long double, on trouve par exemple
12 sur un système Linux et 16 sur MacOS-Darwin.
Les constantes flottantes sont, comme en Java, de type double.
Conversions de types ou transtypages)
Introduction
▶ Lorsque dans une expression arithmétique on mélange le type
des opérandes, les données de type plus petit sont automatiquement convertis en le type le plus grand :
ainsi lorsqu'on ajoute un
int et un
float, l'entier est automatiquement tranformé
en flottant.
▶ Dans certaines opérations, le type des opérandes est essentiel et conditionne la nature des opérations et
des résultats.
Un exemple simple en est la division : appliqué à deux opérandes entiers, l'opérateur
de division
/ réalise une division euclidienne (entière.
Ainsi si
x et
y
sont des variables entières de valeurs respectives 12 et 5, l'expression
x/y
a pour valeur 2.
Si on veut obtenir une division réelle, il faut transformer au moins l'un des deux opérandes en une
grandeur de type
float.
Conversion implicite vs. conversion explicite
On peut distinguer deux types de conversion de types :
▶
les conversions implicites : elles sont réalisées de
manière automatique sans provoquer d'erreur lorsque dans une expression
des opérandes de types différents sont utilisés.
Les règles utilisées sont dans l'ordre les suivantes :
- si l'un des opérandes est de type
double, l'autre est converti en double
- si l'un des opérandes est de type
float, l'autre est converti en float
- les opérandes de type
char ou short sont convertis en int
- si l'un des opérandes est de type
long, l'autre est converti en long
▶
les conversions explicites : elles sont réalisées à la demande explicite du programmeur
au travers d'un
cast (ou
coercition). Ainsi :
- si
x est une variable de type int, « (float)  »
est la valeur de x vue comme un
nombre de type float (et donc représentée comme tel en interne);
- si
x et y sont des variables de type int,
un expression telle que « (float) x / y » calcule la division
réelle des valeurs des deux variables;
- si
ptr1 et ptr2 sont respectivement des pointeurs sur les types T1 et
T2, il est possible d'écrire
ptr1 = (T1 *) ptr2;
pour affecter la valeur de ptr2 à ptr1 et donc voir le contenu de la
zone pointée par ptr2 comme une grandeur de type T1.
Les valeurs booléennes
▶
Il n'y a pas de type booléen en langage C.
▶ Toute expression arithmétique (à valeur entière)
possède une valeur logique/booléenne :
- la valeur 0 correspond
à la valeur logique « FAUX »
- tout ce qui n'a pas comme valeur 0
a comme valeur logique « VRAI »
▶ La valeur « VRAI » par défaut est l'entier 1 (valeur affectée à
un entier dont la valeur est le résultat d'un test vrai).
▶ L'exemple suivant illustre quelques aspects de ce mode particulier
de traitement du calcul booléen en langage C
et les liens qui y existent entre entiers et booléens :
--> cat booleen.c
#include <stdio.h>
void main( ) { // on suppose que le binaire s'appelle booleen
int x = 5, y = 7;
int z = x < y;
printf("valeur de %d < %d : %d\n", x, y, z);
printf("non z = %d\n", !z);
z = x > y;
printf("valeur de %d > %d : %d\n", x, y, z);
printf("non z = %d\n", !z);
printf("non %d = %d\n", x, !x);
if (x) // test équivalent à (x != 0)
printf("vrai\n");
else
printf("faux\n");
return EXIT_SUCCESS; // return 0;
}
--> ./booleen
valeur de 5 < 7 : 1
non z = 0
valeur de 5 > 7 : 0
non z = 1
non 5 = 0
vrai
-->
Les énumérations
▶ Un type énumération permet de définir des listes de
noms symboliques correspondant à des valeurs entières.
▶ Tout type construit sur ce modèle est de fait une « copie »
du type
int dans laquelle un certain nombre de constantes symboliques ont
été définies par exemple à des fins de lisibilité
des applications. Définir une énumération ne définit
donc pas un domaine de définition mais simplement quelques constantes
symboliques pour des entiers (un entier peut d'ailleurs posséder plusieus
constantes symboliques dans une même énumération).
▶ Ce type de défintions est une alternative à la définition de constantes
avec la directive
#define du préprocesseur C.
Exemple 1
Dans ce premier exemple, qui permet d'introduire la syntaxe utilisée
on définit le type
booleen comme énumération
dans laquelle deux constantes nommées
faux et
vrai sont définies :
--> cat enum1.c
#include <stdio.h>
enum booleen { // l'énumération se utilisable via « enum booleen »
faux, // faux est implicitement un nom symbolique pour l'entier 0
true, // true est implicitement un nom symbolique pour l'entier 1
vrai = 1 // vrai est explicitement un nom symbolique pour l'entier 1
false = vrai - 1
};
int main( ) {
enum booleen b1, b2, b3; // déf. de 3 variables du type « enum booleen »
printf("%d\n", false);
b1 = faux;
if(b1 == vrai) printf("oui\n");
else printf("non\n");
b2 = b1 + 1;
printf("valeur de b2 : %d\n", b2);
if(b2 == true && b2 == vrai) printf("oui\n");
else printf("non\n");
b3 = 10;
printf("valeur de b3 : %d\n", b3);
b3 = -20 + b1 + b2;
printf("valeur de b3 : %d\n", b3);
return EXIT_SUCCESS; // return 0;
}
--> ./enum1
0
non
valeur de b2 : 1
oui
valeur de b3 : 10
valeur de b3 : -19
-->
Exemple 2
Ce deuxième exemple montre que définir une énumération
définit un ensemble de constantes symboliques plutôt qu'un type.
Ces constantes peuvent être utilisées en lieu et place d'entiers
(une telle définition est équivalente à une
séquence de
#define.
Une même constante ne peut pas être définie
plusieurs fois (même dans des énumérations différentes).
--> cat enum2.c
#include <stdio.h>
enum e1 {
bleu = 0, // #define bleu 0
blanc = 1, // #define blanc 1
rouge = 2 // #define rouge 2
};
enum e2 {
vert = 0, // #define vert 0
jaune = 1, // #define jaune 1
// bleu = 3, provoquerait une erreur d'ambiguité
gris = 4 // #define gris 4
};
int n;
enum e1 c1;
enum e2 c2;
int main( ) {
n = jaune;
c1 = blanc;
c2 = c1;
c1 = gris;
printf ("c1 = %d\n", c1);
printf ("c2 = %d\n", c2);
printf ("n = %d\n", n);
return 1;
}
--> ./enum2
c1 = 4
c2 = 1
n = 1
-->
Les structures
Définition d'une structure
▶ Il s'agit de pouvoir regrouper dans une seule entité des
valeurs de types éventuellement différents et de pouvoir
accéder à chacun de ces constituants (appelés
champs
ou
membres) individuellement.
▶ La description de la structure est
- introduite par le mot-clé (ou réservé)
struct ;
- éventuellement nommée au moyen d'un identificateur suivant
ce mot clé (séparé par au moins un espace). On parle de structure anonyme si cette possibilité de nommage n'est pas utilisée ;
- définie entre accolades par une suite de déclarations
(type identificateur) donnant les types et les noms des
différents champs de la structure. Ces déclarations sont séparées par des
;
(des champs de même type peuvent être énumérés
simultanément en séparant leurs noms par des virgules).
▶ Des exemples de définitions de structures sont données ci-après:
- une structure de nom
couple_entiers correspondant à un couple d'entiers :
struct couple_entiers {
int a;
int b;
};
- une structure de nom
adresse possible pour coder une adresse postale :
struct adresse {
int num;
char rue[40];
long code;
char ville[20], pays[20]; /* deux champs tableaux de char */
};
- une structure de nom
individu dont l'un des champs
a la structure adresse précédente (dont la définition est supposée connue)
et contenant la définition d'une structure interne anonyme pour le champ de
nom identite :
struct individu{
struct {
char nom[20];
char prenom[20];
} identite;
int age;
struct adresse domicile;
}
La structure correspond à l'arborescence suivante :
▶ Il est évidemment possible de créer des
alias sur une structure
en utilisant
typedef comme dans :
typedef
struct {
int a, b; // a et b dans la même déclaration
} couple;
Déclaration de variables associées à une structure donnée
▶ La déclaration d'une variable
cpl ayant la structure
couple_entiers définie précédemment :
struct couple_entiers cpl;
▶ Une telle déclaration aurait pu être réalisée en même temps que la déclaration de la structure elle-même :
struct couple_entiers {int a, b;} cpl;
Il est important de noter, dès à présent, que lorsqu'on définit une variable
dont le type est une structure, il lui correspond en mémoire
un espace de la taille de la structure. Ceci est différent de ce qui
ce passe par exemple en Java où, lorsqu'on déclare une
variable dont le type est une classe, il n'est pas alloué
d'espace pour un objet de ce type, mais l'espace pour une
référence vers un tel objet. Nous y reviendrons plus loin.
▶ Plusieurs variables peuvent être définies simultanément.
Dans l'exemple suivant, on
définit deux variables
adr1 et
adr2
ayant la structure
adresse et une variable
ptr_adresse
de type « pointeur sur adresse » :
struct adresse adr1, adr2, *ptr_adresse;
Initialisation d'une variable ayant une structure donnée
Elle peut être réalisée :
- après déclaration de la variable par affectation de valeurs à chacun des champs;
- simultanément à la déclaration de la variable
en faisant suivre le nom de la variable du symbole d'affectation
= suivi d'une liste de valeurs entre accolades. Ces valeurs
sont affectées dans l'ordre aux différents champs
(s'il n'y a pas assez de valeurs, la valeur nulle est affectée aux autres).
Opérations possibles
- l'accès aux champs d'une structure se fait avec un point « . »
- l'affectation d'une variable à une autre (de même structure)
est possible.
- la comparaison de deux variables ayant la même structure avec les opérateurs
== et != est
interdite (erreur de compilation).
struct couple_entiers {int a, b;} cpl1 = {2}; /* le champ de nom a vaut 2, le champ n'est pas initialisé */
struct couple_entiers cpl2 = {4, 5}; /* les champs a et b sont initialisés respectivement à 3 et 4 */
Exemples
▶ Une structure correspondant à un nombre complexe dont la partie réelle et
la partie imaginaire sont de type
double et la définition
de deux variables
c1 et
c2 ayant cette structure
(s'agissant d'une définition l'espace mémoire nécessaire à chaque variable est alloué) :
struct complexe {
double reel;
double imag;
} c1, c2;
..........
c1.reel = 1.13;
c1.imag = -2.25;
c2 = c1; // copie physique : c2.reel=c1.reel; c2.image=c1.imag;
..........
struct complexe c3 = {21.5, 4}; // c3.reel = 21.5; c3.imag = 4;
Taille d'une structure
La taille d'une structure est a priori égale à la somme des tailles des champs qui la constituent.
Ainsi, on a pour la structure
complexe définie ci-dessus :
- sizeof(double) = 8
- sizeof(struct complexe) = 16, c'est-à-dire 2 * sizeof(double).
Dans cet autre exemple, utilisant pour l'un de ses champs, le type
couleur
définie comme une énumération, la structure ainsi définie :
struct point_colore {
float abs, ord;
enum { // énumération anonyme
blanc = 1,
bleu = 3,
rouge,
noir = 10
} couleur; // champ couleur du type enum définie localement
int intensite;
};
a comme taille 16 (sizeof(int) + 2 * sizeof(float) + sizeof(couleur), chacune de ces tailles étant égale à 4).
Cependant les choses peuvent ne pas être aussi simples.
Si on considère les trois structures
st1,
st2 et
st3 suivantes :
struct st1 {
char a;
int distance;
char b;
couleur couleur;
};
struct st2 {
char a;
char b;
int distance;
couleur couleur;
};
struct st3 {
char a;
char b;
char c;
char d;
int distance;
couleur couleur;
};
on a pu observer que :
- sizeof(struct st1) = 16
- sizeof(struct st2) = 12
- sizeof(struct st3) = 12
L'explication en est dans le problème de l'
alignement des adresses.
Les structures anonymes
Ainsi que nous l'avons dit, il est possible de nommer une structure sans lui de donner de nom. Cela n'a d'intérêt que
si on y déclare
instantanément des variables.
Il est important de noter que des variables définies dans deux définitions identiques d'une structure ne pas considérées comme étant de même type,
ce qu'illustre l'exemple suivant :
--> cat structAnonyme.c
#include <stdio.h>
struct {int a, b;} c1 = {3}, c2 = {4, 5}, c3;
struct {int a, b;} c4;
void main( ) { // on suppose que le binaire s'appelle booleen
c3 = c2; /* affectation légale : variables de même type */
printf("%d %d\n", c3.a, c3.b);
c4 = c1; /* affectation illégale : variables de type différents */
return 0;
}
--> gcc structAnonyme.c
structAnonyme.c: In function 'main':
structAnonyme.c:7: error: incompatible types in assignment
-->
Les unions
Définition
Alors que les structures permettent la concaténation d'informations,
les unions visent en quelque sorte à en permettre la superposition. Une union
permet ainsi d'interpréter de différente manière un même emplacement
en mémoire.
Du point de vue syntaxique, la définition ou l'utilisation d'une union
obéit aux mêmes règles que les structures, le mot clé
union remplaçant le mot clé
struct.
Dans l'exemple suivant, on définit une union dont la définition
reflète le fait qu'un mot de la mémoire
d'un ordinateur (qui n'est jamais qu'une suite d'un certain nombre [par exemple 32] de bits) est interpétable
comme un nombre entier, une suite de deux entiers courts (ces grandeurs pouvant être ou non signées),
un flottant, un tableau de 4 caractères (signés ou non), voire comme une instruction machine.
union mot {
signed char car[4];
unsigned char ucar[4];
short sh[2];
unsigned short ush[2];
int ent;
unsigned int uent;
float reel;
struct instruction inst; // supposée définie par ailleurs
};
La taille d'une union est égale à la taille de son membre
le plus « long » (les différents membres ne sont pas nécessairement
tous de même taille).
Ainsi en supposant que la taille de la structure
inst soit 8,
la taille de l'union
mot est 8.
Accès aux membres constituant une union
Une union permet d'avoir des vues différentes d'un même espace (ses membres).
L'accès à l'un de ces membres est réalisé avec l'opérateur «
. »
En reprenant la définition de l'union
mot précédente,
dans la séquence suivante, on :
- déclare une variable m de ce type;
- on initialise l'espace qui lui correspond en affectant à l'un de ses membres
(ici le membre de type
int) une valeur du type correspondant;
- on affiche l'interpétation de ce contenu selon les différentes autres vues
(accédées via les autres membres de l'union).
..........
union mot m ;
m.ent = 0x6789ABCD; // binaire 01100111100010011010101111001101
printf("m.ent = %d\n", m.ent);
printf("m.reel : %f\n", m.reel);
printf("m.sh : %d [%x] %d [%x]\n",
m.sh[0], m.sh[0], m.sh[1], m.sh[1]);
printf("m.ush : %d [%x] %d [%x]\n",
m.ush[0], m.ush[0], m.ush[1], m.ush[1]);
printf("m.car : %d %d %d %d\n",
m.car[0], m.car[1], m.car[2], m.car[3]);
printf("m.ucar : %d %d %d %d\n",
m.ucar[0], m.ucar[1], m.ucar[2], m.ucar[3]);
...........
Son exécution donne :
m.ent = 1737075661 // binaire 01100111100010011010101111001101
m.reel : 1300266746393047005659136.000000
m.sh : -21555 [ffffabcd] 26505 [6789]
m.ush : 43981 [abcd] 26505 [6789]
m.car : -51 -85 -119 103
m.ucar : 205 171 137 103
Ces résultats résultent de la représentation interne des types des membres de cette union (rappels).
Définition de nouveaux types
Alors qu'en C++ par exemple, la définition d'une énumération, d'une structure ou d'une union
nommée définit implicitement un nouveau type, en C il n'en est rien : la définition d'un nouveau type
utilisable pour déclarer directement des variables, nécéssite sa définition en utilisant
le mot-clé
typedef.
Pour se rappeler son utilisation, il suffit d'imaginer qu'on veut déclarer une variable
du type, utiliser le nom qu'on veut donner au type en guise de nom de variable et
faire précéder le tout du mot clé
typedef.
- avec la la séquence :
typedef unsigned short ushort;
ushort devient un alias (synonyme) de
unsigned short.
Il est ensuite possible de déclarer ou définir une variable x par
ushort x;
- avec la séquence
typedef
enum {
blanc = 1,
bleu = 3,
rouge,
noir = 10
}
couleur;
couleur devient un synonyme de l'énumération anonyme et une déclaration telle que
couleur c;
est possible.
- la séquence :
typedef void (*sighandler_t)(int);
définit le type sighandler_t
comme pointeur sur fonction ne renvoyant pas de valeur et
ayant un paramètre de type int.