Pourquoi les littéraux de caractère C sont-ils au lieu de caractères?

En C ++, sizeof('a') == sizeof(char) == 1 . Cela a un sens intuitif, puisque 'a' est un littéral de caractère et sizeof(char) == 1 tel que défini par la norme.

En C cependant, sizeof('a') == sizeof(int) . Autrement dit, il semble que les littéraux de caractères C sont en fait des entiers. Quelqu’un sait-il pourquoi? Je peux trouver beaucoup de mentions de cette bizarrerie C mais aucune explication pour expliquer pourquoi il existe.

discussion sur le même sujet

“Plus précisément, les promotions intégrales. Dans K & R C, il était pratiquement impossible d’utiliser une valeur de caractère sans être promue en int, donc faire en sorte que le caractère constant int élimine en premier lieu cette étape. des constantes telles que “abcd” ou bien plusieurs vont tenir dans un int. ”

Je ne connais pas les raisons spécifiques pour lesquelles un littéral de caractère dans C est de type int. Mais en C ++, il y a une bonne raison de ne pas aller dans ce sens. Considère ceci:

 void print(int); void print(char); print('a'); 

Vous vous attendez à ce que l’appel à imprimer sélectionne la deuxième version prenant un caractère. Avoir un littéral de caractère étant un int rendrait cela impossible. Notez que dans C ++, les littéraux ayant plusieurs caractères ont toujours le type int, bien que leur valeur soit définie par l’implémentation. Donc, 'ab' a le type int , alors que 'a' a le type char .

La question initiale est “pourquoi?”

La raison en est que la définition d’un caractère littéral a évolué et changé, tout en essayant de restr compatible avec le code existant.

Dans les jours sombres du début de C, il n’y avait aucun type. Au moment où j’ai appris à programmer en C, les types avaient été introduits, mais les fonctions n’avaient pas de prototypes pour indiquer à l’appelant quels étaient les types d’arguments. Au lieu de cela, il était standardisé que tout ce qui passe en paramètre soit la taille d’un int (cela incluait tous les pointeurs) ou ce serait un double.

Cela signifiait que lorsque vous écriviez la fonction, tous les parameters qui n’étaient pas doubles étaient stockés dans la stack sous la forme ints, peu importe comment vous les déclariez, et le compilateur mettait du code dans la fonction pour gérer cela pour vous.

Cela a rendu les choses quelque peu incohérentes, alors quand K & R a écrit leur célèbre livre, ils ont mis en place la règle selon laquelle un littéral de caractère serait toujours promu dans un int dans n’importe quelle expression, pas seulement un paramètre de fonction.

Lorsque le comité ANSI a standardisé C pour la première fois, il a modifié cette règle afin qu’un littéral de caractère soit simplement un int, car cela semblait être un moyen plus simple d’obtenir la même chose.

Lors de la conception de C ++, toutes les fonctions devaient avoir des prototypes complets (cela n’est toujours pas requirejs en C, bien qu’il soit universellement accepté comme une bonne pratique). Pour cette raison, il a été décidé qu’un littéral de caractère pourrait être stocké dans un caractère. L’avantage de ceci dans C ++ est qu’une fonction avec un paramètre char et une fonction avec un paramètre int ont des signatures différentes. Cet avantage n’est pas le cas en C.

C’est pourquoi ils sont différents. Évolution…

en utilisant gcc sur mon MacBook, j’essaie:

 #include  #define test(A) do{printf(#A":\t%i\n",sizeof(A));}while(0) int main(void){ test('a'); test("a"); test(""); test(char); test(short); test(int); test(long); test((char)0x0); test((short)0x0); test((int)0x0); test((long)0x0); return 0; }; 

qui lors de l’exécution donne:

 'a': 4 "a": 2 "": 1 char: 1 short: 2 int: 4 long: 4 (char)0x0: 1 (short)0x0: 2 (int)0x0: 4 (long)0x0: 4 

ce qui suggère qu’un caractère est de 8 bits, comme vous vous en doutez, mais un littéral de caractère est un int.

À l’époque où C était en cours d’écriture, le langage d’assemblage MACRO-11 du PDP-11 avait:

 MOV #'A, R0 // 8-bit character encoding for 'A' into 16 bit register 

Ce genre de chose est assez courant en langage assembleur – les 8 bits inférieurs contiendront le code de caractère, les autres bits seront effacés à 0. PDP-11 avait même:

 MOV #"AB, R0 // 16-bit character encoding for 'A' (low byte) and 'B' 

Cela fournit un moyen pratique de charger deux caractères dans les octets bas et haut du registre 16 bits. Vous pourriez alors écrire ceux-ci ailleurs, en mettant à jour certaines données textuelles ou la mémoire d’écran.

Ainsi, l’idée que les caractères sont promus pour enregistrer la taille est tout à fait normale et souhaitable. Mais, disons que vous devez placer ‘A’ dans un registre non pas comme partie de l’opcode codé en dur, mais depuis quelque part dans la mémoire principale contenant:

 address: value 20: 'X' 21: 'A' 22: 'A' 23: 'X' 24: 0 25: 'A' 26: 'A' 27: 0 28: 'A' 

Si vous voulez lire un “A” de cette mémoire principale dans un registre, lequel lisez-vous?

  • Certains processeurs ne prennent en charge directement la lecture d’une valeur de 16 bits que dans un registre de 16 bits, ce qui signifie qu’une lecture à 20 ou 22 nécessiterait que les bits de «X» soient effacés et en fonction de l’unité du processeur. aurait besoin de passer dans l’octet de bas ordre.

  • Certains processeurs peuvent nécessiter une lecture alignée sur la mémoire, ce qui signifie que l’adresse la plus basse doit être un multiple de la taille des données: vous pourrez peut-être lire les adresses 24 et 25, mais pas 27 et 28.

