Aller au contenu

[Cours complémentaires] Les bonnes pratiques en C


AlexMog
 Share

Recommended Posts

Bonjour à tous,

Dans ce cours un peu spécial, nous allons voir ensemble les bonnes pratiques à prendre en C, nous allons aussi voir dans quels cas certaines choses sont plus optimisées que d'autres.

 

Nous allons donc voir d'une manière générale la propreté du code, et l'optimisation de celui-ci en passant par des exemples d'erreures que tout le monde fait.

 

I- Propreté du code.

Pour un code lisible en C, il est impératif de bien construire son code. Les fichiers .h (headers) sont là pour les prototypes des fonctions, ainsi, le .c ne contiendra QUE le code des dites fonctions (et non pas des prototypes ou autres structures qui trainent dans des .c... BUARK!).

DECOUPEZ VOTRE CODE! Plus votre code sera découpé, et mieu il sera, plus vous ferez en sorte d'avoir un certain nombre de fonctions (en évitant les fonctions "poubelles" quand même...), plus votre code sera lisible et propre! Prensez donc bien à découper votre code!

Evtez les fonctions de 300 lignes! (ce qui rejoint le paragraphe précédent), effectivement, vous vous en sortez mieu en découpant le tout :).

Essayez de respecter la taille d'un terminal (hé oui, les programmeurs Linux programment souvent sur le terminal, il faut penser à eux!) qui est de 80 caractères. Ainsi, évitez de dépasser les 80 caractères par lignes de code ;).

Les includes se font EN HAUT du code, je ne veux plus jamais voir des includes en plein milieu d'un code source... BRRRRR!

Vous pouvez, en plus, respecter un certain format d'include, certains trient par ordre alphabétique, d'autres par longueur, le but étant de mettre tout de même les includes System AVANT les includes perso.

ex:

// Ca c'est bon
#include <stdio.h>
#include "mon_include.h"

// CA C'EST BOF!
#include "mon_include.h"
#include <stdio.h>

Evitez les includes inutiles! Cela ralentiera votre compilation si vous gardez des includes qui n'ont rien à faire là!

Pensez à sauter des lignes, éviter le multi-exécution sur une ligne... c'est pas très propre.

 

Bref, rendez votre code LISIBLE! Un code source, c'est comme un livre, si tout est en bordel, on a pas envie de le lire :).

 

II- Optimisations et bonnes pratiques!

Passons à la partie la plus intéressente! L'optimisation et les bonnes pratiques. D'une manière générale les deux sont proches.

 

Au niveau des optimisations, nous allons d'abord commencer par un cas très spécial:

Doit-on passer par copie ou pas? (ceci ne s'applique pas aux typpages de base, mais plutôt aux tableaux et aux structures).

La réponse est simple: TOUT DEPENDS DE VOTRE CODE!

Si vous souhaitez utiliser la variable que vous passez dans une fonction de manière locale à ladite fonction, passez la par copie.

Dans le cas contraire, si vous voulez modifier une structure ou les cases d'un tableau par exemple, NE LA PASSEZ PAS PAR COPIE! Laissez votre stack tranquile non de dieu!

Pour bien vous faire différencier ce qui est une copie et ce qui ne l'est pas, vouci des exemples concrets.

// Ceci est une copie (buark)
void mafonction(int tab[10]);

// Ceci n'est pas une copie
void mafonction(int *tab);
void mafonction(int tab[]);

// Ceci est une copie
void mafonction(struct mastruct s);

//Ceci n'est pas une copie
void mafonction(struct mastruct *s);

Nous avons aussi le cas de l'allocation, quand faut-il allouer de la mémoire? Déjà, il faut savoir que l'allocation dynamique est la chose la plus LOURDE dans le monde de la programmation.

Il est donc conseillé d'allouer tout l'espace naisséssaire au chargement de son programme, pour éviter d'allouer en plein milieu.

Si les allocations sont rares, on peut tout de même les utiliser de manière dynamique. Mais il ne faut surtout pas en abuser! (c'est pour cela qu'un tableau à une dimension (au niveau de l'alloc) sera moins lourd qu'un tableau à double dimensions).

 

Une autre pratique qui est plutôt bonne à connaitre pour l'optimisation: les calculs à base d'INT.

Pourquoi utiliser le typpage int plutôt que float? Il faut savoir que les int sont stockés sur un seul registre, alors qu'un float (chiffre à virgule) est stocké sur 4 registres (merci l'ASM de nous le démontrer), il est donc conseillé d'utiliser des int plutôt que des float pour optimiser les calculs. N'oubliez pas que le calcul sur 4 registres sera TOUJOURS plus long que le calcul sur 1 registre (4x plus long), et cela, peut importe la valeur stockée dans les registres.

 

Enfin, la meilleure pratique pour bien optimiser son programme, c'est savoir de quoi est composé le dit programme!

Je m'explique, utiliser des fonctions préfaites par la LibC, c'est bien, mais quand on ne sait pas comment elles fonctionnent, c'est MAL!

En effet, vous êtes plusieurs à ne pas savoir que dans certains cas, printf n'est pas du tout optimisé (en effet, printf passe par 3 boucles ET une bufferisation des données AVANT affichage, alors qu'un simple puts ou write aurait fait l'affaire!), je vous conseille donc de vous renseigner sur les fonctions que vous utilisez, ou encore mieu! RECODEZ LES! Vous apprendrez beaucoup plus ainsi que de n'importe qu'elle autre manière!

Je vous invites à lire mes cours dans la section C, qui utilisent le principe de l'utilisation de Fonctions System au lieu de passer par des fonctions de la LibC, pour vous apprendre à bien comprendre la différence entre les deux, et avoir de bonnes habitudes (qui sait, vous comprendrez peut être comment ça marche :))

 

Si vous désirez d'autres informations, n'hésitez pas à laisser un commentaire ci-dessous!

 

A très bientôt pour un prochain cours!

AlexMog.

  • Upvote 3
Lien vers le commentaire
Partager sur d’autres sites

Merci :) .
Cependant pourrais tu entrer plus dans les détails sur plusieurs points?
 
