Pourquoi les caractères C sont-ils des entiers au lieu de caractères?

103

En C ++, sizeof('a') == sizeof(char) == 1. Cela a un sens intuitif, car il 'a's'agit d'un caractère littéral et sizeof(char) == 1tel 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 soient en fait des entiers. Quelqu'un sait-il pourquoi? Je peux trouver beaucoup de mentions de cette bizarrerie C mais aucune explication pour pourquoi elle existe.

Joseph Garvin
la source
sizeof renverrait simplement la taille d'un octet, n'est-ce pas? Un caractère et un int ne sont-ils pas de taille égale?
Josh Smeaton
1
Cela dépend probablement du compilateur (et de l'architecture). Voulez-vous dire ce que vous utilisez? La norme (au moins jusqu'en 89) était très lâche.
dmckee --- ex-moderator chaton
2
non. un char est toujours large de 1 octet, donc sizeof ('a') == 1 toujours (en c ++), alors qu'un int peut théoriquement être sizeof de 1, mais cela nécessiterait un octet ayant au moins 16 bits, ce qui est très peu probable: ) donc sizeof ('a')! = sizeof (int) est très probable en C ++ dans la plupart des implémentations
Johannes Schaub - litb
2
... alors que c'est toujours faux en C.
Johannes Schaub - litb
22
'a' est un entier dans la période C. C est arrivé le premier - C a établi les règles. C ++ a changé les règles. Vous pouvez affirmer que les règles C ++ ont plus de sens, mais changer les règles C ferait plus de mal que de bien, donc le comité de standard C n'a pas touché à cela.
Jonathan Leffler

Réponses:

36

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 qu'elle soit d'abord promue au rang int, donc rendre le caractère constant int en premier lieu éliminé cette étape. Il y avait et il y a toujours plusieurs caractères des constantes telles que 'abcd' ou peu importe la taille d'un int. "

Malx
la source
Les constantes à plusieurs caractères ne sont pas portables, même entre compilateurs sur une seule machine (bien que GCC semble être auto-cohérent entre les plates-formes). Voir: stackoverflow.com/questions/328215
Jonathan Leffler
8
Je note que a) Cette citation n'est pas attribuée; la citation dit simplement "Seriez-vous en désaccord avec cette opinion, qui a été publiée dans un fil de discussion passé sur la question en question?" ... et b) C'est ridicule , car une charvariable n'est pas un int, donc rendre une constante de caractère un est un cas particulier. Et il est facile d'utiliser une valeur de caractère sans promotion: c1 = c2;. OTOH, c1 = 'x'est une conversion à la baisse. Plus important encore, sizeof(char) != sizeof('x')ce qui est un sérieux problème de langage. Quant aux constantes de caractères multi-octets: c'est la raison, mais elles sont obsolètes.
Jim Balter
27

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 rester rétrocompatible avec le code existant.

Dans les jours sombres du début du 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 dire à l'appelant quels étaient les types d'arguments. Au lieu de cela, il était normalisé que tout ce qui était passé en paramètre aurait soit la taille d'un int (cela incluait tous les pointeurs), soit un double.

Cela signifiait que lorsque vous écriviez la fonction, tous les paramètres qui n'étaient pas doubles étaient stockés sur la pile en tant qu'ints, quelle que soit la manière dont vous les avez déclarés, et le compilateur a mis du code dans la fonction pour gérer cela pour vous.

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

Lorsque le comité ANSI a normalisé C pour la première fois, il a changé cette règle pour qu'un caractère littéral soit simplement un int, car cela semblait être un moyen plus simple de réaliser la même chose.

