Skip to content
Snippets Groups Projects

% Base II % Inspirés des slides de F. Glück % 25 septembre 2019

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.

Les pointeurs (1/N)

  • Un pointeur est une adresse mémoire.

    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.

    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.

Les pointeurs (3/N)

  • Permettent d'accéder à une valeur avec une indirection.

    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

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.

Les fonctions (1/N)

  • Les parties indépendantes d'un programme.

  • Permettent de modulariser et compartimenter le code.

  • Syntaxe:

    type identificateur(paramètres) { 
        // variables optionnelles
        instructions;
        // type expression == type
        return expression; 
    }

Les fonctions (2/N)

Exemple

int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

int main() {
    int c = max(4, 5);
}

Les fonctions (3/N)

  • Il existe un type void{.C}, "sans type", en C.

  • Il peut être utilisé pour signifier qu'une fonction ne retourne rien, ou qu'elle n'a pas d'arguments.

  • return{.C} utilisé pour sortir de la fonction.

  • Exemple:

    void show_text(void) { // second void optionnel
        printf("Aucun argument et pas de retour.\n");
        return; // optionnel
    }
    
    void show_text_again() { // c'est pareil
        printf("Aucun argument et pas de retour.\n");
    }

Les fonctions (4/N)

Prototypes de fonctions

  • Le prototype donne la signature de la fonction, avant qu'on connaisse son implémentation.

  • L'appel d'une fonction doit être fait après la déclaration du prototype.

    int max(int a, int b); // prototype
    
    int max(int a, int b) { // implémentation
        if (a > b) {
            return a;
        } else {
            return b;
        }
    }

Les fonctions (5/N)

Arguments de fonctions

  • Les arguments d'une fonction sont toujours passés par copie.

  • Les arguments d'une fonction ne peuvent jamais être modifiés.

    void set_to_two(int a) { // a: nouvelle variable
        // valeur de a est une copie de x
        // lorsque la fonction est appelée, ici -1
    
        a = 2; // la valeur de a est fixée à 2
    } // a est détruite
    
    int main() {
        int x = -1;
        set_to_two(x); // -1 est passé en argument
        // x vaudra toujours -1 ici
    }

Les fonctions (6/N)

Arguments de fonctions: pointeurs

  • Pour modifier un variable, il faut passer son adresse mémoire.
  • L'adresse d'une variable, x{.C}, est accédé par &x{.C}.
  • Un pointeur vers une variable entière a le type, int *x{.C}.
  • La syntaxe *x{.C} sert à déréférencer le pointeur (à accéder à la mémoire pointée).

Les fonctions (7/N)

Exemple

void set_to_two(int *a) { 
    // a contient une copie de l'adresse de la
    // variable passée en argument

    *a = 2; // on accède à la valeur pointée par a,
            // et on lui assigne 2
} // le pointeur est détruit, pas la valeur pointée
int main() {
    int x = -1;
    set_to_two(&x); // l'adresse de x est passée
    // x vaudra 2 ici
}

Quiz: Les fonctions

Quiz: Les fonctions

La fonction main() (1/N)

Généralités

  • Point d'entrée du programme.
  • Retourne le code d'erreur du programme:
    • 0: tout s'est bien passé.
    • Pas zéro: problème.
  • La valeur de retour peut être lue par le shell qui a exécuté le programme.
  • EXIT_SUCCESS{.C} et EXIT_FAILURE{.C} (de stdlib.h) sont des valeurs de retour portables de programmes C.

La fonction main() (2/N)

Exemple

int main() {
    // ...
    if (error)
	    return EXIT_FAILURE;
    else
	    return EXIT_SUCCESS;
}
  • Le code d'erreur est lu dans le shell avec $?{.bash}
$ ./prog
$ echo $?
0 # tout s'est bien passé par exemple
$ if [ $? -eq 0 ]; then echo "OK" ; else echo "ERROR"; fi
ERROR # si tout s'est mal passé

Les tableaux (1/N)

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/N)

Exemple

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

Les tableaux (3/N)

Itérer sur les éléments d'un tableau

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/N)

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.

    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/N)

Quels sont les bugs dans ce code?

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

Quels sont les bugs dans ce code?

#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;
}