Ressources Supplémentaires en Init Prog C

Les "Règles d'or"

Ou comment éviter de faire exploser des fusées ou des centrales nucléaires.
  • Règle n°0 : Une fonction ne fait qu'une et une seule chose.
  • Règle n°1 : Une variable = un type + une adresse + une valeur
  • Règle n°2 : type = zone mémoire + interprétation
  • 💡 Je suis une personne élégante, donc j'initialise toujours mes variables, comme ça je sais ce qu'il y a dedans. Si je ne sais pas avec quoi l'initialiser, c'est généralement mauvais signe ... (cf erreurs classique).

  • Règle n°3 : La durée de vie d'une variable = durée de vie du block où elle a été déclarée (c'est à dire les { } dans lesquels elle a été déclarée).
  • Règle n°4 - Masquage : une variable plus locale "masque" une variable plus globale de même nom.
  • 💡 Je suis une personne élégante, donc je nomme toujours mes variables avec des noms différents et qui ont un sens. Pour débuter, une bonne règle est "plus le nom est long, mieux c'est"
  • Règle n°5 : Il faut toujours toujours toujours tester si une référence ne vaut pas NULL avant de la déréférencer à l'aide de l'opérateur *.
  • 💡 Je suis une personne élégante, donc je nomme mes références avec un p ou un p_ devant mes variables.
    Par exemple :
    int a = 0; 
    int *p_a = &(a);
    💡 Je suis une personne élégante, donc si mon pointeur pointe vers NULL, j'affiche une erreur sur stderr et j'arrête immédiatement le code :
    if (p_a == NULL){
    	fprintf(stderr, "p_a est null !\n");
    	exit(EXIT_FAILURE);
    }
    
  • 🆕 Règle n°6 : On ne fait jamais jamais jamais d'opérations sur les structures elles-même, mais uniquement sur leurs champs. Si on veut comparer deux structures (par ex. pour vérifier si elles sont égales), on doit les comparer champ par champ.

  • Les erreurs "classiques"

    • J'affiche les éléments d'un tableau mais ça n'affiche pas les valeurs attendues.
    • Plus de détails
      • ⚠ Attention à la chaîne de formattage : si je veux afficher les éléments d'un tableau contenant que des entiers non-signés, il faut que j'affiche avec l'option "%u".
      • Par exemple : printf("tab[i] = %u", tab[i]);
    • Je veux mettre un tableau en argument d'une fonction.
    • Plus de détails Je veux mettre le tableau d'entier tab en argument de la fonction toto. J'ai écrit void toto(int *tab[]), mais ça me renvoie une erreur. Que se passe-t-il ?
      • Pour passer un tableau en argument d'une fonction, la syntaxe est type nom[]. Il n'y a pas besoin du *.
      • Dans le cas où je met le *, je crée un tableau de références vers int : ce n'est en général pas ce que l'on veut ici ...
    • J'utilise des pointeurs et la compilation me renvoit une Segmentation fault (ou segv pour les intimes)
    • Plus de détails
      • Avez-vous pensé à tester si les pointeurs valent NULL ? (cf Règle n°5)
    • J'ai testé si le pointeur était NULL, mais j'ai encore des erreurssegv.
    • Plus de détails
      • Quelques fois, le déréférencement peut être "caché" dans une autre structure.
      • Prenons l'exemple de la fonction suivante qui cherche si le caractère c est dans la chaîne de caractère chaineCaracteres :
        bool chercheCaractere(char *p_char, char c){
        	int i = 0; 
        	for (i=0; *(p_char+i) != '\0'; i = i+1)
        	{
        		if (p_char+i == NULL){ 
        			fprintf(stderr, "pointeur NULL !\n");
        			exit(EXIT_FAILURE);}
        		else{
        			if (*(p_char+i) == c) {
        				return true ;}
        		}
        	}
        	return false;
        }
        
        Il semble que nous ayions bien testé que p_char+i ne soit pas NULL à la 5ème ligne. Cependant, ce code produira malgré tout une erreur s'il est appelé sur une chaîne de caractères vide chercheCaractere(NULL, 'a').
      • ⚠ Il y a ici un déréférencement que nous n'avons pas testé dans la boucle for :
        *(p_char+i) != '\0'
      • Pour éviter toute erreur, on peut ajouter une condition à la boucle for :
      • ...
        for (i=0; p_char+i != NULL || *(p_char+i) != '\0'; i = i+1)
        ... 
        
      • On peut ensuite, après l'arrêt de la boucle, vérifier si p_char+i vaut NULL :
      • ...
        
        ... 
        
        bool chercheCaractere(char *p_char, char c){
        	int i = 0; 
        	for (i=0; p_char+i != NULL || *(p_char+i) != '\0'; i = i+1)
        	{
        		if (*(p_char+i) == c) {
        			return true ;}
        	}
        	if (p_char+i == NULL){ 
        		fprintf(stderr, "pointeur NULL !\n");
        		exit(EXIT_FAILURE);}
        	else return false;
        }
        

    * ou & ?

    Les opérations sur les références (ou pointeurs) sont parfois difficile à maîtriser lors du premier contact avec ces notions. Voici un petit flowchart qui vous indique quel symbole utiliser :

    🆕 Structures et pointeurs vers structure

    A quoi sert une structure ?

    Dérouler Imaginons que l'on souhaite constituer une bibliothèque. Chaque livre aura les mêmes caractéristiques : un titre, un auteur, une année de publication. Comment stocker ces informations ? Un tableau nous imposerai de tout stocker avec le même type (ici des str), mais c'est assez peu pratique quand on veut ensuite faire des opérations sur ces champs. La solution ? Les structures.

    Comment déclarer une nouvelles structure ?

    Dérouler Déclarer une structure, c'est déclarer un nouveau type, qui est ensuite utilisable comme tous les types que l'on connaît déjà (int, char, etc.). Par exemple :
    struct livre{
    	str titre; 
    	str auteur;
    	int annee;
    };
    
    ⚠ le point virgule après la déclaration de la structure est obligatoire.
    Que fait le code ci-dessus ? Il
    • déclare un nouveau type struct livre
    • qui possède trois champs : un champ titre, de type str, un champ auteur, de type str, et un champ année, de type int

    Comment créer une nouvelle variable de type structure ?

    Dérouler Si l'on reprend notre exemple précécent, et que l'on veut déclarer et initialiser deux livres, voici la marche à suivre :
    1. On définit le type struct livre selon les lignes précédentes en entête de programme (cf. 'Où placer la déclaration de la structure ?').
    2. On déclare une nouvelle variable du type désiré -- ici struct livre
      ⚠ le type est struct livre et pas livre tout seul. Pour voir comment éviter d'avoir à écrire le struct à chaque fois, cf typedef.
    3. struct livre nana ;
      
    4. On initialise chacun de ses champs un par un, grâce à l'opérateur . :
    5. nana.titre = "Nana" ;
      nana.titre = "Zola" ;
      nana.annee =  1880;
      

      Et voilà pour le premier !
    6. On fait de même pour toutes les variables de type struct livre que l'on souhaite instancier. Par exemple, pour Germinal de Zola, 1885 :
    7. struct livre germinal ;
      germinal.titre = "Germinal" ;
      germinal.titre = "Zola" ;
      germinal.annee =  1885;
      

    💡 Il est possible d'initialiser tous les champs en une seule ligne, avec la syntaxe suivante :
    struct livre germinal = {"Germinal", "Zola", 1885} ;
    

    Je déconseille cette pratique dans un premier temps puisqu'elle est plus susceptible de générer des erreurs (inversion de paramètres par exemple), et une éventuelle erreur pointera sur l'unique ligne, rendant le bug d'autant plus diffile à trouver qu'il y a de champs dans la structure (contrairement à l'approche 'un champ=une ligne', permettant de trouver l'erreur instantanément)

    Où placer la déclaration de la structure ?

    Dérouler Dans le main ? Dans une fonction ? Ni l'un ni l'autre ! La définition d'une structure est comme la définition d'un nouveau type, et on a donc interêt à ce que tous les éléments du code en bénéficie. Il doit donc se placer dans l'entête du code, après les #include et autres DEFINE.
    Le typedef doit se placer après la structure (logique), toujours dans l'entête du code.
    💡 Plus tard, quand nous aurons appris à compiler nous-mêmes, nous pourrons être élégants et mettre les définitions des structures dans un fichier dédié, permettant de partager le nouveau type à tous les fichiers d'un même programme !

    Marre de devoir taper struct machin à chaque fois ? typedef est là pour ça !

    Dérouler Arriver un moment, ça va bien de toujours taper struct devant le nom de notre structure, mais c'est un peu long pour pas grand chose ...
    On peut alors utiliser le mot clé typedef à la fin de l'entête du programme pour mettre des alias (des surnoms) aux types existants.
    On peut ainsi changer notre précédent type struct livre en un_livre avec le code suivant :
    struct livre{ // déja dans le code normalement
    	str titre; 
    	str auteur;
    	int annee;
    }; 
    typedef struct livre un_livre
    

    Notez l'abscence de point-virgule à la fin de la commande : typedef fonctionne comme DEFINE, les alias seront remplacé par leur valeur lors de la précompilation.
    💡 typedef fonctionne aussi pour les types qui existe déjà. Flemme de taper unsigned int en entier ?
    typedef unsigned int uint
    

    Et le tour est joué ! Maintenant, uint est strictement équivalent à unsigned int !

    Quelles opérations peut-on faire sur les structures ?

    Dérouler AUCUNE,AUCUNE,AUCUNE !
    Les opérations ne se font pas sur les structures, mais sur leur champs ! Ceci était un test, et vous venez d'échouer. Retournez en règle d'or n°6.

    Passage par valeur ou par adresse ?

    Développer Imaginons que l'on ait une structure s très volumineuse (quelques Go) de type struct struct_volumineuse, et que l'on veuille mettre à jout la valeur d'un champ (mettons, le champ toto) grâce à une fonction update_toto .
    Une première façon de faire est de passer la structure en argument et de la renvoyer dans la fonction :
    struct struct_volumineuse update_toto(struct struct_volumineuse s){
    	s.toto = s.toto + 1 ;
    	return s ;
    }
    
    int main(){
    	struct struct_volumineuse s ; 
    	s.toto = 0
    	s = update_toto(s);
    	return EXIT_SUCCESS;
    }
    
    L'écriture est relativement simple, mais le coût mémoire et temporel est très important : non seulement on crée une copie de la structure en la passant en argument par valeur (bon courage pour la RAM), mais en plus on doit à la fin du programme recopier toutes les valeurs de la structure interne à la fonction dans celle définie dans le main.
    La solution ? Le passage par référence.

    Passage par référence

    L'écriture avec un passage par référence est très similaire au code précédent, sauf qu'au lieu de passer en argument et de renvoyer une structure (ce qui est l'opération coûteuse), on passe en argument une référence vers la structure à modifier, que l'on modifie directement en mémoire. En reprenant l'exemple précédent :
    void update_toto(struct struct_volumineuse *p_s){
    	if (s == NULL){
    		fprinf("Pointeur nul") ; 
    		exit(EXIT_FAILURE)
    	}
    	*(p_s).toto = *(p_s).toto + 1 ;
    }
    
    int main(){
    	struct struct_volumineuse s ; 
    	s.toto = 0
    	update_toto(&s);
    	return EXIT_SUCESS;
    }
    
    On ainsi modifié le prototype de la fonction update_toto pour
    1. Qu'elle ne revoie rien : on travaille directement sur la structure en mémoire
    2. Qu'elle prenne en argument un pointeur vers la structure, de type struct struct_volumineuse *

    Opérateur ->

    Développer Il existe un opérateur équivalent au déréférencement puis à l'accès à un champ d'une structure. Par exemple, les deux lignes suivantes sont strictement équivalentes :
    *(p_s).toto = 1 ;
    p_s -> toto = 1 ;
    

    💡 A moins que l'inverse ne soit spécifié (ou nécessaire pour obtenir 100/100 sur VPL), la notation *(p_s).toto est recommandée car
    1. Elle utilise l'opérateur *(), pour lequel on a toujours le réflexe de tester si le pointeur passé en argument est différent de NULL (cf règle d'or n°5).
    2. Elle permet de suivre, à l'aide du schéma de la RAM sur une feuille, les différentes opérations effectuées.