Leçon 4
- Hexadecimal
- Pointers
- Chaînes de caractères
- Comparaisons et copies
- Valgrind
- Échanges
- Disposition en mémoire
- get_int
- Fichiers
- JPEG
Hexadecimal
- Durant la semaine 0, nous avons appris le système binaire, un système de comptage avec des 0 et des 1.
- Durant la semaine 2, nous avons parlé de la mémoire et de la façon dont chaque octet possède une adresse, ou identifiant, pour que nous puissions référencer l’endroit où nos variables sont réellement stockées.
- Il s’avère que, par convention, les adresses pour la mémoire utilisent le système de comptage hexadécimal, dans lequel il y a 16 chiffres : 0-9 et A-F.
-
Rappelons que, en binaire, chaque chiffre correspondait à une puissance de 2 :
128 64 32 16 8 4 2 1 1 1 1 1 1 1 1 1
- Avec 8 bits, nous pouvons compter jusqu’à 255.
-
Il s’avère qu’en hexadécimal, nous pouvons parfaitement compter jusqu’à 8 bits binaires avec seulement 2 chiffres :
16^1 16^0 F F
- Ici, « F » est une valeur de 15 en décimal, et chaque place est une puissance de 16. Donc le premier « F » est 16^1 _ 15 = 240, plus le deuxième « F » avec une valeur de 16^0 _ 15 = 15, pour un total de 255.
-
Et « 0A » est la même chose que 10 en décimal, et « 0F » la même chose que 15. « 10 » en hexadécimal serait 16, et nous le prononcerions « un zéro en hexadécimal » plutôt que « dix », pour éviter toute confusion.
- Le système de couleurs RVB utilise également traditionnellement l’hexadécimal pour décrire la quantité de chaque couleur. Par exemple, « 000000 » en hexadécimal signifie 0 pour chaque couleur rouge, verte et bleue, pour une couleur noire. Et « FF0000 » serait 255, ou la quantité la plus élevée possible, de rouge. Avec des valeurs différentes pour chaque couleur, nous pouvons représenter des millions de couleurs différentes.
- À l’écrit, nous pouvons aussi indiquer qu’une valeur est en hexadécimal en la faisant précéder de « 0x », comme dans « 0x10 », où la valeur est égale à 16 en décimal, par opposition à 10.
Pointeurs
-
Nous pouvons créer une valeur
n
et l'imprimer :#include <stdio.h> int main(void) { int n = 50; printf("%i\n", n); }
-
Dans la mémoire de notre ordinateur, il y a maintenant 4 octets quelque part qui ont la valeur binaire 50, étiquetés
n
: - Il s'avère qu'avec les milliards d'octets en mémoire, ces octets pour la variable
n
commencent à une adresse unique qui pourrait ressembler à0x12345678
. -
En C, nous pouvons réellement voir l'adresse avec l'opérateur
&
, qui signifie « obtenir l'adresse de cette variable » :#include <stdio.h> int main(void) { int n = 50; printf("%p\n", &n); }
- Et dans l'IDE CS50, nous pourrions voir une adresse comme
0x7ffe00b3adbc
, où il s'agit d'un emplacement spécifique dans la mémoire du serveur.
- Et dans l'IDE CS50, nous pourrions voir une adresse comme
-
L'adresse d'une variable est appelée un pointeur, que nous pouvons considérer comme une valeur qui « pointe » vers un emplacement en mémoire. L'opérateur
*
nous permet « d'aller » à l'emplacement vers lequel pointe un pointeur. -
Par exemple, nous pouvons imprimer
*&n
, où nous « allons » à l'adresse den
, et cela imprimera la valeur den
,50
, puisque c'est la valeur à l'adresse den
:#include <stdio.h> int main(void) { int n = 50; printf("%i\n", *&n); }
-
Nous devons également utiliser l'opérateur
*
(d'une manière malheureusement déroutante) pour déclarer une variable que nous voulons être un pointeur :#include <stdio.h> int main(void) { int n = 50; int *p = &n; printf("%p\n", p); }
- Ici, nous utilisons
int *p
pour déclarer une variable,p
, qui a le type de*
, un pointeur, vers une valeur de typeint
, un entier. Ensuite, nous pouvons imprimer sa valeur (quelque chose comme0x12345678
), ou imprimer la valeur à son emplacement avecprintf("%i\n", *p);
.
- Ici, nous utilisons
-
Dans la mémoire de notre ordinateur, les variables pourraient ressembler à ceci :
- Nous avons un pointeur,
p
, avec l'adresse d'une variable.
- Nous avons un pointeur,
- Nous pouvons maintenant faire abstraction de la valeur réelle des adresses, car elles seront différentes lorsque nous déclarerons des variables dans nos programmes, et simplement penser à
p
comme « pointant vers » une valeur : - Supposons que nous ayons une boîte aux lettres étiquetée « 123 », avec le numéro « 50 » à l'intérieur. La boîte aux lettres serait
int n
, car elle stocke un entier. Nous pourrions avoir une autre boîte aux lettres avec l'adresse « 456 », à l'intérieur de laquelle se trouve la valeur « 123 », qui est l'adresse de notre autre boîte aux lettres. Ce seraitint *p
, puisqu'il s'agit d'un pointeur vers un entier. - Grâce à la possibilité d'utiliser des pointeurs, nous pouvons créer différentes structures de données, ou différentes manières d'organiser les données en mémoire que nous verrons la semaine prochaine.
- De nombreux systèmes informatiques modernes sont « 64 bits », ce qui signifie qu'ils utilisent 64 bits pour adresser la mémoire. Un pointeur fera donc 8 octets, soit deux fois plus qu'un entier de 4 octets.
string
- On pourrait avoir une variable
string s
pour un nom tel queEMMA
et pouvoir accéder à chaque caractère avecs[0]
et ainsi de suite :
- Mais il s’avère que chaque caractère est stocké en mémoire dans un octet avec une adresse donnée, et
s
est en fait juste un pointeur avec l’adresse du premier caractère :
- Et puisque
s
est juste un pointeur vers le début, seul le\0
indique la fin de la chaîne. - En fait, la CS50 Library définit une
string
avectypedef char *string
, qui dit juste que nous voulons nommer un nouveau type,string
, comme unchar *
, ou un pointeur vers un caractère. -
Imprimons une chaîne :
#include <cs50.h> #include <stdio.h> int main(void) { string s = "EMMA"; printf("%s\n", s); }
-
C’est familier, mais nous pouvons simplement dire :
#include <stdio.h> int main(void) { char *s = "EMMA"; printf("%s\n", s); }
- Cela imprimera également
EMMA
.
- Cela imprimera également
-
Avec
printf("%p\n", s);
, nous pouvons imprimers
comme sa valeur en tant que pointeur, comme0x42ab52
. (printf
sait aller à l’adresse et imprimer la chaîne entière lorsque nous utilisons%s
et transmettonss
, même sis
pointe uniquement vers le premier caractère.) - Nous pouvons également essayer
printf("%p\n", &s[0]);
, qui est l’adresse du premier caractère des
, et c’est exactement la même chose que d’imprimers
. Et l'impression de&s[1]
,&s[2]
et&s[3]
nous donne les adresses qui sont les caractères suivants en mémoire après&s[0]
, comme0x42ab53
,0x42ab54
et0x42ab55
, exactement un octet après l'autre. - Et enfin, si on essaie de
printf("%c\n", *s);
, on obtient un seul caractèreE
, puisqu'on va à l'adresse contenue danss
, qui contient le premier caractère de la chaîne. - En fait,
s[0]
,s[1]
ets[2]
sont réellement mappés directement sur*s
,*(s+1)
et*(s+2)
, puisque chacun des caractères suivants se trouve juste à l'adresse de l'octet suivant.
Comparer et copier
-
Examinons
compare0
:#include <cs50.h> #include <stdio.h> int main(void) { // Obtenir deux entiers int i = get_int("i : "); int j = get_int("j : "); // Comparer les entiers if (i == j) { printf("Identique\n"); } else { printf("Différent\n"); } }
- Nous pouvons compiler et exécuter ce code, et notre programme fonctionne comme prévu : si les deux entiers sont identiques, le résultat est « Identique », et s'ils sont différents, le résultat est « Différent ».
-
Dans
compare1
, nous constatons que des valeurs de chaîne identiques font que notre programme affiche « Différent » :#include <cs50.h> #include <stdio.h> int main(void) { // Obtenir deux chaînes string s = get_string("s : "); string t = get_string("t : "); // Comparer les adresses des chaînes if (s == t) { printf("Identique\n"); } else { printf("Différent\n"); } }
- Compte tenu de ce que nous savons à présent sur les chaînes, cela est logique parce que chaque variable de « chaîne » pointe vers un emplacement différent en mémoire, où est stocké le premier caractère de chaque chaîne. Ainsi, même si les valeurs des chaînes sont identiques, l'affichage sera toujours « Différent ».
- Par exemple, notre première chaîne peut être à l'adresse 0x123, notre seconde à l'adresse 0x456, et
s
sera0x123
ett
sera0x456
, donc ces valeurs seront différentes. - Et
get_string
, pendant tout ce temps, n'a renvoyé qu'unchar *
, ou un pointeur vers le premier caractère d'une chaîne provenant de l'utilisateur.
-
Essayons à présent de copier une chaîne :
#include <cs50.h> #include <ctype.h> #include <stdio.h> int main(void) { string s = get_string("s : "); string t = s; t[0] = toupper(t[0]); // Afficher la chaîne deux fois printf("s : %s\n", s); printf("t : %s\n", t); }
- Nous obtenons une chaîne
s
, et copions la valeur des
danst
. Ensuite, nous mettons en majuscule la première lettre danst
. - Mais lorsque nous exécutons notre programme, nous constatons que
s
ett
sont à présent tous les deux en majuscules. - Dans la mesure où nous définissons
s
ett
avec les mêmes valeurs, ils sont en réalité des pointeurs vers le même caractère, et nous venons donc de mettre en majuscule le même caractère !
- Nous obtenons une chaîne
-
Pour effectuer une véritable copie d'une chaîne, nous devons fournir un effort un peu plus important :
#include <cs50.h> #include <ctype.h> #include <stdio.h> #include <string.h> int main(void) { char *s = get_string("s : "); char *t = malloc(strlen(s) + 1); for (int i = 0, n = strlen(s); i < n + 1; i++) { t[i] = s[i]; } t[0] = toupper(t[0]); printf("s : %s\n", s); printf("t : %s\n", t); }
- Nous créons une nouvelle variable,
t
, de typechar *
, avecchar *t
. À présent, nous souhaitons la faire pointer vers un nouveau bloc de mémoire suffisamment volumineux pour stocker la copie de la chaîne. Avecmalloc
, nous pouvons allouer un certain nombre d'octets en mémoire (qui ne sont pas déjà utilisés pour stocker d'autres valeurs), et nous faisons passer le nombre d'octets que nous souhaiterions. Nous connaissons déjà la longueur des
, nous ajoutons donc 1 pour le caractère null de terminaison. Ainsi, notre dernière ligne de code estchar *t = malloc(strlen(s) + 1);
. - Ensuite, nous copions chaque caractère, un à la fois, et à présent, nous pouvons mettre en majuscule uniquement la première lettre de
t
. Et nous utilisonsi < n + 1
, car nous souhaitons en réalité atteindren
, afin de garantir de copier le caractère de terminaison dans la chaîne. - Nous pouvons également utiliser la fonction de bibliothèque
strcpy
avecstrcpy(t, s)
à la place de notre boucle pour copier la chaînes
danst
. Pour être clair, le concept d'une « chaîne » vient du langage C et est bien pris en charge ; les seules roulettes d'entraînement de CS50 sont le typestring
à la place dechar *
et la fonctionget_string
.
- Nous créons une nouvelle variable,
-
Si nous ne copions pas le caractère null de terminaison,
\0
, et essayons d'imprimer notre chaînet
,printf
continuera et imprimera les valeurs inconnues, ou indésirables, que nous avons en mémoire, jusqu'à atteindre un\0
, ou s'arrêtera complètement, dans la mesure où notre programme pourrait se mettre à essayer de lire de la mémoire qui ne lui appartient pas !
valgrind
- Il s'avère qu'après avoir terminé avec la mémoire que nous avons allouée avec
malloc
, nous devrions appelerfree
(comme dansfree(t)
), ce qui indique à notre ordinateur que ces octets ne sont plus utiles à notre programme, permettant ainsi à ces octets en mémoire d'être réutilisés. - Si nous continuions à exécuter notre programme et à allouer de la mémoire avec
malloc
, mais que nous ne libérions jamais la mémoire après l'avoir utilisée, nous aurions une fuite de mémoire, ce qui ralentirait notre ordinateur et utiliserait de plus en plus de mémoire jusqu'à ce que notre ordinateur manque de mémoire. valgrind
est un outil en ligne de commande que nous pouvons utiliser pour exécuter notre programme et vérifier s'il présente des fuites de mémoire. Nous pouvons exécuter valgrind sur notre programme ci-dessus avechelp50 valgrind ./copy
et constater, à partir du message d'erreur, que la ligne 10 montre que nous avons alloué de la mémoire que nous n'avons jamais libérée (ou "perdue").- Ainsi, à la fin, nous pouvons ajouter une ligne
free(t)
, ce qui ne changera pas le fonctionnement de notre programme, mais ne générera plus d'erreurs avec valgrind. -
Regardons le fichier
memory.c
:// http://valgrind.org/docs/manual/quick-start.html#quick-start.prepare #include <stdlib.h> void f(void) { int *x = malloc(10 * sizeof(int)); x[10] = 0; } int main(void) { f(); return 0; }
- Il s'agit d'un exemple provenant de la documentation de valgrind (valgrind est un véritable outil, tandis que help50 a été spécialement écrit pour nous aider dans ce cours).
- La fonction
f
alloue suffisamment de mémoire pour 10 entiers et stocke l'adresse dans un pointeur appeléx
. Ensuite, nous essayons de définir la 11ème valeur dex
avecx[10]
à0
, ce qui dépasse le tableau de mémoire que nous avons alloué pour notre programme. Cela s'appelle un dépassement de tampon, où nous dépassons les limites de notre tampon, ou tableau, et accédons à une mémoire inconnue.
-
valgrind nous indiquera également qu'il y a une "Écriture invalide de taille 4" pour la ligne 8, où nous essayons en effet de modifier la valeur d'un entier (de taille 4 octets).
- Et pendant tout ce temps, la bibliothèque CS50 a libéré la mémoire qu'elle a allouée dans
get_string
, lorsque notre programme se termine !
Inversion
- Nous avons deux boissons colorées, violette et verte, chacune dans une tasse. Nous voulons intervertir les boissons entre les deux tasses, mais nous ne pouvons pas le faire sans une troisième tasse dans laquelle verser d'abord une des boissons.
-
Maintenant, disons que nous voulons intervertir les valeurs de deux entiers.
void swap(int a, int b) { int tmp = a; a = b; b = tmp; }
-
Avec une troisième variable à utiliser comme espace de stockage temporaire, nous pouvons le faire assez facilement, en mettant
a
danstmp
, puisb
dansa
, et enfin la valeur d'origine dea
, maintenant danstmp
, dansb
. -
Mais, si nous essayions d'utiliser cette fonction dans un programme, nous ne verrions aucun changement :
#include <stdio.h> void swap(int a, int b); int main(void) { int x = 1; int y = 2; printf("x est %i, y est %i\n", x, y); swap(x, y); printf("x est %i, y est %i\n", x, y); } void swap(int a, int b) { int tmp = a; a = b; b = tmp; }
-
Il s'avère que la fonction
swap
obtient ses propres variables,a
etb
lorsqu'elles sont passées, qui sont des copies dex
ety
, et donc changer ces valeurs ne change pasx
ety
dans la fonctionmain
.
Disposition en mémoire
- Dans la mémoire de notre ordinateur, les différents types de données qui doivent être stockés pour notre programme sont organisés dans différentes sections :
- La section de code machine est le code binaire de notre programme compilé. Lorsque nous exécutons notre programme, ce code est chargé dans la « partie supérieure » de la mémoire.
- Les globales sont des variables globales que nous déclarons dans notre programme ou d’autres variables partagées auxquelles tout notre programme peut accéder.
- La section heap est une zone vide dans laquelle
malloc
peut obtenir de la mémoire libre, pour que notre programme puisse l’utiliser. - La section stack est utilisée par des fonctions de notre programme lorsqu’elles sont appelées. Par exemple, notre fonction
main
est tout en bas de la pile et possède les variables localesx
ety
. Lorsque la fonctionswap
est appelée, elle possède son propre cadre ou tranche de mémoire qui se trouve au-dessus de la mémoire demain
, avec les variables localesa
,b
ettmp
:- Une fois que la fonction
swap
renvoie, la mémoire qu’elle utilisait est libérée pour l’appel de fonction suivant, et nous perdons tout ce que nous avons fait, mis à part les valeurs renvoyées, et notre programme revient à la fonction qui a appeléswap
. - Par conséquent, en transmettant les adresses de
x
ety
demain
àswap
, nous pouvons réellement modifier les valeurs dex
ety
:
- Une fois que la fonction
-
En transmettant l’adresse de
x
et dey
, notre fonctionswap
peut réellement fonctionner :#include <stdio.h> void swap(int *a, int *b) ; int main(void) { int x = 1 ; int y = 2 ; printf("x est %i, y est %i\n", x, y) ; swap(&x, &y) ; printf("x est %i, y est %i\n", x, y) ; } void swap(int *a, int *b) { int tmp = *a ; *a = *b ; *b = tmp ; }
- Les adresses de
x
et dey
sont transmises dansmain
àswap
, et nous utilisons la syntaxeint *a
pour déclarer que notre fonctionswap
prend des pointeurs. Nous enregistrons la valeur dex
danstmp
en suivant le pointeura
, puis nous prenons la valeur dey
en suivant le pointeurb
et nous la stockons dans l’emplacement vers lequela
pointe (x
). Enfin, nous stockons la valeur detmp
dans l’emplacement pointé parb
(y
), et nous avons terminé.
- Les adresses de
-
Si nous appelons
malloc
trop souvent, nous aurons un dépassement de segment, où nous finissons par dépasser notre segment. Ou bien, si nous avons trop de fonctions appelées, nous aurons un dépassement de pile, où notre pile a également trop de cadres de mémoire allouée. Et ces deux types de dépassement sont généralement appelés dépassements de tampon, après quoi notre programme (ou ordinateur entier) peut planter.
get_int
- Nous pouvons implémenter
get_int
nous-mêmes avec une fonction de la bibliothèque C,scanf
:
```
include
int main(void) { int x; printf("x : "); scanf("%i", &x); printf("x : %i\n", x); } ```
-
scanf
prend un format,%i
, donc l'entrée est « scannée » pour ce format, ainsi que l'adresse en mémoire où nous voulons que cette entrée aille. Maisscanf
ne vérifie pas beaucoup les erreurs, donc nous n'obtiendrons peut-être pas un entier. -
Nous pouvons essayer d'obtenir une chaîne de la même manière :
```
include
int main(void) { char *s = NULL; printf("s : "); scanf("%s", s); printf("s : %s\n", s); } ```
- Mais nous n'avons en fait alloué aucune mémoire pour
s
(s
estNULL
, ou ne pointe vers rien), donc nous pourrions vouloir appelerchar s[5]
pour allouer un tableau de 5 caractères pour notre chaîne. Ensuite,s
sera traité comme un pointeur dansscanf
etprintf
. - Maintenant, si l'utilisateur tape une chaîne d'une longueur de 4 ou moins, notre programme fonctionnera en toute sécurité. Mais si l'utilisateur tape une chaîne plus longue,
scanf
pourrait essayer d'écrire au-delà de la fin de notre tableau dans une mémoire inconnue, provoquant la soudaine perte de contrôle de notre programme.
Fichiers
- Avec la possibilité d'utiliser des pointeurs, nous pouvons également ouvrir des fichiers :
```
include
include
include
int main(void) { // Ouvrir le fichier FILE *file = fopen("phonebook.csv", "a");
// Obtenir des chaînes de l'utilisateur
char *name = get_string("Nom : ");
char *number = get_string("Numéro : ");
// Imprimer (écrire) des chaînes dans le fichier
fprintf(file, "%s,%s\n", name, number);
// Fermer le fichier
fclose(file);
} ```
fopen
est une nouvelle fonction que nous pouvons utiliser pour ouvrir un fichier. Elle retournera un pointeur vers un nouveau type,FILE
, à partir duquel nous pouvons lire et écrire. Le premier argument est le nom du fichier et le second argument est le mode dans lequel nous souhaitons ouvrir le fichier (r
pour lecture,w
pour écriture eta
pour ajout).- Après avoir obtenu des chaînes, nous pouvons utiliser
fprintf
pour imprimer dans un fichier. -
Finalement, nous fermons le fichier avec
fclose
. -
Nous pouvons désormais créer nos propres fichiers CSV (valeurs séparées par des virgules), comme de mini-tableurs, par programmation.
JPEG
-
Nous pouvons aussi écrire un programme qui ouvre un fichier et qui nous dit si c'est un fichier JPEG (une image) :
#include <stdio.h> int main(int argc, char *argv[]) { // Vérifie l'utilisation if (argc != 2) { return 1; } // Ouvre le fichier FILE *file = fopen(argv[1], "r"); if (!file) { return 1; } // Lit les trois premiers octets unsigned char bytes[3]; fread(bytes, 3, 1, file); // Vérifie les trois premiers octets if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff) { printf("Peut-être\n"); } else { printf("Non\n"); } // Ferme le fichier fclose(file); }
-
Maintenant, si on exécute ce programme avec
./jpeg brian.jpg
, notre programme va essayer d'ouvrir le fichier que nous spécifions (en vérifiant qu'on obtient un fichier non-NULL), et lit les trois premiers octets du fichier avecfread
. -
On peut comparer les trois premiers octets (en hexadécimal) aux trois octets requis pour commencer un fichier JPEG. S'ils sont identiques, alors notre fichier est probablement un fichier JPEG (bien que d'autres types de fichiers peuvent aussi commencer par ces octets). Mais si les octets ne sont pas les mêmes, alors on sait que ce n'est définitivement pas un fichier JPEG.
-
On peut utiliser ces capacités pour lire et écrire des fichiers, en particulier des images, et de les modifier en changeant les octets qu'ils contiennent, dans le problème de cette semaine !