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
net 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
ncommencent à 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 *ppour 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 à
pcomme « 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 spour un nom tel queEMMAet pouvoir accéder à chaque caractère avecs[0]et ainsi de suite :
![cases côte à côte, contenant : E étiqueté s[0], M étiqueté s[1], M étiqueté s[2], A étiqueté s[3], \0 étiqueté s[4]](https://cs50.harvard.edu/x/2020/notes/4/s_array.png)
- Mais il s’avère que chaque caractère est stocké en mémoire dans un octet avec une adresse donnée, et
sest en fait juste un pointeur avec l’adresse du premier caractère :

- Et puisque
sest juste un pointeur vers le début, seul le\0indique la fin de la chaîne. - En fait, la CS50 Library définit une
stringavectypedef 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 imprimerscomme sa valeur en tant que pointeur, comme0x42ab52. (printfsait aller à l’adresse et imprimer la chaîne entière lorsque nous utilisons%set transmettonss, même sispointe 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,0x42ab54et0x42ab55, 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
ssera0x123ettsera0x456, 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 desdanst. Ensuite, nous mettons en majuscule la première lettre danst. - Mais lorsque nous exécutons notre programme, nous constatons que
settsont à présent tous les deux en majuscules. - Dans la mesure où nous définissons
settavec 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
strcpyavecstrcpy(t, s)à la place de notre boucle pour copier la chaînesdanst. 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,printfcontinuera 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. valgrindest 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 ./copyet 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
falloue 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 dexavecx[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
adanstmp, puisbdansa, 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
swapobtient ses propres variables,aetblorsqu'elles sont passées, qui sont des copies dexety, et donc changer ces valeurs ne change pasxetydans 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
mallocpeut 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
mainest tout en bas de la pile et possède les variables localesxety. Lorsque la fonctionswapest 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,bettmp:
- Une fois que la fonction
swaprenvoie, 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
xetydemainàswap, nous pouvons réellement modifier les valeurs dexety:
- Une fois que la fonction
-
En transmettant l’adresse de
xet dey, notre fonctionswappeut 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
xet deysont transmises dansmainàswap, et nous utilisons la syntaxeint *apour déclarer que notre fonctionswapprend des pointeurs. Nous enregistrons la valeur dexdanstmpen suivant le pointeura, puis nous prenons la valeur deyen suivant le pointeurbet nous la stockons dans l’emplacement vers lequelapointe (x). Enfin, nous stockons la valeur detmpdans l’emplacement pointé parb(y), et nous avons terminé.
- Les adresses de
-
Si nous appelons
malloctrop 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_intnous-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); } ```
-
scanfprend 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. Maisscanfne 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(sestNULL, 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,ssera traité comme un pointeur dansscanfetprintf. - 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,
scanfpourrait 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);
} ```
fopenest 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 (rpour lecture,wpour écriture etapour ajout).- Après avoir obtenu des chaînes, nous pouvons utiliser
fprintfpour 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 !