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.