Le langage C est routinier dans l'informatique. On peut programmer tous les types d'ordinateurs avec ce langage. La majorité des langages actuels de programmation ont une syntaxe qui se rapproche au C, alors connaître le C permet d'avancer facilement vers ces langages. Le C est un langage impératif. Les lignes de code sont organisés en hiérarchies et sections. Les différents éléments des lignes de code sont des séquences de caractères. La grammaire du langage permet de déterminer ces séquences. Certaines séquences forment des pairs qui bornent d'autres séquences, d'où les hiérarchies.

Le code est réparti dans plusieurs fichiers source. Ces fichiers sont envoyés au compilateur pour la construction des programmes. Le compilateur traduit le code source en code machine, et inscrit ce code dans un nouveau fichier, qui lui est le programme compilé. L'ordinateur peut décoder directement le code machine, sans passer par un interprète. C'est pourquoi le C est un des langages de haut niveau dont la vitesse d'exécution est la plus rapide.

L'instruction la plus utile dans un langage impératif est la copie. La copie est l'opération de copier un contenu, vers un endroit où se trouvait précédemment un autre contenu. Voici un exemple erroné:

5 = 3;

Le point virgule termine l'instruction. On peut avoir plus d'une instruction par ligne de code. On peut aussi avoir une seule instruction qui est répartie sur plusieurs lignes de code. Le signe égale sépare ce qui doit être copié, de l'endroit où se fait cette copie. Cette instruction-ci particulière est erroné, car on essaie de copier la valeur 3 dans la valeur 5. Le nombre 5 seul n'existe dans aucun endroit en particulier, et donc n'a pas en lui-même de contenant. On ne peut copier une valeur vers un endroit qui n'existe pas.

Les contenants sont des endroits dans la mémoire vive de l'ordinateur. Cette mémoire est adressable. C'est à dire que pour chaque endroit différent dans la mémoire, on associe un nombre entier positif, qui représente cet endroit. On peut modifier l'instruction ci-haut pour qu'elle ne soit plus erroné:

*(int*)5 = 3;

Cette nouvelle instruction demande au compilateur de remplacer le contenu du contenant représenté par l'emplacement qui est la position 5 de la mémoire vive, par le contenu 3. Les séquences délimités par les parenthèses permettent de dire au compilateur que le programmeur souhaite ici considérer le nombre 5 comme étant un contenant. L'astérisque qui commence cette ligne indique au compilateur de manipuler le contenu du contenant, plutôt que le contenant lui-même. Sans cet astérisque le contenant lui-même serait considéré comme un contenu, mais dans cette ligne de code particulière, il y aurait alors deux problème : On ne peut remplacer l'adresse 5 par l'entier 3, car cette adresse n'a pas de contenant particulier. De même, on ne peut mettre comme valeur de contenant, une valeur qui n'est pas celle d'un autre contenant. De fait, un contenu doit être converti vers une valeur pouvant être contenu dans un contenant, si le type de valeur que contient le contenant n'est pas la même que celle de la valeur à se faire convertir. Cette conversion se fait implicitement par le compilateur, lorsque la valeur est celle d'un nombre. Cependant, pour les adresses, la conversion doit être explicite. C'est pourquoi un type en particulier est indiqué entre les parenthèses. Le type indiqué par le mot-clé int est le type des entiers de taille 32-bits. Chaque copie dans un contenant de ce type copie 32 bits consécutifs. Les différentes adresses successives pointent sur des incréments de 8 bits, et 8 bits c'est la taille d'un octet, qui est du type char. Donc toutes les variables sont un multiple de la taille d'une valeur de type char. Les champs de bits sont pour plus tard.

Chaque adresse est de même taille que toutes les autres. La taille des adresses dépend du mode de fonctionnement du processeur. Mais quasiment tout le temps, la taille est 32-bits ou 64-bits. Un processeur en mode 32-bits peut adresser jusqu'à 2 à la 32 adresses différentes. Comme chaque adresse est en incrément d'un octet, le maximum adressable est de 4 gigaoctets. En 64-bits, beaucoup plus d'adresses sont disponibles. C'est pour cela entre autres que ce mode a supplanté le mode 32-bits sur quelques plate-formes.

