Les variables et leurs types

Une chaîne de caractère est une suite contiguë de caractères se terminant par le caractère \0 (son code est l'entier 0).

Par exemple :

#include <stdio.h>

int main() {

char s[] = {'p', 'o', 'u', 't', 'o', 'u', '\0'};

printf("%s\n", s);

}

En utilisant la chaîne de caractères définie dans le code précédent :

  • quel est la taille de s ?
  • en utilisant la fonction strlen déterminez la taille de la chaîne de caractères contenue dans s

solution

TBD

Si l'on demande à printf d'afficher une chaîne de caractère, il va afficher la suite de caractères jusqu'à arriver au caractère \0. Ceci permet d'écrire plusieurs chaînes dans un tableau, comme le montre le code suivant :

#include <stdio.h>

int main() {

char s[] = {'p', 'o', 'u', '\0', 't', 'o', 'u', '\0'};

printf("%s\n", s);

}

En utilisant la chaîne de caractères définie dans le code précédent :

  • quel est la taille de s ?
  • en utilisant la fonction strlen déterminez la taille de la chaîne de caractères contenue dans s
  • comment afficher la seconde chaîne de caractères ?

solution

TBD :&s[4]

Il faut cependant faire très attention, si on oublie d'ajouter \0 à la fin du tableau, le comportement est UB (et résultera dans le meilleur des cas à un crash).

Pour éviter ceci, C fournit l'outil des strings literals :

en écrivant "une chaîne", le compilateur C crée dans la partie data du code (même une partie en lecture seule) un tableau contenant toute la chaîne suivie du caractère \0. Il est alors possible d'accéder à cette chaîne via un pointeur, comme le montre le code suivant :

#include <stdio.h>

int main() {

char *s = "poutou";

printf("%s\n", s);

}

Le pointeur s vaut l'adresse du premier caractère du strings literal. Ce string literals étant dans la partie en lecture seule de la partie data du programme, il est impossible de la modifier. Tenter de modifier un caractère d'un strings literal est UB.

On peut cependant mixer les deux approchent en écrivant :

#include <stdio.h>

int main() {

char s[] = "poutou";

s[0] = s[3] = 'c';

printf("%s\n", s);

}

Son fonctionnement est le suivant :

  1. le compilateur C place toujours le strings literal dans la partie data du code (il faut bien que la chaîne soit connue à l'exécution)
  2. lors de l'exécution, à la création du tableau, les caractères de la strings literal sont copiés dans le tableau (et donc la pile), ce qui permet de les modifier.

Ce tutoriel résume les différences dans ces deux modes de création.

En particulier :

danger !

Si vous stockez des chaînes de caractères dans des tableaux, assurez vous bien d'avoir toujours de la place pour stocker le \0 final !

Exécutez le code suivant et expliquer la différence de taille entre les trois tableaux :

#include <stdio.h>
#include <string.h>

int main() {

char *s= "coucou";
char s2[] = "coucou";
char s3[20] = "coucou";

printf("%lu\n", sizeof(s));
printf("%lu\n", sizeof(s2));
printf("%lu\n", sizeof(s3));

}

La libc de possède de nombreuses fonctions permettant de gérer les chaînes de caractères. Elles sont majoritairement définies dans [string.h] (https://www.w3schools.com/c/c_strings_functions.php). Vous connaissez déjà strlen mais il y en a de nombreuses autres :

Créez un programme qui concatène les trois chaînes "hello", "world" et "!" puis affiche le résultat.

solution

TBD

TBD : char *tab[] = {"coucou", "les", "gars"} idem que char **

entrées et sorties

stdout

printf envoie les caractères lues sur stdout. Si l'encodage des caractères est en UTF8 et que stdout l'est aussi, tout se passera bien.

stdin

Lire des chaînes de caractère depuis stdin peut être assez pénible. Ce tutoriel explique certaines techniques pour le faire.

Unicode

On rappelle qu'un char en C est le plus petit adressage possible. Sur (quasi) toutes les architectures, ce type est composé de 1 byte.

L'encodage par défaut de toute chaîne de caractère est actuellement utf-8.

Cet encodage est une façon de représenter des codepoint Unicode par un ou plusieurs byte. Il est donc tout à fait raisonnable d'utiliser des suite de char pour représenter une chaîne de caractères encodée en utf8.

Il faut cependant faire attention si l'on cherche à représenter ou afficher un caractère Unicode encodé en utf8, car les seuls caractères Unicode représentés par un unique byte (1 char) sont ceux de la table ASCII, de code allant de 1 à 127 (le code 0 étant égal à la fin de la chaîne).

Le code suivant fonctionne donc uniquement avec des caractères de la table ASCII :

char c = 'A'

printf("Caractère : %c\n", c);
printf("Code Unicode associé : %i\n", c);

Caractères multi-bytes

Les codes supérieurs ou égal à 128 sont encodés sur plusieurs bytes, donc plusieurs char. Par exemple le caractère Unicode "é" de code 233 (0xe9 en hexadécimal) est représenté par 2 bytes valant respectivement 195 (0xc3 en hexadécimal) et 169 (0xa9 en hexadécimal). Ces deux caractères sont des char char mais ne peuvent être représentés graphiquement isolément.

Le C ne présuppose par si le type char doit être considéré comme un entier signé ou non. Le code suivant peut donc ou pas donner le bon résultat :

char *s = "é"; // 2 char pour encoder é

printf("Caractère : %s ; Code utf8s associé : %i et %i\n", s, s[0], s[1]);

Il se trouve que :

Sous Linux le type char est non signé alors qu'il l'est sur mon mac...

Danger !

N'utilisez jamais le type char tel quel pour représenter des entiers, vous ne savez pas s'ils seront signé ou non.

Pour éviter toute confusion, dans sa version c23, le C définit le type char8_t dans <uchar.h> pour garantir que ce char est non signé et qu'il peut être utilisé pour représenter les entier associés aux caractères multi-byte de l'utf8. Il correspond à :

typedef unsigned char char8_t;

Le code suivant fonctionnera donc toujours :

printf("Caractère : %s ; Code utf8 associé : %i, %i\n", s, (char8_t)s[0], (char8_t)s[1]);

Si on a :

wide characters

Si l'on ne veut pas d'encodage multi-byte, on peut utiliser le type wchar_t défini dans <wchar.h>. Sur 99.9% des systèmes ce type correspond à un entier sur 32b qui contient directement le point de code Unicode, sans conversion.

Un point de code Unicode tenant sur 21bits, un type sur 4B est la taille minimum pour le contenir (il n'existe pas de structure entière sur 3 bytes. On ne garde que des puissances de 2 : 1, 2, 4 et 8).

On peut écrire directement une chaîne de caractère en wchar_t en préfixant le string literal par un L :

#include <stdio.h>
#include <wchar.h>


typedef unsigned char char8_t;

int main() {
wchar_t wc = L'é'; // 1 wchar_t pour encoder é

printf("Taille d'un wide char : %zu \n", sizeof(wchar_t));
printf("Pointcode : %i \n", wc);

}

Danger !

Ce à quoi correspond un wchar_t n'est pas défini et dépend du locale. Il est dans 99.9% du temps égal à un pint de code Unicode sur 4 bytes, mais ce n'est pas garanti.

Oui, c'est nul.

Danger !

Il existe aussi l'encodage utf16, sur 2 bytes mais je ne vois aucune raison raisonnable d'utiliser un tel codage.

Conversion

Danger !

Pour faire fonctionner les conversion, il faut charger une locale spécifique (par défaut cette locale est "C" qui ne connaît que l'ASCII).

TBD example de conversion et vérification de la taille finale.

c23

Le standard c23 essaie de mettre un peu d'ordre dans ces conversions. On définit dans <uchar.h> :

On peut également directement écrire :

Il existe aussi u"une chaîne en utf16" pour un encodage en utf16, mais il ne faut pas l'utiliser puisque le format utf16 est une aberration de la nature et ne devrait pas exister.

TBD : conversion c23 lorsque ça sera inclut dans le llvm mac.

Ce nest cependant pas encore parfait, mais tout devrait cependant bien se passer si vous compilez vos programmes pour un système récent.