Tout ce que j'aurais aimé qu'on me dise la première fois que j'ai appris le C
2021-01-20T21:07:51Z
Je code comme certains colorient un carreau sur 2, gribouillents des petits dessins rigolos ou font des arabesques pour s'occuper pendant les réunions. Je ne pense pas être bon à ça, mais j'aime beaucoup m'occuper l'esprit en écrivant du C. C'est un peu comme jouer à résoudre une énigme en s'amusant avec les règles du jeu.
J'ai appris le C il y a longtemps, en lisant le tutoriel du "site du zero" (j'ai dit, c'était il y a longtemps). Je m'y suis remis récemment, et avec du recul, sans être un expert, je m'aperçois que j'aurais bien aimé savoir certaines choses à l'époque. Voici donc tout ce que j'aurais aimé lire la première fois que j'ai appris le C.
Macros utiles
Quand on déclare une chaîne de caractères, on ne sait jamais trop quelle doit être sa longueur. Il existe en fait des limites déjà bien définies pour nous :
- PATH_MAX : La taille maximale d'un chemin vers un fichier (#include limits.h)
- BUFSIZ : Taille d'un buffer, souvent utiliser pour traiter du texte. (#include stdio.h)
Lecture
Aller regarder dans les fichiers /usr/include peut parfois aider à mieux comprendre. Mais par dessus tout, il faut lire les pages man. J'ignore si c'est spécifique à OpenBSD, mais les sections (3) regorgent d'explication, d'exemples, et de suggestions de fonctions associées dont on ignore peut-être l'existence. Par exemple, man strncat propose de lire des infos sur strlcpy pour la compléter/remplacer. Cette dernière nous montre carrément des exemples.
http://man.openbsd.org/strncat
http://man.openbsd.org/strlcpy
ternaire
Bon, c'est pas original, mais je n'avais jamais vraiment compris avant:
(condition) ? expression_si_vrai : expression_si_faux ;
Les chaînes de caractères
En C, il y a le type "char". Les chaînes de caractères ne sont pas un type. En C, il n'y a que des tableaux de "char", terminés par un NUL : '\0'. C'est idiot, mais en fait très important à garder en tête pour travailler sur du texte. La confusion est facile car on mélange les expressions avec des " ou des '. En bref :
- 'a' : c'est le char a
- "a" : c'est la "chaîne" constituée de 'a' puis '\0'.
On ne peut pas "additionner" ou modifier un tableau de char aussi "facilement" que dans d'autres langages. Pourtant, dans string.h, on peut trouver tout ce qu'il faut pour gérer les chaînes de caractères.
Il vaut mieux se garder une bibliothèque personnelle de fonctions gérant les chaînes, et les réemployer au besoin.
Voir le fichier "C" avec quelques exemples.
Si une manipulation sur des chaînes de caractères nécessite d'allouer de la mémoire avec malloc, alors il existe certainement une solution plus simple. Ce n'est pas toujours vrai ceci dit ^^.
Utiliser la fonction strdup() permet de copier une chaîne de caractères. Il faut penser à appeler "free()" ensuite avec cette fonction.
La fonction strsep() modifie la chaîne qui lui est donnée. Il faut donc parfois utiliser strdup() avant, car il faut donner une chaîne modifiable et non constante.
Autrement dit:
char *str = "coucou"; /* n'est pas modifiable */ char *str = strdup("coucou"); /* modifiable */
strchr() et strstr() sont très utiles pour récupérer l'emplacement d'un caractère ou d'une chaîne de caractères. On peut directement intervenir à partir de ce point ensuite.
Par exemple
char *pos = NULL; char *str = "Respirer de la compote fait tousser"; pos = strstr(str, "compote"); puts(str); /* affiche "compote fait tousser /*
Toujours initialiser une chaîne avec des 0 : ainsi, on est sûr qu'elle est toujours terminée correctement (par un '\0').
char str[BUFSIZ] = {'\0'}; char *str2 = calloc(BUFSIZ, sizeof(char));
Pour "vider" (remettre à zéro) une chaine de caractère (reset) :
bzero(s, sizeof(s));
Fichiers
Il vaut mieux utilier les fonctions fread()/fwrite() plutôt que read()/write() car elles sont plus rapides (utilisation d'un cache).
size_t nread = 0; FILE *fd = NULL; if ((fd = fopen(fp, "r")) == NULL) { goto err; } while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0) fwrite(buffer, 1, nread, stdout); fclose(fd); if (ferror(fd)) { err(1,"closefile"); }
Pour lister le contenu répertoire: scandir()
#include <sys/types.h> #include <dirent.h> int n = 0; struct dirent **namelist; if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) { err(1, "Can't scan %s", path); } else { for(int j = 0; j < n; j++) { if (!strcmp(namelist[j]->d_name, ".")) { continue; } if (namelist[j]->d_type == DT_DIR) { printf("%s\n", namelist[j]->d_name); } free(namelist[j]); } free(namelist); }
Gestion des erreurs
#include <err.h> #include <errno.h>
- Pour afficher une erreur et quitter : err(1, "erreur :%s", blabla);
- Pour afficher un avertissement et quitter : warn("attention :%s", blabla);
Compiler
Dans la plupart des cours en C, on n'apprend pas à compiler.
Pour un simple fichier "main.c", il suffit d'un:
make main
Qui est en fait un raccourci pour:
cc -o main main.c
Pour compiler en utilisant une bibliothèque, il faut utiliser les options "-l" "-L", et "-I".
Le "-I" permet de dire où se trouvent les entêtes (.h), "-L" indique où se trouvent les bibliothèques et "-l" indique quelle bibilothèque utiliser.
C'est à partir de ce moment qu'un fichier Makefile devient intéressant. Voici le modèle que j'utilise. Il est assez strict au niveau de la gestion des erreurs, ça me force à écrire du code moins sensible:
NAME = thename VERSION = 0.1 PREFIX?=/usr/local/ INCS = -I/usr/local/include LIBS = -L/usr/local/lib -lCHANGEME -lOTHERLIB LDFLAGS = ${LIBS} CPPFLAGS = -DVERSION=\"${VERSION}\" CFLAGS += -pedantic -Wall -Wextra -Wmissing-prototypes \ -Werror -Wshadow -Wstrict-overflow -fno-strict-aliasing \ -Wstrict-prototypes -Wwrite-strings \ ${CPPFLAGS} \ ${INCS} \ -Os .SUFFIXES: .c .o SRC != find . -type f -name \*.c H != find . -type f -name \*.h OBJ = ${SRC:.c=.o} .c.o: ${CC} ${CFLAGS} -c $< all: ${NAME} ${OBJ}: ${H} clean: rm -f ${NAME} *.core *.o ${NAME}: ${OBJ} ${CC} -o $@ ${OBJ} ${CFLAGS} ${LDFLAGS}
Bien sûr, il faudra changer "NAME", les "CHANGEME" après les "-l" et pourquoi pas lire "man make" :)
Si on veut compiler un binaire pour un chroot, il faut ajouter l'option "-static".
Listes chaînées
Les listes chaînées sont une méthode pour enregistrer des données. Un peu comme un dictionnaire en python. Et si c'est trop lent, vous pouvez regarder du côté des hashtables. Mais je trouve les premières largement suffisantes (on parle du C hein, c'est hyper rapide! :)).
J'en ai mis un exemple dans ma liste de "snippets":
gemini://si3t.ch/Logiciel-libre/Code/Snippets/C.gmi
https://si3t.ch/Logiciel-libre/Code/Snippets/C.html
Modestie
Parfois, on lit du code très difficile à comprendre, avec des opérations sur pointeurs pour gérer des chaînes de caractère par exemple.
Il faut rester parfois modeste, et arrêter de vouloir un code surpuissant meilleur que les autres. Il n'y a pas de code parfait.
Par exemple, c'est parfois tout aussi efficace de lire un fichier caractère après caractères plutôt que de chercher à le découper en morceaux à la recherche d'un motif particulier. Le C est très rapide :)
Lire un fichier ligne après lignes
getline(), et hop :)
Le man présente même un exemple.
Ceci dit, c'est souvent aussi rapide d'utiliser fgets() ou fgetc().
Ressources
Excellent site pour apprendre les bases du C
Learn C the Hard Way de Zed A. Shaw
Des remarques intéressantes pour écrire du C récent
Notions pour présenter du code propre
Des exemples de code très instructifs
Une réaction?
📧 Envoyez votre commentaire par mail.