Une sorte de séquence particulière est la séquence identifiant. Cette sorte de séquence commence par une lettre ou un sous-tiret, et peut être suivi par un nombre illimité de lettres, sous tirets, ou chiffres. Les lettres peuvent être en majuscule ou minuscule, mais ne doivent pas avoir d'accents. Le nombre de caractères utilisés est déterminé lors de la définition d'un identifiant. L'identifiant est utilisé par le compilateur pour différencier un objet des autres objets. Alors il est important de toujours écrire de la même façon un identifiant donné. Si l'identifiant utilisé n'a pas été préalablement défini, le compilateur émettra une erreur, et la compilation n'aura pas lieu.

On ne peut pas déclarer un identifiant qui s'écrit de la même manière qu'un mot-clé. Un exemple de mots-clés sont les types de données.

Un identifiant sert donc à identifier un objet quelconque du code source, parmi les autres objets du code. On peut définir un pointeur, alors l'endroit où se trouve son contenu ( l'adresse qui pointe ailleurs) est décidé par le compilateur : cela rend les définitions de variables plus facile. Un pointeur est donc une variable, qui contient une adresse plutôt qu'un autre type de valeur.

Pour déclarer une variable ( ici un pointeur), il faut écrire une instruction, qui doit commencer par le type de donnée. L'astérisque qui suit indique au compilateur que c'est une adresse (un entier positif) vers une donnée du type donné, qui doit être réservée. On peut optionnellement initialiser lors de la déclaration, une valeur choisie, pour que la variable contienne dès la définition, cette valeur pour qu'elle soit utilisée. Sinon, elle contiendrait probablement un contenu aléatoire, tant qu'une première copie n'aura pas été faite, pour lui attribuer une valeur (une adresse).

int *pointe = (int*)5;

pointe = pointe + 1;

*pointe = 4;

Ici on a déclaré une variable de type pointeur sur entier, et on l'a initialisée à l'adresse 5 de la mémoire vive. Ensuite on copie vers le pointeur, son ancienne valeur, ajoutée d'une unité. À la fin, on copie à l'emplacement de la mémoire vive contenu dans le pointeur, la valeur 4 (un nombre entier).

Ajouter 1 à un pointeur n'incrémente pas l'adresse d'une unité. Ce que l'incrémentation d'une unité sur un pointeur fait, est de le faire pointer vers l'élément suivant dans la mémoire vive. Si le type est de 32-bits, alors l'élément suivant est 4 octets plus loin. Si le type est de 16-bits, qui est le type short, alors ajouter 1 à ce pointeur le fait pointer 16 bits plus loin (2 octets). Pour qu'un pointeur pointe un octet plus loin, il faut qu'il soit du type char. On peut momentanément convertir un pointeur vers le type char, de la même manière que le 5 a été converti en pointeur dans l'exemple.

Une manière plus simple de faire la même chose que l'exemple précédent est celle-ci :

int *pointe = (int*)5;

*++pointe = 4;

Lorsqu'une variable est précédée par deux plus ou deux moins, le contenu de la variable est incrémenté (décrémenté) d'une unité avant d'être utilisé. Si la variable se termine par deux plus ou deux moins, elle est alors utilisé avant que sont contenu soit incrémenté ou décrémenté. L'astérisque indique au compilateur d'accéder au contenu pointé de la variable. Mais en général on ne veut pas incrémenter un pointeur définitivement, si cela fait perdre l'adresse contenu originellement dans le pointeur.

On peut pointer temporairement sur un élément donné, sans que le pointeur d'origine ne soit modifié:

*(pointe + 1) = 4;

Il y a une syntaxe différente, pour faire la même chose, qui est plus simple à appliquer :

pointe[1] = 4;

Le nombre entier entre les crochets est temporairement additionné au pointeur, et ensuite celui-ci est déréférencé, permettant d'accéder à sa référence (la valeur pointé). Un pointeur est une référence vers un endroit référé. Un autre exemple :

pointe[0] = 4;

*(pointe+1) = 4;

pointe[2] = 4;

