Variables et nombres 
Définition de variable¶
De la même façon qu'en OCaml, une variable en C possède un nom, un type et une valeur :
int x = 42; // définition d'une variable de type int (entier), de nom x, de valeur 42
Notons que toutes les instructions en C se terminent par un ;.
Il est possible de modifier une variable, comme en Python (ou comme les ref en OCaml) :
x = -3; // affectation de la valeur -3 à x
-3
Pour définir une variable constante, on ajoute le mot-clé const :
const float PI = 3.14; // PI est une constante
PI = 3; // erreur
input_line_10:3:4: error: cannot assign to variable 'PI' with const-qualified type 'const float' PI = 3; // erreur ~~ ^ input_line_10:2:14: note: variable 'PI' declared const here const float PI = 3.14; // PI est une constante ~~~~~~~~~~~~^~~~~~~~~
Interpreter Error:
Conversion de type¶
Contrairement à Python, il n'est pas possible de changer le type d'une variable :
x = 3.14;
input_line_24:2:6: warning: implicit conversion from 'double' to 'int' changes value from 3.14 to 3 [-Wliteral-conversion] x = 3.14; ~ ^~~~
En fait, l'interpréteur nous donne un warning : l'instruction a quand même fonctionné en convertissant implicitement le flottant 3.14 en entier, qui est devenu 3. Les conversions implicites de type sont considérées comme une mauvaise pratique, d'où ce warning.
Il est possible de rendre cette conversion (casting) explicite :
x = (int)3.74; // convertit 3.74 en entier (en prenant la partie entière)
3
Représentation des entiers¶
L'ordinateur ne peut stocker que des 0 et des 1. Les entiers doivent donc être convertis en binaire. Les entiers positifs sont stockés en base 2 :
Soit $n \in \mathbb{N}$ et $b \geq 2$ un entier (la base). Il existe une unique façon d'écrire $n$ comme somme de puissances de $b$. Dit autrement, il existe une unique suite d'entiers $n_0$, ... $n_{p-1}$ telle que : $$n = \sum_{k=0}^{p-1} n_k b^k$$ $$\forall k \in \{0, ..., p - 1\}, ~0 \leq n_k < b$$ On note $\boxed{n = {n_{p-1} ... n_1 n_0}_b}$. Lorsqu'on ne spécifie pas la base, il s'agit de la base 10.
Exemples de conversion depuis la base 10 :
- $11 = \boldsymbol1\times 3^2 + \boldsymbol0\times 3 + \boldsymbol2\times 0$ en base $3$ donc $11 = {\boldsymbol{102}}_3$
- $42 = 32 + 8 + 2 = 2^5 + 2^3 + 2^1$ donc $42 = {101010}_2$
Exemples de conversion vers la base 10 :
- ${10011}_2 = 2^4 + 2^1 + 2^0 = 16 + 2 + 1 = 19$
- ${304}_5 = 3\times 5^2 + 4 = 79$
Exercice
Que vaut ${101010}_2 + {10011}_2$ ? On fera tous les calculs en base 2.
Exercice
Quel est l'entier maximum dont la représentation en base 2 utilise $p$ bits ?
Solution
Il s'agit de l'entier dont la représentation en base $2$ ne contient que des $1$ : $${\underbrace{11...11_2}_p} = \sum_{k=0}^{p - 1}2^k = \frac{2^p - 1}{2 - 1} = \boxed{2^p - 1}$$
Entier non signé (unsigned)¶
Un unsigned (unsigned int : entier non signé, c'est-à-dire positif) est un entier positif, qui est stocké en mémoire avec sa représentation en base 2 :
unsigned x = 42;
x // affichage de la valeur de x
42
On peut connaître le nombre d'octets utilisés par une variable avec la fonction sizeof :
sizeof(x) // un unsigned utilise 4 octets ici, c'est à dire 4*8 = 32 bits
4
La plus grande valeur d'un unsigned est donc $2^{32} - 1 = 4294967295$. Si on dépasse, on revient à $0$ :
x = 4294967295; // unsigned maximum
printf("%u", x); // affiche x
x = x + 1; // x vaut maintenant 0
4294967295
0
Il est possible d'utiliser un nombre de bits différents avec, par exemple, unsigned16_t permettant de stocker un entier non signé sur 16 bits (2 octets) :
unsigned16_t y = -1; // un unsigned ne peut pas être négatif
// -1 est automatiquement convertit en l'entier non signé maximum
y // le plus grand unsigned16 est 2**16 - 1 = 65535
65535
Entier signé (int)¶
int est un type d'entier qui peut être positif ou négatif.
On pourrait imaginer stocker un int en utilisant un bit pour le signe, mais cela rendrait les opérations sur les entiers plus compliqués.
À la place, les int sont stockés avec une représentation par complément à deux. Soit $p$ le nombre de bits utilisés pour un int ($4$ octets, c'est-à-dire $p = 32$ bits en général). Un int permet de stocker tout entier $n$ entre $-2^p$ et $2^{p-1} - 1$ :
- si $n \geq 0$, $n$ est stocké en mémoire avec sa représentation en base 2
- si $n < 0$, $n$ est stocké en mémoire en utilisant la représentation en base 2 de $2^{p-1} - 1 + n$ (qui est positif)
int x = -42; // définition d'un int
$-42$ est alors stocké en mémoire par la représentation en base $2$ de $2^{32} - 1 - 42$, c'est-à-dire de $4294967253$.
Il faut faire attention à ne pas dépasser la taille d'un int (32 bits par défaut), sinon on revient sur l'entier le plus petit :
int x = 2147483647; // valeur maximum d'un int (2**31 - 1)
printf("%d", x + 1); // donne l'entier minimum (-2**32)
int y = 2147483648; // cet entier ne peut pas être stocké dans un `int`
input_line_110:4:9: warning: implicit conversion from 'long' to 'int' changes value from 2147483648 to -2147483648 [-Wconstant-conversion]
int y = 2147483648; // cet entier ne peut pas être stocké dans un `int`
~ ^~~~~~~~~~
-2147483648
Remarque : La taille d'un unsigned ou int peut varier suivant le compilateur.
De même que pour les unsigned, on peut spécifier la taille d'un int avec, par exemple, int_t8 qui utilise 8 bits en mémoire (1 octet) :
int8_t x = 127; // 127 est la plus grande valeur signée que l'on peut stocker sur 8 bits
printf("%d\n", x); // affichage de la valeur de x
x = x + 1;
printf("%d", x); // dépassement !
127 -128
4
Représentation des flottants¶
Un float en C est typiquement stocké avec 32 bits (4 octets), suivant la norme IEEE754 qui consiste à l'écrire sous forme scientifique avec :
- 1 bit pour le signe (0 pour positif, 1 pour négatif)
- 8 bits pour l'exposant (puissance de 2)
- 23 bits pour la mantisse (chiffres après la virgule)
L'exposant et la mantisse sont stockés en base 2.
Exemple :
$$\begin{array}{|c|c|c|c|c|c|c|c|c|c|c|c|} \hline \color{red}{\textbf{1}} & \color{green}{\textbf{0}} & ... & \color{green}{\textbf{0}} & \color{green}{\textbf{1}} & \color{green}{\textbf 1} & \color{blue}{\textbf 1} & \color{blue}{\textbf 0} & \color{blue}{\textbf 1} & \color{blue}{\textbf 0} & ... & \color{blue}{\textbf 0} \\ \hline \end{array}$$représente: $\color{red}{\textbf{-}} 1,\color{blue}{625} \times 2^{\color{green}{\textbf{3}}}$ En effet :
- Le 1er bit de signe est égal à $\color{red}{1}$, donc c'est un nombre négatif
- Les 8 bits suivants correspondent à $\color{green}{0000011}_2 = 2^1 + 2^0 = \color{green}{\textbf{3}}$
- Les 23 derniers bits correspondent à $0,\color{blue}{101}_2 = 0,\color{blue}{625}$
float x = 3.14; // définition d'un flottant
sizeof(x) // 4 octets
4
Il existe aussi des double (flottant double précision) qui s'utilisent comme des float mais en utilisant 64 bits au lieu de 32.
Remarque : En Python et OCaml les float sont stockés sur 64 bits (ce sont donc plutôt des double).
double x = 3.14;
sizeof(x) // 8 octets soit 64 bits
8
Opérations numériques¶
Les opérations sur les nombres sont pour la plupart similaires à Python et OCaml :
printf("%d ", 2+3);
printf("%d ", 2*3);
printf("%d ", 2-3);
5 6 -1
printf("%d", false);
0
Le comportement de / est différent suivant que les opérandes soient entiers ou flottants :
7/3 // division entière (quotient de la division euclidienne de 7 par 3)
2
7./3. // division flottante
2.3333333
% permet de calculer le modulo (reste de la division euclidienne) :
7 % 3
1
Comparaison OCaml / C / Python :
| OCaml | C | Python | |
|---|---|---|---|
| Quotient flottant | /. |
/ |
/ |
| Quotient entier | / |
/ |
// |
| Modulo | mod |
% |
% |
Opérations bit à bit¶
& permet de faire le et binaire de 2 entiers : a & b donne un entier dont le $i$ème bit est à 1 si les $i$ème bits de a et b sont à 1, 0 sinon.
Exemple : si a = 13 = 1101$_2$ et b = 25 = 11001$_2$ alors a & b = 1001$_2$ = 9. En effet :
13 & 25
9
De façon similaire, le ou binaire de deux entiers s'utilise avec |. Le $i$ème bit est égal à 1 si et seulement si au moins l'un des deux bits correspondants est égal à 1.
Par exemple, 13 | 25 = 1101$_2$ | 11001$_2$ = 11101$_2$ = 29 :
13 | 25
29
Le ou exclusif binaire de deux entiers s'utilise avec ^. Le $i$ème bit est égal à 1 si et seulement si les deux bits correspondants sont différents.
Par exemple, 13 ^ 25 = 1101$_2$ ^ 11001$_2$ = 10100$_2$ = 20 :
13 ^ 25
20
Remarque : ces opérations bit à bit fonctionnent aussi en Python.
Exercice
Calculer à la main 29 & 23, 29 | 23 et 29 ^ 23 puis vérifier.
Le shift << permet de décaler la représentation binaire d'un entier. Par exemple, comme $13 = 1101_2$, 13 << 2 donne $110100_2 = 2^5 + 2^4 + 2^2 = 52$ :
13 << 2
52
On se sert souvent du shift sur $1$, ce qui permet d'obtenir des puissances de $2$ :
1 << 10 // 2**10
1024
~ est l'opérateur de négation binaire, qui inverse les bits (0 devient 1 et inversement). Par exemple, sur 8 bits, $52 = 00110100_2$ donc ~$52$ = $11001011_2 = 2^7 + 2^6 + 2^3 + 2 + 1 = 128 + 64 + 8 + 2 + 1 = 203 ~(= 256 - 52)$ :
unsigned8_t x = 52;
x = ~x;
printf("%u", x)
203
3
&, |, << et ~ sont des opérations directement exécutées par le processeur, donc très rapides.