Lors de la conception de C ++, toutes les fonctions devaient avoir des prototypes complets (ce n'est toujours pas nécessaire en C, bien que cela soit universellement accepté comme une bonne pratique). Pour cette raison, il a été décidé qu'un littéral de caractère pouvait être stocké dans un char. L'avantage de ceci en 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 chez C.

C'est pourquoi ils sont différents. Évolution...

John Vincent
la source
2
+1 de ma part pour avoir répondu «pourquoi?». Mais je ne suis pas d'accord avec la dernière déclaration - "L'avantage de ceci en C ++ est qu'une fonction avec un paramètre char et une fonction avec un paramètre int ont des signatures différentes" - en C ++, il est toujours possible que 2 fonctions aient des paramètres de même taille et des signatures différentes, par exemple void f(unsigned char)Vs void f(signed char).
Peter K
3
@PeterK John aurait pu mieux dire, mais ce qu'il dit est essentiellement exact. La motivation pour le changement dans C ++ était, si vous écrivez f('a'), vous voulez probablement que la résolution de surcharge choisisse f(char)pour cet appel plutôt que f(int). Les tailles relatives de intet charne sont pas pertinentes, comme vous le dites.
zwol
21

Je ne connais pas les raisons spécifiques pour lesquelles un littéral de caractère en C est de type int. Mais en C ++, il y a une bonne raison de ne pas suivre cette voie. 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 caractère littéral étant un int rendrait cela impossible. Notez que dans les littéraux C ++ ayant plus d'un caractère ont toujours le type int, bien que leur valeur soit définie par l'implémentation. Donc, 'ab'a type int, tandis que 'a'a type char.

Johannes Schaub - litb
la source
Oui, "Design and Evolution of C ++" dit que les routines d'entrée / sortie surchargées étaient la principale raison pour laquelle C ++ a changé les règles.
Max Lybbert
5
Max, ouais j'ai triché. j'ai regardé dans la norme dans la section compatibilité :)
Johannes Schaub - litb
18

en utilisant gcc sur mon MacBook, j'essaye:

#include <stdio.h>
#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 le pensez, mais un littéral de caractère est un int.

dmckee --- chaton ex-modérateur
la source
7
+1 pour être intéressant. Les gens pensent souvent que sizeof ("a") et sizeof ("") sont des char * et devraient donner 4 (ou 8). Mais en fait, ce sont des char [] à ce point (sizeof (char [11]) donne 11). Un piège pour les débutants.
paxdiablo
3
Un littéral de caractère n'est pas promu en int, c'est déjà un int. Il n'y a aucune promotion en cours si l'objet est un opérande de l'opérateur sizeof. S'il y en avait, cela irait à l'encontre du but de sizeof.
Chris Young
@Chris Young: Oui. Vérifier. Merci.
dmckee --- ex-moderator chaton
8

À 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 dans le langage d'assemblage - les 8 bits faibles contiendront le code de caractère, les autres bits effacés à 0. PDP-11 avait même:

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

Cela a fourni un moyen pratique de charger deux caractères dans les octets bas et haut du registre 16 bits. Vous pouvez ensuite les écrire ailleurs, en mettant à jour certaines données textuelles ou la mémoire d'écran.

Ainsi, l'idée de caractères promus pour enregistrer la taille est tout à fait normale et souhaitable. Mais, disons que vous devez insérer 'A' dans un registre non pas dans le cadre 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 juste un «A» de cette mémoire principale dans un registre, lequel liriez-vous?

  • Certains processeurs peuvent uniquement prendre en charge la lecture d'une valeur 16 bits dans un registre 16 bits, ce qui signifierait qu'une lecture à 20 ou 22 nécessiterait alors l'effacement des bits de `` X '', et en fonction de l'endianité du processeur, l'un ou l'autre aurait besoin de passer dans l'octet de poids faible.

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

Ainsi, un compilateur générant du code pour obtenir un 'A' dans le registre peut préférer gaspiller un peu de mémoire supplémentaire et encoder la valeur comme 0 'A' ou 'A' 0 - en fonction de l'endianness, et en s'assurant également qu'il est correctement aligné ( c'est-à-dire pas à une adresse mémoire impaire).

Je suppose que C a simplement porté ce niveau de comportement centré sur le processeur, en pensant aux constantes de caractère occupant des tailles de registre de mémoire, confirmant l'évaluation courante de C comme un "assembleur de haut niveau".

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

Tony Delroy
la source
5