La première instruction copie 4 comme contenu à l'endroit pointé par le pointeur. La deuxième instruction copie 4 un entier plus loin. Et la troisième instruction copie 4 deux entiers plus loin. Il n'y a pas de règle qui décide de la manière de faire. On peut utiliser une syntaxe ou l'autre, mais la manière avec les crochets est plus simple :

(pointe + 1)[1] = 4;

pointe[2] = 4;

Ces deux instructions-ci font la même chose l'une et l'autre. La valeur 4 est copié deux fois d'affilé au même endroit. Ce n'est pas grave, mais c'est inutile. Dans la première instruction, ce n'est pas permis de déréférencer le pointeur avec un astérisque pour faire la copie, car le déréférencement se fait grâce aux crochets. Lorsqu'un pointeur est utilisé avec les crochets, c'est comme si on utilisait un tableau. Mais on ne peut pas réserver un pointeur avec des crochets vide, à la place de l'astérisque.

int tableau[4];

tableau[3] = 4;

Ici on a réservé une séquence de quatre entiers consécutifs, et on a copié 4 au dernier emplacement de cette réserve. Un tableau est comme un pointeur, mais on ne peut pas changer l'adresse où il pointe. De même que pour les variables ordinaires, l'emplacement d'un tableau n'est connu que du compilateur.

Il n'y a pas de mécanisme implicite pour s'assurer que l'on ne dépasse pas le dernier élément du tableau lorsqu'on accède à un de ses éléments :

int tableau[4];

int *pointe;

tableau[4] = 5;

Ici on a réservé une variable de type pointeur après avoir réservé un tableau de quatre éléments. On copie 5 au cinquième emplacement du tableau. Mais le tableau n'a que quatre éléments, et l'endroit suivant de la mémoire vive a été réservée pour la variable pointeur. Alors copier 5 dans le tableau au cinquième emplacement, revient à copier cette valeur dans le pointeur. Mais si le programme fonctionne en mode 64-bits, la copie n'est pas complète, et la moitié du pointeur n'est pas modifié, parce qu'un int est 32-bits, et une adresse est dans ce mode, 64 bits. Donc seul la première moité du pointeur a été assigné à 5. De même que le premier bit d'un entier ne détermine que la parité de cet entier, le premier octet ne détermine que les 8 premiers bits. Ce qui change d'une architecture à l'autre, est l'endroit où se trouve ce premier octet. Dans une architecture à petite finitude, le premier octet de la séquence d'octets consécutifs qui forment un entier, est l'octet qui détermine les 8 premiers bits de cet entier. Alors que le dernier octet de la séquence détermine les 8 derniers bits. Dans une architecture à grande finitude, le dernier octet de la séquence détermine les 8 premiers bits, et les 8 derniers bits sont déterminés par le premier octet de la séquence. Si le pointeur de l'exemple contenait zéro lorsque déclaré, alors la copie aurait assigné l'adresse 5 au pointeur, mais seulement si l'architecture où l'exemple est exécuté, est de petite finitude. Si elle aurait été de grande finitude, seul le quatrième octet de la séquence aurait effectivement changé, et l'adresse pointé aurait été 21474836480, plutôt que 5. En petite finitude, c'est seulement le premier octet de la séquence qui aurait effectivement changé, puisque 5 peut être contenu dans un octet. Les trois autres premier octets auraient également changés, mais leurs valeurs auraient étés mises à zéro (opération inutile puisque le pointeur contenait déjà zéro). Alors si le pointeur avait été 0 avant la copie, un seul octet aurait effectivement changé, mais le processeur aurait quand même copié 4 octets, même si cela était inutile pour trois d'entre eux. Alors si une caractéristique de l'architecture est la petite finitude, le pointeur se fait assigner l'adresse 5 aussi bien en mode 64-bits, que 32-bits. Mais en 64-bits il faut préalablement que le pointeur contienne la valeur zéro, parce que la copie ne change que la moitié des octets.

