--- title: Base III date: 2 octobre 2019 --- # Les tableaux (1/6) ## Généralités - `C` offre uniquement des tableaux statiques - Un tableau est un "bloc" de mémoire contiguë associé à un nom - taille fixe déterminée à la déclaration du tableau - la taille ne peut pas être changée. - Pas d’assignation de tableaux. - Un tableau déclaré dans une fonction ou un bloc est détruit à la sortie de celle/celui-ci - $\Rightarrow$ Un tableau local à une fonction ne doit **jamais être retourné** (aussi valable pour toute variable locale)! - Les éléments d’un tableau sont accédés avec `[i]`{.C} où `i`{.C} est l’index de l’élément. - Le premier élément du tableau à l’index `0`{.C}! - Lorsqu’un tableau est déclaré, la taille de celui-ci doit toujours être spécifiée, sauf s’il est initialisé lors de sa déclaration. # Les tableaux (2/6) ## Exemple ```C float tab1[5]; // tableau de floats à 5 éléments // ses valeurs sont indéfinies int tab2[] = {1, 2, 3}; // tableau de 3 entiers, // taille inférée int val = tab2[1]; // val vaut 2 à présent int w = tab1[5]; // index hors des limites du tableau // comportement indéfini! // pas d'erreur du compilateur ``` <!-- TODO QUIZ: ```C int a1[5]; // OK int a2[] = { 1, 2, 3 }; // OK int a3[4][5]; // OK int [] a4; // Erreur int a5[]; // Erreur int[] function(void) { // Erreur int array[5]; // OK return array; // Erreur } void foo(int a[]) { // OK a[3] = 0; // OK } void bar(void) { int a[5]; // OK foo(a); // OK a = a5; // Erreur } ``` --> <!-- ```C #include <stdio.h> int main(void) { char i; char a1[] = { 100,200,300,400,500 }; char a2[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; a2[10] = 42; for (i = 0; i < 5; i++) { printf("a1[%d] = %d\n", i, a1[i]); } return 0; } ``` --> # Les tableaux (3/6) ## Itérer sur les éléments d'un tableau ```C int x[10]; for (int i = 0; i < 10; ++i) { x[i] = 0; } int j = 0; while (j < 10) { x[j] = 1; j += 1; } int j = 0; do { x[j] = -1; j += 1; } while (j < 9) ``` # Les tableaux (4/6) ## Les tableaux comme argument - Un tableau est le pointeur vers sa première case. - Pas moyen de connaître sa taille: `sizeof()`{.C} inutile. - Toujours spécifier la taille d'un tableau passé en argument. ```C void foo(int tab[]) { // sans taille... for (int i = 0; i < ?; ++i) { // on sait pas quoi mettre pour ? printf("tab[%d] = %d\n", i, tab[i]); } } // n doit venir avant tab, [n] optionnel void bar(int n, int tab[n]) { for (int i = 0; i < n; ++i) { printf("tab[%d] = %d\n", i, tab[i]); } } ``` # Les tableaux (5/6) ## Quels sont les bugs dans ce code? ```C #include <stdio.h> int main(void) { char i; char a1[] = { 100,200,300,400,500 }; char a2[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; a2[10] = 42; for (i = 0; i < 5; i++) { printf("a1[%d] = %d\n", i, a1[i]); } return 0; } ``` # Les tableaux (6/6) ## Quels sont les bugs dans ce code? ```C #include <stdio.h> int main(void) { char i; // 200, .., 500 char overflow char a1[] = { 100,200,300,400,500 }; char a2[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; a2[10] = 42; // [10] out of bounds for (i = 0; i < 5; i++) { printf("a1[%d] = %d\n", i, a1[i]); } return 0; } ``` <!-- TODO quiz: --> <!-- que retourne sizeof(tab[]) --> # Représentation des variables en mémoire (1/N) ## La mémoire * La mémoire est: - ... un ensemble de bits, - ... accessible via des adresses, +------+----------+----------+------+----------+------+------+ | bits | 00110101 | 10010000 | .... | 00110011 | .... | .... | +======+==========+==========+======+==========+======+======+ | addr | 2000 | 2001 | .... | 4000 | .... | .... | +------+----------+----------+------+----------+------+------+ - ... gérée par le système d'exploitation. - ... séparée en deux parties: **la pile** et **le tas**. ## Une variable * Une variable, `type a = valeur`{.C}, possède: - un type (`char`{.C}, `int`{.C}, ...), - un contenu (une séquence de bits qui encode `valeur`{.C}), - une adresse mémoire (accessible via `&a`{.C}), - une portée. # Représentation des variables en mémoire (2/N) {width=100%} # Les pointeurs (1/N) - Un pointeur est une adresse mémoire. ```C type *id; ``` - Pour interpréter le contenu de ce qu'il pointe, il doit être typé. - Un pointeur n'est rien d'autre qu'un entier (64bit sur x86-64, soit 8 octets). - Un pointeur peut être **déréférencé**: on accède à la valeur située à l'adresse mémoire sur laquelle il pointe. ```C char *c; // déclaration pointeur de char *c = 'a'; // assign. 'a' à valeur pointée par c c = 1000; // on modifie l'adresse pointée par c char d = *c; // on lit la valeur pointée par c. UB! ``` - `NULL`{.C} (ou `0`{.C}) est la seule adresse **toujours** invalide. # Les pointeurs (2/N) {width=100%} # Les pointeurs (3/N) - Permettent d'accéder à une valeur avec une indirection. ```C int a = 2; int *b = &a; *b = 7; // on met 7 dans la case pointée par b // ici a == 7 aussi a = -2; // ici *b == -2 aussi ``` - Permettent d'avoir plusieurs chemins d'accès à une valeur. - Lire **et** écrire en même temps dans un bout de mémoire devient possible: **danger**. # Quiz: Les pointeurs ## [Quiz: Les pointeurs](https://cyberlearn.hes-so.ch/mod/evoting/view.php?id=1038526) # La fonction `sizeof()` (1/N) - La fonction `sizeof()`{.C} permet de connaître la taille en octets: - d'une valeur, - d'un type, - d'une variable. - Soit `int a = 2`{.C}, sur l'architecture x86_64 que vaut: - `sizeof(a)`{.C}? - `sizeof(&a)`{.C}? - Soit `char b = 2`{.C}, - `sizeof(b)`{.C}? - `sizeof(&b)`{.C}? # La fonction `sizeof()` (2/N) - Réponses: - `sizeof(a) == 4`{.C}, `int`{.C} entier 32 bits. - `sizeof(&a) == 8`{.C}, une adresse est de 64 bits. - `sizeof(b) == 1`{.C}, `char`{.C} entier 8 bits. - `sizeof(&b) == 8`{.C}, une adresse est de 64 bits. # Types complexes: `struct`{.C} (1/N) ## Généralités - Plusieurs variables qu'on aimerait regrouper dans un seul type: `struct`{.C}. ```C struct complex { // déclaration double re; double im; }; struct complex num; // déclaration de num ``` - Les champs sont accessible avec le sélecteur "`.`{.C}". ```C num.re = 1.0; num.im = -2.0; ``` # Types complexes: `struct`{.C} (2/N) ## Simplifications - `typedef`{.C} permet de définir un nouveau type. ```C typedef unsinged int uint; typedef struct complex complex_t; typedef struct complex { double re, im; } complex_t; ``` - L'initialisation peut aussi se faire avec ```C complex_t num = {1.0, -2.0}; // re = 1.0, im = -2.0 complex_t num = {.im = 1.0, .re = -2.0}; complex_t num = {.im = 1.0}; // argl! .re non initialisé complex_t num2 = num; // copie ``` # Types complexes: `struct`{.C} (3/N) ## Pointeurs - Comme pour tout type, on peut avoir des pointeurs vers un `struct`{.C}. - Les champs sont accessible avec le sélecteur `->`{.C} ```C complex_t *num; // on crée un pointeur num->re = 1.0; // seg fault... num->im = -1.0; // mémoire pas allouée. ``` {width=100%} # Allocation dynamique de mémoire (1/N) - La fonction `malloc`{.C} permet d'allouer dynamiquement (pendant l'exécution du programme) une zone de mémoire contiguë. ```C #include <stdlib.h> void *malloc(size_t size); ``` - `size`{.C} est la taille de la zone mémoire **en octets**. - Retourne un pointeur sur la zone mémoire ou `NULL`{.C} en cas d'échec: **toujours vérifier** que la valeur retournée est `!= NULL`{.C}. # Allocation dynamique de mémoire (2/N) - Avec l'exemple de tout à l'heure: ```C complex_t *num = malloc(sizeof(complex_t)); num->re = 1.0; // maintenant ... num->im = -1.0; // ça marche. ``` - La zone mémoire **n'est pas** initialisée. - La mémoire doit être désallouée explicitement $\Rightarrow$ **fuites mémoires**. <!-- - Toujours garder un pointeur sur la mémoire allouée sinon **pointeur pendouillant**. --> {width=100%} # Allocation dynamique de mémoire (3/N) - La fonction `free()`{.C} permet de libérer une zone préalablement allouée avec `malloc()`{.C}. ```C #include <stdlib.h> void free(void *ptr); ``` - Pour chaque `malloc()`{.C} doit correspondre exactement un `free()`{.C}. - Si la mémoire n'est pas libérée: **fuite mémoire** (l'ordinateur plante quand il y a plus de mémoire). - Si la mémoire est **libérée deux** fois: seg fault. - Pour éviter les mauvaises surprises mettre `ptr`{.C} à `NULL`{.C}. # Allocation dynamique de mémoire (4/N) ## Tableaux dynamiques - Pour allouer un espace mémoire de 50 entiers: ```C int *p = malloc(50 * sizeof(int)); ``` - Cette espace peut alors être utilisé comme un tableau de 50 entiers: ```C for (int i = 0; i < 50; ++i) { p[i] = 0; } ``` ## Arithmétique de pointeurs - On peut parcourir la mémoire différemment qu'avec l'indexation ```C int *p = malloc(50 * sizeof(int)); // initialize somehow double a = p[7]; double b = *(p + 7); // on avance de 7 "double" p[0] == *p; // le pointeur est le premier élément ``` # Allocation dynamique de mémoire (5/N) ## Arithmétique de pointeurs {width=100%} # Allocation dynamique de mémoire (6/N) ## Pointeur de pointeur - Tout comme une valeur a une adresse, un pointeur a lui-même une adresse: ```C int a = 2; int *b = &a; int **c = &b; ``` - Chaque `*`{.C} ou `&`{.C} rajoute une indirection. # Allocation dynamique de mémoire (7/N) ## Pointeur de pointeur {height=100%} # Allocation dynamique de mémoire (8/N) - Avec `malloc()`, on peut allouer dynamiquement des tableaux de pointeurs: ```C int **p = malloc(50 * sizeof(int*)); for (int i = 0; i < 50; ++i) { p[i] = malloc(70 * sizeof(int)); } int a = p[5][8]; // on indexe dans chaque dimension ``` - Ceci est une matrice (un tableau de tableau). # Prototypes de fonctions (1/N) ## Principes généraux de programmation - Beaucoup de fonctionnalités dans un code $\Rightarrow$ Modularisation. - Modularisation du code $\Rightarrow$ écriture de fonctions. - Beaucoup de fonctions $\Rightarrow$ regrouper les fonctions dans des fichiers séparés. ## Mais pourquoi? - Lisibilité. - Raisonnement sur le code. - Débogage. ## Exemple - Libraire `stdio.h`: `printf()`{.C}, `scanf()`{.C}, ... # Prototypes de fonctions (2/N) - Prototypes de fonctions nécessaires quand: 1. Utilisation de fonctions dans des fichiers séparés. 2. Utilisation de librairies. - Un prototype indique au compilateur la signature d'une fonction. - On met les prototypes des fonctions **publiques** dans des fichiers *headers*, extension `.h`. - Les *implémentations* des fonctions vont dans des fichier `.c`. # Prototypes de fonctions (3/N) ## Fichier header - Porte l'extension `.h` - Contient: - définitions des types - prototypes de fonctions - macros - directives préprocesseur (cf. plus loin) - Utilisé pour décrire **l'interface** d'une librairie ou d'un module. - Un fichier `C` (extension `.c`) utilise un header en *l'important* avec la directive `#include`{.C}: ```C #include <stdio.h> // libraire dans LD_LIBRARY_PATH #include "chemin/du/prototypes.h"// chemin explicite ``` # Génération d'un exécutable (1/N) ## Un seul fichier source {width=100%} # Génération d'un exécutable (2/N) ## Un seul fichier source ```bash gcc proc.c -o prog ``` 1. **Précompilation: ** `gcc` appelle `cpp`, le préprocesseur qui effectue de la substitution de texte (`#define`, `#include`, macros, ...) et génère le code `C` à compiler, portant l'extension `.i` (`prog.i`). 2. **Compilation assembleur: ** `gcc` compile le code C en code assembleur, portant l'extension `.s` (`prog.s`). 3. **Compilation code objet: ** `gcc` appelle `as`, l'assembleur, qui compile le code assembleur en code machine (code objet) portant l'extension `.o` (`prog.o`). 4. **Édition des liens: ** `gcc` appelle `ld`, l'éditeur de liens, qui lie le code objet avec les librairies et d'autres codes objet pour produire l'exécutable final (`prog`). Les différents codes intermédiaires sont effacés. # Génération d'un exécutable (3/N) ## Plusieurs fichiers sources {width=100%} # Génération d'un exécutable (4/N) ::: Main ## `main.c` ```C #include <stdio.h> #include "sum.h" int main() { int tab[] = {1, 2, 3, 4}; printf("sum: %d\n", sum(tab, 4)); return 0; } ``` ::: :::::::::::::: {.columns} ::: {.column width="45%"} ## `sum.h` ```C #ifndef _SUM_H_ #define _SUM_H_ int sum(int tab[], int n); #endif ``` ::: ::: {.column width="55%"} ## `sum.c` ```C #include "sum.h" int sum(int tab[], int n) { int s = 0; for (int i = 0; i < n; i++) { s += tab[i]; } return s; } ``` ::: :::::::::::::: # Génération d'un exécutable (4/N) La compilation séparée se fait en plusieurs étapes. ## Compilation séparée 1. Générer séparément les fichiers `.o` avec l'option `-c`. 2. Éditer les liens avec l'option `-o` pour générer l'exécutable. ## Exemple - Création des fichiers objets, `main.o` et `sum.o` ```bash $ gcc -Wall -Wextra -std=c11 -c main.c $ gcc -Wall -Wextra -std=c11 -c sum.c ``` - Édition des liens ```bash $ gcc main.o sum.o -o prog ``` # Préprocesseur (1/N) ## Généralités - Première étape de la chaîne de compilation. - Géré automatiquement par `gcc` ou `clang`. - Lit et interprète certaines directives: 1. Les commentaires (`//`{.C} et `/* ... */`{.C}). 2. Les commandes commençant par `#`{.C}. - Le préprocesseur ne compile rien, mais subtitue uniquement du texte. ## La directive `define`{.C} - Permet de définir un symbole: ```C #define PI 3.14159 #define _SUM_H_ ``` - Permet de définir une macro. ```C #define NOM_MACRO(arg1, arg2, ...) [code] ``` # Préprocesseur (2/N) ## La directive `include`{.C} - Permet d'inclure un fichier. - Le contenu du fichier est ajouté à l'endroit du `#include`{.C}. - Inclusion de fichiers "globaux" ou "locaux" ```C #include <file.h> // LD_LIBRARY_PATH #include "other_file.h" // local path ``` - Les inclusions multiples peuvent poser problème: définitions multiples. Les headers commencent par: ```C #ifndef _VAR_ #define _VAR_ /* commentaires */ #endif ```