-Les registres sont contenus dans la mémoire la plus rapide de l'ordinateur, non? Étant donné que le type "char" est stocké sur moins d'octets, non par rapport à un type "int" (sous Windows: int==> 4 octets // char ==> 1 octet), un "char" est aussi contenu dans 1 registre?
 
-Maintenant en rapport avec les tableaux, quel sont les risques de le passer par copie lorsque l'on compte modifier ses valeurs? tu parles souvent de stack, tu fais référence à Piles? Pour aller plus loin toutes les structures de données sont modifiés sur le principe des piles?
 
-Enfin par rapport aux fonctions, pour les recoder, il faut se diriger vers quel type de fonctions? Avant faut bien sur aller voir comment elles sont codés, non? Elles sont situées dans les fichiers d'Ide "code-blocks"?
Lien vers le commentaire
Partager sur d’autres sites

Merci :) .

Cependant pourrais tu entrer plus dans les détails sur plusieurs points?

 

-Les registres sont contenus dans la mémoire la plus rapide de l'ordinateur, non? Étant donné que le type "char" est stocké sur moins d'octets, non par rapport à un type "int" (sous Windows: int==> 4 octets // char ==> 1 octet), un "char" est aussi contenu dans 1 registre?

Les registres sont contenus dans la mémoire du processeur (sisi, vous savez, la mémoire cache ;)), on parle de registres en ASM uniquement. Ils ne sont pas accessibles en C, c'est la base des calculs du processeur.

 

-Maintenant en rapport avec les tableaux, quel sont les risques de le passer par copie lorsque l'on compte modifier ses valeurs? tu parles souvent de stack, tu fais référence à Piles? Pour aller plus loin toutes les structures de données sont modifiés sur le principe des piles?

Je l'avais déjà expliqué plus tôt (voir mon cours sur les stacks), lorsque tu lance une fonction, celle-ci sauvegarde l'ancienne stack (et la récupère à la fin de la fonction), du coup, tu as un push sur la stack de l'ancienne stack, si tu y ajoute ce que tu as passé par copie, tu repush ce qui a déjà été push en stack. POur les structures, tout est dans mon cours.

-Enfin par rapport aux fonctions, pour les recoder, il faut se diriger vers quel type de fonctions? Avant faut bien sur aller voir comment elles sont codés, non? Elles sont situées dans les fichiers d'Ide "code-blocks"?

Alors non, pas du tout, et c'est là que linux est concret pour apprendre à programmer!

Le principe de l'openSource de linux te permet de savoir beaucoup de choses sur les fonctions que tu utilises, mais là n'est pas la question.

Pour recoder une fonction de la libC, il te faut uniquement les fonctions qu'à utilisée la libC pour être crée: les fonctions system. Une fois que tu les as, surtout, ne regarde pas les sources existantes! Ca ne sert à rien, autant essayer de réfléchir par toi même pour comprendre comment ça marche! Une fois que tu as tenté de recoder toi même, vas voir les sources, et pleure un bon coup, en voyant que printf, c'est assez lourd quand même :).

Code blocks ne t'aidera pas, car les libs ne sont pas open-source sur windows (en tout cas, les libC)

  • Upvote 2
Lien vers le commentaire
Partager sur d’autres sites

"les libs ne sont pas open-source sur windows (en tout cas, les libC)"

La libc et la STL sont open source sous Windows. Si tu as Visual Studio d’installé, les sources sont dans le dossier "\Microsoft VisualStudio XX.x\VC\crt\src\" et les headers : "\Microsoft VisualStudio XX.x\include\".

 