Si le tableau n'avait eu que trois éléments, le résultat ci-haut serait encore le même si le mode de fonctionnement était en 64-bits! Mais si le tableau n'avait eu que trois éléments et que c'est le quatrième qui aurait eu une assignation, alors le pointeur n'aurait alors pas changé, alors qu'en mode 32-bits il aurait changé. Parce que le quatrième élément du tableau aurait existé quand même, si le tableau n'avait été défini qu'avec trois éléments, si le mode d'adressage était de 64-bits. L'endroit où se trouve un élément de taille 32-bits doit être une adresse aligné. C'est à dire que l'adresse d'un entier 32-bits doit être un multiple de 32-bits, c'est à dire 4 octets. Plus généralement, une donnée doit avoir une adresse qui est aligné dans un multiple de la taille de cette donnée. Puisque en mode 64-bits un pointeur a une taille de 8 octets, son contenu doit être localisé dans un endroit en mémoire vive dont l'adresse est un multiple de 8. Avec un tableau de trois éléments de taille 32-bits, l'emplacement en mémoire vive qui suit le troisième élément est aligné en 32-bits, mais pas en 64-bits, alors le compilateur doit réserver 4 octets supplémentaires pour que le pointeur ensuite réservé soit aligné correctement. Ces 4 octets supplémentaires sont réservés, mais ne peuvent être normalement accédés, parce qu'ils ne sont pas liés à un identifiant. Sans identifiant, ces 4 octets n'auraient même pas étés réservés si ce code avait été compilé pour un mode 32-bits. Alors c'est recommandé de toujours utiliser des emplacements en mémoire vive qui ont d'abords étés réservés (avec identifiant), ou bien alloués.

La mémoire vive qu'un programme peut utiliser est séparé en deux parties. Une partie se nomme la pile, qui est l'endroit où sont réservé les variables. L'autre partie est le tas, c'est l'endroit où peuvent se faire allouer explicitement des régions de mémoire vive. Par exemple, si on veut mettre en mémoire vive certaines données dont la taille est déterminé pendant l'exécution du programme, on ne peut pas nécessairement pouvoir réserver un tableau assez grand pour toute les contenir, puisque le compilateur doit savoir au moment de la compilation la taille des données à réserver sur la pile. On doit donc allouer cet espace à partir du tas. Quand le tas n'a plus assez d'espace disponible pour allouer des données, le système en place pour gérer le tas demande au système d'exploitation d’agrandir le tas pour permettre d'acquérir assez d'espace pour continuer d'allouer des données. Si cet agrandissement n'est pas possible, alors l'allocation ne se fait pas. On dit alors qu'il n'y a plus de mémoire disponible. C'est pourquoi il est impératif de relâcher la mémoire alloué depuis le tas, lorsque les régions de mémoire alloués ne sont plus nécessaires.

Ce n'est pas nécessaire de relâcher toutes les données en mêmes temps. Plusieurs allocations peuvent se faire au cours de l'exécution du programme. Il n'y a pas d'ordre dans lequel on doit relâcher la mémoire: on peut donc allouer et relâcher comme on veut. Évidemment, il ne faut pas relâcher de la mémoire et ensuite continuer de l'utiliser, parce que la mémoire relâché peut être réaffecté ailleurs. Mais la règle est de relâcher la mémoire qui n'est plus utile, pour éviter qu'il y ait des fuites de mémoire. Ces fuites peuvent causer un manque de mémoire vive pour le programme, et parfois pour l'ordinateur en entier. Déterminer à quels endroits on peut relâcher la mémoire alloué permet de mieux comprendre le fonctionnement du programme. Cette compréhension rends la logique du programme plus robuste et aussi généralise l'habitude de relâcher une ressource lorsqu'elle n'est plus nécessaire.