Je me souviens avoir lu K&R et vu un extrait de code qui lirait un caractère à la fois jusqu'à ce qu'il atteigne EOF. Étant donné que tous les caractères sont des caractères valides dans un fichier / flux d'entrée, cela signifie que EOF ne peut pas être une valeur de caractère. Ce que le code a fait était de mettre le caractère lu dans un int, puis de tester EOF, puis de le convertir en 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 reste des littéraux de caractère soit sizeof (int) si le littéral EOF l'é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;
}
Kyle Cronin
la source
Je ne pense pas que 0 soit un caractère valide.
gbjbaanb
3
@gbjbaanb: Bien sûr. C'est le caractère nul. Pensez-y. Pensez-vous qu'un fichier ne devrait pas être autorisé à contenir zéro octet?
P Daddy le
1
Lire wikipedia - "La valeur réelle de EOF est un nombre négatif dépendant du système, généralement -1, qui est garanti comme étant différent de tout code de caractère valide."
Malx du
2
Comme le dit Malx - EOF n'est pas un type char - c'est un type int. getchar () et ses amis renvoient un int, qui peut contenir n'importe quel caractère ainsi que EOF sans conflit. Cela n'exigerait vraiment pas que les caractères littéraux aient le type int.
Michael Burr
2
EOF == -1 est venu longtemps après les constantes de caractères de C, donc ce n'est pas une réponse et même pas pertinent.
Jim Balter
5

Je n'ai pas vu de justification à cela (les littéraux de caractères C étant des types int), mais voici quelque chose que Stroustrup avait à dire à ce sujet (de Design and Evolution 11.2.1 - Fine-Grain Resolution):

En C, le type d'un littéral de caractère tel que 'a'is int. Étonnamment, donner du 'a'type charen C ++ ne pose aucun problème de compatibilité. À l'exception de l'exemple pathologique sizeof('a'), chaque construction qui peut ê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.

Michael Burr
la source
Intéressant! Un peu contredit ce que les autres disaient sur la façon dont le comité des normes C a décidé «sagement» de ne pas supprimer cette bizarrerie de C.
j_random_hacker
2

La raison historique en est que C, et son prédécesseur B, ont été développés à l'origine sur divers modèles de mini-ordinateurs DEC PDP avec différentes tailles de mots, qui supportaient l'ASCII 8 bits mais ne pouvaient exécuter l'arithmétique que sur les registres. (Pas le PDP-11, cependant; cela est venu plus tard.) Les premières versions de C intétaient définies comme étant la taille de mot native de la machine, et toute valeur inférieure à un intdevait être élargie intpour être transmise vers ou depuis une fonction , ou utilisé dans une expression binaire, logique ou arithmétique, car c'était ainsi que fonctionnait le matériel sous-jacent.

C'est aussi pourquoi les règles de promotion d'entiers indiquent toujours que tout type de données inférieur à an intest promu vers int. Les implémentations C sont également autorisées à utiliser les mathématiques du complément à un au lieu du complément à deux pour des raisons historiques similaires. La raison pour laquelle les échappements de caractères octaux et les constantes octales sont des citoyens de première classe par rapport à hexadécimal est également que ces premiers mini-ordinateurs DEC avaient des tailles de mot divisibles en blocs de trois octets mais pas en grignotages de quatre octets.

Davislor
la source
... et charfaisait exactement 3 chiffres octaux
Antti Haapala
1

C'est le comportement correct, appelé «promotion intégrale». Cela peut également arriver dans d'autres cas (principalement des opérateurs binaires, si je me souviens bien).

EDIT: Juste pour être sûr, j'ai vérifié ma copie de Programmation Expert C: Deep Secrets , et j'ai confirmé qu'un char littéral 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é du livre:

Les caractères littéraux ont le type int et ils y parviennent en suivant les règles de promotion à partir du type char. Ceci est trop brièvement couvert 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 flottants d'une expression sont convertis en double .... Puisqu'un argument de fonction est une expression, les conversions de type ont également lieu lorsque des arguments sont passés aux fonctions: in particulier, char et short deviennent int, float devient double.

PolyThinker
la source
Si l'on en croit les autres commentaires, l'expression 'a' commence par le type int - aucune promotion de type n'est effectuée à l'intérieur d'un sizeof (). Que «a» ait le type int est juste une bizarrerie de C il semble.
j_random_hacker
2
Un littéral char n'avoir le type int. La norme ANSI / ISO 99 les appelle 'constantes de caractère entier' (pour les différencier des 'constantes de caractère large', qui ont le type wchar_t) et dit spécifiquement: "Une constante de caractère entier a le type int."
Michael Burr
Ce que je voulais dire, c'est qu'il ne commence pas par le type int, mais plutôt converti en un int à partir de char (réponse modifiée). Bien sûr, cela ne concerne probablement personne sauf les rédacteurs de compilateurs puisque la conversion est toujours effectuée.
PolyThinker
3
Non! Si vous lisez la norme ANSI / ISO 99 C, vous trouverez qu'en C, l'expression «a» commence par le type int. Si vous avez une fonction vide f (int) et une variable char c, alors f (c) va effectuer la promotion intégrale, mais f ( 'a') ne sera pas le type de 'a' est déjà int. Etrange mais vrai.
j_random_hacker
2
"Juste pour être sûr" - Vous pourriez être plus sûr en lisant réellement l'instruction: "Les caractères littéraux ont le type int". "Je ne peux que supposer que c'était l'un des changements silencieux" - vous supposez à tort. Les caractères littéraux en C ont toujours été de type int.
Jim Balter
0