Ainsi, un compilateur générant du code pour placer un “A” dans le registre préférera peut-être gaspiller un peu de mémoire supplémentaire et encoder la valeur 0 “A” ou “A” 0 en fonction de l’endianness et en veillant à ce qu’il soit correctement aligné ( c’est-à-dire pas à une adresse mémoire irrégulière).

Je suppose que C a simplement porté ce niveau de comportement centré sur le processeur, pensant aux constantes de caractères occupant les tailles de registre de la mémoire, en corroborant l’évaluation commune de C en tant qu ’« assembleur de haut niveau ».

(Voir 6.3.3 page 6-25 de http://www.dmv.net/dec/pdf/macro.pdf )

Je me souviens d’avoir lu K & R et d’avoir vu un fragment de code qui lisait un caractère à la fois jusqu’à ce qu’il touche EOF. Comme tous les caractères sont des caractères valides dans un fichier / stream d’entrée, cela signifie que EOF ne peut pas être une valeur de caractère. Ce que le code a fait a été de mettre le caractère lu dans un int, puis de tester EOF, puis de convertir en un caractère si ce n’était pas le cas.

Je me rends compte que cela ne répond pas exactement à votre question, mais il serait logique que le rest des caractères littéraux soit sizeof (int) si le littéral EOF était.

 int r; char buffer[1024], *p; // don't use in production - buffer overflow likely p = buffer; while ((r = getc(file)) != EOF) { *(p++) = (char) r; } 

Je n’ai pas vu de raison pour cela (les caractères littéraux C étant des types int), mais voici quelque chose que Stroustrup a à dire à ce sujet (tiré de Design and Evolution 11.2.1 – Résolution de grain fin):

En C, le type d’un littéral de caractère tel que 'a' est int . Étonnamment, donner 'a' type 'a' en C ++ ne pose aucun problème de compatibilité. Sauf pour l’exemple pathologique sizeof('a') , chaque construction pouvant être exprimée à la fois en C et en C ++ donne le même résultat.

Donc, pour la plupart, cela ne devrait poser aucun problème.

C’est le comportement correct, appelé “promotion intégrale”. Cela peut arriver dans d’autres cas aussi (principalement des opérateurs binarys, si je me souviens bien).

EDIT: Juste pour être sûr, j’ai vérifié ma copie d’ Expert C Programming: Deep Secrets , et j’ai confirmé qu’un littéral de caractère ne commence pas par un type int . Il est initialement de type char mais lorsqu’il est utilisé dans une expression , il est promu en int . Ce qui suit est cité dans le livre:

Les caractères littéraux ont le type int et ils y parviennent en suivant les règles de promotion du type char. Ceci est trop brièvement traité dans K & R 1, à la page 39 où il est dit:

Chaque caractère d’une expression est converti en un int …. Notez que tous les éléments flottants d’une expression sont convertis en double …. Comme un argument de fonction est une expression, les conversions de type ont également lieu lorsque les arguments sont transmis aux fonctions: en particulier, char et short deviennent int, float devient double.

Je ne sais pas, mais je vais deviner que c’était plus facile à mettre en œuvre de cette façon et que ça n’avait pas vraiment d’importance. Ce n’est que lorsque le type C ++ a pu déterminer quelle fonction serait appelée pour être corrigée.

Je ne le savais pas vraiment. Avant que les prototypes n’existent, tout object plus étroit qu’un int était converti en int lors de son utilisation comme argument de fonction. Cela peut faire partie de l’explication.

Ceci est seulement tangentiel à la spécification du langage, mais dans le matériel, le processeur a généralement une seule taille de registre – 32 bits, disons – et donc, chaque fois qu’il fonctionne sur un caractère (en l’ajoutant, en le soustrayant ou en le comparant) une conversion implicite en int lorsqu’elle est chargée dans le registre. Le compilateur prend soin de masquer et de décaler correctement le nombre après chaque opération afin que, si vous ajoutez, disons, 2 à (unsigned char) 254, il soit enroulé autour de 0 au lieu de 256, mais à l’intérieur jusqu’à ce que vous le sauvegardiez en mémoire.

C’est en quelque sorte un point académique car le langage aurait de toute façon pu spécifier un type littéral à 8 bits, mais dans ce cas, la spécification du langage reflète de plus près ce que fait réellement le processeur.

(Les winchs x86 peuvent noter qu’il existe par exemple un addh natif qui ajoute les registres courts dans une étape, mais à l’intérieur du kernel RISC, cela se traduit en deux étapes: append les nombres, puis étendre le signe, comme une paire add / extsh le PowerPC)

La raison historique en est que C, et son prédécesseur B, ont été initialement développés sur différents modèles de mini-ordinateurs DEC PDP avec différentes tailles de mots, qui supportaient le format ASCII 8 bits mais ne pouvaient effectuer que des calculs arithmétiques. (Pas le PDP-11, cependant; cela est venu plus tard.) Les premières versions de C définies comme étant la taille de mot native de la machine, et toute valeur inférieure à un int devait être élargie à int pour être transmise à ou à partir d’une fonction, ou utilisé dans une expression binary, logique ou arithmétique, car c’est ainsi que fonctionnait le matériel sous-jacent.

C’est également pour cette raison que les règles de promotion des nombres entiers indiquent toujours que tout type de données plus petit qu’un int est promu dans int . Les implémentations en C sont également autorisées à utiliser des mathématiques complémentaires plutôt que des compléments à deux pour des raisons historiques similaires. La raison pour laquelle les caractères octaux s’échappent et les constantes octales sont des citoyens de première classe par rapport à l’hex. De même, les minicompresseurs DEC ont été divisés en blocs de trois octets, mais pas en quatre octets.