Pour apprendre à allouer et relâcher la mémoire, il faut d'abord connaître le nom de deux identifiants, et aussi la manière de les invoquer. Le premier identifiant est malloc, et permet d'allouer la mémoire. Le deuxième identifiant est free, et permet de la relâcher. Ces identifiant représente un type d'objet qui n'est pas une donnée. Ce type est une routine, qui dans le C a pour terme, fonction. Les paramètres des fonctions se retrouvent entre parenthèses, et sont délimités entre-eux par des virgules. Une routine peut retourner ou non une valeur, qui sera utilisé après l'appel à la routine, à l'endroit où celle-ci a été appelé. Donc, si une fonction retourne un entier, on peut utiliser l'appel de la fonction dans un calcul plutôt que de copier préalablement la valeur retournée dans une variable. Bien entendu, si la fonction exécute elle-même un calcul, l'appeler qu'une fois et sauvegarder dans une variable sa valeur retournée, permet d'économiser du temps d'exécution, rendant le programme plus rapide. En C une fonction peut n'avoir aucun paramètres, si elle est déclarée à ce sens. Aussi bien malloc que free ne prennent chacun qu'un paramètre. Mais le paramètre que prends malloc est différent du paramètre pris par free. Le paramètre de malloc lui communique la quantité d'octets continus que doit allouer la fonction à partir du tas. La valeur que retourne malloc est l'endroit dans le tas où est localisé la mémoire qui a ainsi été alloué. Le paramètre de free doit être une des locations précédemment retournés par malloc. Après être relâché, une location de doit pas être relâché à nouveau, si elle n'avait pas d'abord été ré-alloué par malloc. Une troisième fonction est utile dans la gestion de la mémoire, c'est realloc. Cette fonction permet d’agrandir de la mémoire préalablement alloué. Si l’agrandissement n'est pas possible, la fonction alloue un nouveau bloc de mémoire de la bonne grosseur, copie dans ce bloc les donnée de l'ancien bloc, puis relâche l'ancien bloc au tas. La valeur retournée par realloc est l'endroit où a été ré-alloué la mémoire, ou l'endroit où la mémoire se trouve toujours, si l’agrandissement a pu avoir lieu. Cette fonction prends deux paramètres. Le premier paramètre est l'endroit où se trouve la mémoire à agrandir, et le deuxième paramètre est la taille que doit prendre la mémoire après agrandissement. Si la taille est plus petite que la taille que prends déjà le bloc de mémoire, alors il y a rétrécissement. Dans tel cas c'est peu concevable que realloc doive ré-allouer le bloc. Mais on ne sait jamais, alors il ne faut garder que la location retourné par realloc comme valeur, et considérer l'adresse utilisé comme premier paramètre, comme plus valide après l'appel. La fonction free peut également être utilisée pour relâcher la mémoire alloué par realloc. La fonction realloc ne peut que ré-allouer la mémoire précédemment alloué par realloc ou malloc, et la fonction realloc fonctionne comme malloc quand l'adresse à agrandir est l'adresse zéro. C'est à dire qu'il alloue un nouveau bloc de mémoire avec comme taille, celle demandée pour l'agrandissement.

L'adresse zéro est une convention en C. Toute adresse qui pointe vers le premier octet possible en mémoire vive est considéré comme non valide. Puisque c'est une convention, on peut quand même faire des programmes qui peuvent accéder à cette adresse sans que celle-ci soit invalide. Un exemple de tel programmes sont les noyaux des systèmes d'exploitation. Mais la convention est forte, et quand c'est possible les noyaux ainsi faits peuvent mettre en place un mécanisme pour qu'un programme utilisateur qui tente d'accéder à cette adresse cause une erreur nommé parfois, faute de segmentation. Cette erreur se produit aussi lorsque le programme tente d'accéder à une donnée dans la pile ou le tas qui n'est pas disponible, par exemple si l'adresse mémoire est plus grande que la taille totale du tas. Alors quand un pointeur est utilisé dans un programme et que l'on ne sait pas s'il contient une adresse valide, en vérifiant s'il pointe sur l'adresse zéro, on peut se persuader qu'il ne contient pas à ce moment une adresse vers des données.

Une autre erreur possible avec les pointeurs est d'utiliser un pointeur qui pointe vers un endroit dans un bloc mémoire alloué, mais ne pas réactualiser le pointeur si realloc change la location de ce bloc, ou si le bloc se fait relâcher au tas. Une manière d'éviter cette erreur est de n'avoir qu'un seul pointeur par bloc, et d'utiliser des entiers à la place de pointeurs pour garder en mémoire des endroits précis dans le bloc.

S'il existe des manière alternatives d'allouer et de relâcher des blocs de mémoire, il ne faut pas mélanger des fonctions prévues pour une manière, avec celles d'une autre manière. Différentes manières peuvent se partager entre-eux un tas, mais chaque manière a sa propre manière de garder en mémoire les blocs de mémoire vive alloués. Dans le meilleur des cas, relâcher au tas avec une fonction d'une manière, un bloc alloué avec une autre manière ne produirait qu'une fuite et rien d'autre. Dans le pire des cas, des parties critiques du tas seraient corrompues et le programme se ferait éjecter de l'exécution lors d'une faute de segmentation, faisant ainsi perdre des données qui n'avaient pas encore étés sauvés.