"lorsque tu lance une fonction, celle-ci sauvegarde l'ancienne stack (et la récupère à la fin de la fonction)"  

 

On dirait que tu confond avec les stackframes, il n'y a qu'une seule stack par thread, et sur cette stack il y a les stackframes de chaque fonction en cours d’exécution. 

Lien vers le commentaire
Partager sur d’autres sites

La libc et la STL sont open source sous Windows. Si tu as Visual Studio d’installé, les sources sont dans le dossier "\Microsoft VisualStudio XX.x\VC\crt\src\" et les headers : "\Microsoft VisualStudio XX.x\include\".

 

 

On dirait que tu confond avec les stackframes, il n'y a qu'une seule stack par thread, et sur cette stack il y a les stackframes de chaque fonction en cours d’exécution. 

Autant pour moi pour les sources windows, et merci pour l'info :)

 

Pour le stackframes, sache que la stack est sauvegardée AVANT d'initialiser le stackframe d'une fonciton. (on le voit dans le code ASM des fonctions en C), voici un code ASM d'exemple:

;; exemple avec la fonction "hello world" x86 asm
global main
extern printf

section .text
main:
  push rbp ;; Ici, le generic push
  mov rbp, rsp ;; Suite du generic push (on le retrouvera dans toutes les fonctions en C (sans exceptions, il est ajouté automatiquement par le compilateur)

  mov rdi, FormatStr ;; 1er param de printf
  call printf ;; call de printf

  mov rsp, rbp ;; on remet l'ancienne stack en place
  pop rbp ;; on vire la sauvegarde de l'ancienne stack

  mov rax, 60 ;; on prépare l'appel au systcall "exit"
  xor rdi, rdi ;; on passe le paramètre 0 dans rdi (xor x, x revient à mettre 0 dans x (car un xor de 2 même valeurs donne 0)
  syscall ;; appel du systcall n°60
  ret

;; section read only
section .rodata
  FormatStr db 'Hello World !',0Ah,0

(c'est ce que j'appelle dans mon code le "generic push", on voit très bien que l'ancienne stack est sauvegardée dans la mémoire exécutive)

Lien vers le commentaire
Partager sur d’autres sites

"Pour le stackframes, sache que la stack est sauvegardée AVANT d'initialiser le stackframe d'une fonciton. (on le voit dans le code ASM des fonctions en C)"

 

 C'est l'addresse de l'ancienne stackframe qui est sauvegardée, ce qui crée une linked list. (chaque base pointer (rbp/ebp) pointe vers celui de la stackframe précédente)

;; exemple avec la fonction "hello world" x86 asm
global main
extern printf

section .text
main:
  push rbp ;; Sauvegarde de l'ancien base pointer
  mov rbp, rsp ;; Change le base pointer vers la stackframe de main

  mov rdi, FormatStr ;; 1er param de printf
  call printf ;; call de printf 
;; printf est en cdecl donc faudrait enlever l'argument de la stack ici
;; add  rsp, 4/8 (32 ou 64 bit) vu que tu as passer un pointeur

  mov rsp, rbp ;; Remet le stack pointer de la fonction précédente
  pop rbp ;; Remet le base pointer de la fonction précédente

  mov rax, 60
  xor rdi, rdi
  syscall 
  ret

;; section read only
section .rodata
  FormatStr db 'Hello World !',0Ah,0
Lien vers le commentaire
Partager sur d’autres sites

 

 C'est l'addresse de l'ancienne stackframe qui est sauvegardée, ce qui crée une linked list. (chaque base pointer (rbp/ebp) pointe vers celui de la stackframe précédente)

;; exemple avec la fonction "hello world" x86 asm
global main
extern printf

section .text
main:
  push rbp ;; Sauvegarde de l'ancien base pointer
  mov rbp, rsp ;; Change le base pointer vers la stackframe de main

  mov rdi, FormatStr ;; 1er param de printf
  call printf ;; call de printf 
;; printf est en cdecl donc faudrait enlever l'argument de la stack ici
;; add  rsp, 4/8 (32 ou 64 bit) vu que tu as passer un pointeur

  mov rsp, rbp ;; Remet le stack pointer de la fonction précédente
  pop rbp ;; Remet le base pointer de la fonction précédente

  mov rax, 60
  xor rdi, rdi
  syscall 
  ret

;; section read only
section .rodata
  FormatStr db 'Hello World !',0Ah,0

Autant pour moi, je me suis mal exprimé.

Par contre, ca ne change pas le problème de duplication des données.

  • Upvote 1
Lien vers le commentaire
Partager sur d’autres sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Invité
Répondre à ce sujet…

×   Vous avez collé du contenu avec mise en forme.   Supprimer la mise en forme

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Chargement
 Share

×
×
  • Créer...