Je ne sais pas, mais je suppose que c'était plus facile de l'implémenter de cette façon et que cela n'avait pas vraiment d'importance. Ce n'est qu'en C ++ que le type a pu déterminer quelle fonction serait appelée qu'il fallait la corriger.

Roland Rabien
la source
0

Je ne savais pas cela en effet. Avant que les prototypes n'existent, tout ce qui est plus étroit qu'un int était converti en un int lors de son utilisation comme argument de fonction. Cela peut faire partie de l'explication.

Blaisorblade
la source
1
Une autre mauvaise "réponse". La conversion automatique de charen intrendrait tout à fait inutile que les constantes de caractère soient des entiers. Ce qui est pertinent, c'est que le langage traite les constantes de caractères différemment (en leur donnant un type différent) des charvariables, et ce qu'il faut, c'est une explication de cette différence.
Jim Balter
Merci pour l'explication que vous avez donnée ci-dessous. Vous voudrez peut-être décrire votre explication plus en détail dans une réponse, où elle appartient, peut être votée à la hausse et facilement visible par les visiteurs. De plus, je n'ai jamais dit que j'avais une bonne réponse ici. Par conséquent, votre jugement de valeur ne sert à rien.
Blaisorblade
0

Ce n'est que tangentiel à la spécification du langage, mais dans le matériel, le processeur n'a généralement qu'une seule taille de registre - 32 bits, disons - et donc chaque fois qu'il fonctionne réellement sur un caractère (en l'ajoutant, en le soustrayant ou en le comparant) il y a une conversion implicite en int lorsqu'il est chargé dans le registre. Le compilateur prend soin de bien masquer et de déplacer le nombre après chaque opération de sorte que si vous ajoutez, par exemple, 2 à (caractères non signés) 254, il s'enroulera à 0 au lieu de 256, mais à l'intérieur du silicium, c'est vraiment un int jusqu'à ce que vous le sauvegardiez en mémoire.

C'est en quelque sorte un point académique parce que le langage aurait pu spécifier un type littéral 8 bits de toute façon, mais dans ce cas, la spécification du langage reflète plus étroitement ce que fait vraiment le processeur.

(x86 wons peuvent noter qu'il existe par exemple une opération addh native qui ajoute les registres court-large en une seule étape, mais à l'intérieur du noyau RISC, cela se traduit en deux étapes: ajouter les nombres, puis étendre le signe, comme une paire add / extsh sur le PowerPC)

Crashworks
la source
1
Encore une mauvaise réponse. Le problème ici est de savoir pourquoi les littéraux de caractères et les charvariables ont des types différents. Les promotions automatiques, qui reflètent le matériel, ne sont pas pertinentes - elles sont en fait anti-pertinentes, car les charvariables sont automatiquement promues, ce n'est donc pas une raison pour que les littéraux de caractères ne soient pas de type char. La vraie raison est les littéraux multi-octets, qui sont maintenant obsolètes.
Jim Balter
@Jim Balter Les littéraux multi-octets ne sont pas du tout obsolètes; il y a des caractères Unicode et UTF multi-octets.
Crashworks
@Crashworks Nous parlons de littéraux de caractères multi-octets , pas de littéraux de chaînes multi-octets . Essayez de faire attention.
Jim Balter
4
Chrashworks a écrit des caractères . Vous devriez avoir écrit que les littéraux de caractères larges (disons L'à ') prennent plus d'octets mais ne sont pas appelés littéraux de caractères multi-octets. Être moins arrogant vous aiderait à être plus précis vous-même.
Blaisorblade
Les littéraux de caractères @Blaisorblade Wide ne sont pas pertinents ici - ils n'ont rien à voir avec ce que j'ai écrit. J'ai été précis et vous manquez de compréhension et votre fausse tentative de me corriger est ce qui est arrogant.
Jim Balter