Voici un exemple qui illustre ce qui vient d'être appris :

char *nom;

int sz, len = 0;

sz = 512;

nom = (char*)malloc(sizeof(char)*sz);

Pour les variables ordinaire, le compilateur peut convertir implicitement une donnée d'un type, vers une donnée d'un autre type. Les variables de type pointeur ont tous la même taille, donc aucune conversion n'est nécessaire. Mais ce n'est pas une bonne idée de copier l'adresse d'un type de variable vers un pointeur d'un autre type, en général. Parce qu'une fois l'adresse copié, le compilateur s’attend à ce que les données contenus dans la région de mémoire pointé par le pointeur contienne des données définis par le type du pointeur. Aucune conversion n'aura lieu pour les données d'une région, si on copie son adresse dans un pointeur d'un type différent. Alors si on accède à l'un de ses éléments, c'est la donnée d'un type différent qui sera accédé, mais le compilateur croira que la donnée est du type du pointeur. Pour mitiger les erreurs de typage, il faut convertir explicitement un pointeur vers un pointeur d'un type différent. C'est pourquoi l'adresse renvoyé par la fonction malloc doit être converti pour pouvoir être assigné à la variable nom.

La fonction malloc prend un seul paramètre, qui est la taille de la région à allouer. Le paramètre ici est la quantité contenu dans la variable sz, fois la taille que prends une variable de type char. La fonction sizeof est une fonction spéciale, qui prend comme paramètre un type de donnée plutôt qu'une donnée, et retourne un entier qui est le nombre d'octets que prends en mémoire le type de donnée. Le paramètre pris par malloc est le nombre d'octets à allouer. Puisque pour accéder à chacune des données d'un tableau, il suffit d'ajouter au pointeur l'entier qui est l'index de la donnée, il suffit d'allouer une région qui a la taille d'un multiple de la taille en octets de la donnée, pour avoir un pointeur qui se comporte comme un tableau.

Il est utile de garder en mémoire la taille d'une région alloué, pour avoir un moyen de savoir s'il faut l'agrandir avant d'y inscrire des données. La variable len est suggéré pour contenir la quantité de données déjà inscrites. Les valeurs possibles qu'elle peut prendre sont donc entre zéro et sz. Si on se contentait que de sz pour indiquer la quantité, il faudrait utiliser realloc à chaque fois que l'on voudrait ajouter un élément, ou retirer un élément. Ce n'est pas recommandé d'utiliser les fonctions de gestion de mémoire trop souvent, car cela peut induire une fragmentation de la mémoire, ce qui encourerait de devoir agrandir le tas plus souvent. Également, puisque ce ne serait nécessaire d'appeler la fonction realloc uniquement si la variable len est égale à sz avant d'ajouter un élément, l'appel moins souvent de cette fonction rends le programme plus rapide. Chaque appel à une fonction nécessite la sauvegarde préalable de quelque valeurs. Au retour de la fonction ces valeurs sont récupéré. De plus le saut vers une partie différente du code peut impliquer que le processeur doit accéder en mémoire vive cette partie si elle n'est pas déjà dans la région de mémoire rapide du processeur. Cette région de mémoire est restreinte, alors le processeur ne peut y garder le programme au complet.

La fragmentation du tas implique que les régions alloués ont dans plusieurs cas une taille un peu plus grande que la taille alloué explicitement. Dans d'autres cas, il peut y avoir des régions qui ne sont pas encore alloués entre celles alloués. Si l'allocation de ces régions ne se fait pas, elles resteront inutilisés. Alors plus de mémoire sera nécessaire pour faire fonctionner le programme, alors que le programme utilisera bien moins de mémoire que la taille totale du tas. La gestion de la mémoire ne peut déplacer des régions alloués, car elle ne sait ce que le programme fait de ces régions. Donc le mieux qui peut être fait est d'éviter la fragmentation.

Voici un exemple qui utilise une caractéristique du type char:

