# 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. => /Logiciel-libre/Code/Snippets/ 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 #include 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 #include ``` * 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 => https://www.learn-c.org/ Excellent site pour apprendre les bases du C => https://www.goodreads.com/book/show/13136685-learn-c-the-hard-way Learn C the Hard Way de Zed A. Shaw => https://matt.sh/howto-c Des remarques intéressantes pour écrire du C récent => http://man.openbsd.org/style Notions pour présenter du code propre => http://suckless.org/coding_style/ Idem, à la OpenBSD => https://rosettacode.org Des exemples de code très instructifs => https://c-for-dummies.com/ ## Une réaction? => mailto:bla@bla.si3t.ch?subject=ce-que-j-aurais-aime-premiere-fois-c 📧 Envoyez votre commentaire par mail. => mailto:bla+subscribe@bla.si3t.ch 📫 Abonnez-vous pour recevoir les réponses => /log/commentaires/ 📚 Consultez les archives. => mailto:bla+unsubscribe@bla.si3t.ch 💨 Vous désinscrire