Skip to content
Snippets Groups Projects
base_X.md 15.1 KiB
Newer Older
---
title: Base III
orestis.malaspin's avatar
orestis.malaspin committed
date: 2019-10-02

# 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)

![Les variables en mémoire.](figs/memory.svg){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)

![Les pointeurs, le déréférencement, et la mémoire.](figs/memory_deref.svg){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.
    ```

![La représentation mémoire de `complex_t`.](figs/pointer_struct.svg){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**. -->

![La représentation mémoire de `complex_t` et fuites.](figs/pointer_struct_ok.svg){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

![L'arithmétique des pointeurs.](figs/pointer_arithmetics.svg){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


![L'arithmétique des pointeurs.](figs/double_pointeur.svg){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

![Étapes de génération.](figs/compilation.svg){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

![Étapes de génération, plusieurs fichiers.](figs/compilation_plusieurs.svg){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
    ```