char aphorisme[] = "Vive les maringouins!";

int sz;

sz = strlen(aphorisme);

Le tableau aphorisme n'indique pas directement au compilateur la taille qu'il prendra sur la pile. Chaque donnée de ce tableau est de type char. Une telle donnée peut soit être un entier entre -128 et 127, ou bien un caractère d'imprimerie représenté par un tel entier, tel que défini par l'ASCII. Il est peu usité de définir un tableau de caractères d'imprimerie en le définissant un caractère à la fois. Alors cela est plus utile de pouvoir le définir en tant que texte, car le code source est du texte. Les double guillemets bornent le texte de ce tableau. Les guillemets simples servent à définir qu'un seul caractère d'imprimerie en tant que donnée de type char, car il est peu usité de se servir de l'entier le représentant, plutôt que le caractère d'imprimerie considéré. Il est utile de savoir combien de caractères sont dans une séquence.

La convention est que le nombre zéro représente la fin d'une séquence de caractères! Mais comme pour les pointeurs, ce n'est qu'une convention, utilisé cette fois par le compilateur, qui réserve un élément de plus que la somme des éléments de la chaîne de caractères, pour faciliter l'évaluation de la taille de la chaîne. La fonction strlen prends comme paramètre un pointeur vers une chaîne, et compte le nombre d'éléments précédant l'élément zéro. Comme ceci :

int strlen(const char *str) {

int ret = 0;

while(str[ret]!=0) {

ret++;

}

return ret;

}

Ceci est une définition d'une fonction. C'est particulier car une fonction n'est pas contenu dans une instruction se terminant par un point-virgule. Plutôt, les instructions qui la composent sont bornés par des accolades. Le nom de la fonction doit être précédé par le type de donnée qu'elle retourne, et ses différents paramètres qu'elle prend, sont délimités par les virgules, et le tout bornés par des parenthèses. Comme cette fonction ne prends qu'un paramètre, il n'y a pas de virgules. Chaque paramètre est déclaré comme on déclare une variable, sauf que ce n'est pas permis de l'initialiser lors de la déclaration. L'identifiant de chaque paramètre permet de l'utiliser dans le code entre accolades. Un paramètre est donc une variable de cette fonction, mais initialisé à chaque fois que la fonction est appelé, assigné à la valeur transmise au moment de l'appel. Le paramètre ici utilise le mot-clé const, qui signifie que c'est possible de lire des éléments du pointeur, mais pas possible d'assigner des valeurs à ces éléments. C'est utile parce que le compilateur peut émettre une valeur, si une assignation est demandé. Cela indique alors une erreur de conception du programme. La première instruction est un entier. Cet entier est utilisé dans la fonction pour compter le nombre d'éléments de la chaîne.

Pour chaque élément de la chaîne, la fonction doit prendre une décision. Si la décision est que l'élément n'est pas le nombre zéro, alors la fonction continue à l'élément suivant. Elle arrête lorsque l'élément considéré est égale à zéro. La boucle while n'est pas une fonction. Ses accolades bornent le code exécuté par la boucle. Ce code est exécuté tant que la valeur décidé entre ses parenthèses est VRAI. Elle arrête l'exécution du code entre ses accolade lorsque la décision est FAUX. À chaque fois, elle prends la décision, ensuite le code est exécuté une fois, jusqu'à la prochaine décision, qui décide si le code se fait exécuter une nouvelle fois ou non. La décision ici est de savoir si l'élément considéré est égale ou non à zéro. Si l'élément n'égale pas zéro, alors la décision est VRAI, sinon elle est FAUSSE. La variable ret contient l'index de l'élément à considérer dans la chaîne. Cet index est incrémenté d'une unité à chaque fois que l'élément considéré n'est pas égale à zéro. Si le premier élément est égale à zéro, alors l'index n'est pas incrémenté. Puisque la valeur retournée est le nombres d'éléments de la chaîne, le nombre alors retournée est zéro.

La dernière instruction de la fonction est une fonction spéciale. Elle n'a pas besoin d'avoir son paramètre entre parenthèses. Elle arrête l'exécution de la fonction appelante, et la fait retourner comme valeur son paramètre.

8 C facile pour toi 2014-04-05