Почему символьные литералы C являются целыми числами, а не символами?

StackOverflow https://stackoverflow.com/questions/433895

  •  10-07-2019
  •  | 
  •  

Вопрос

В C++, sizeof('a') == sizeof(char) == 1.Это имеет интуитивный смысл, поскольку 'a' является символьным литералом, и sizeof(char) == 1 как определено стандартом.

Однако в C, sizeof('a') == sizeof(int).То есть, похоже, что символьные литералы C на самом деле являются целыми числами.Кто-нибудь знает почему?Я могу найти множество упоминаний об этой причуде C, но никакого объяснения, почему она существует.

Это было полезно?

Решение

обсуждение той же темы

  

" Более конкретно, интегральные рекламные акции. В K & R C это было практически (?)   невозможно использовать символьное значение без того, чтобы его сначала повысили до int,   таким образом, делая символьной константой int в первую очередь, исключается этот шаг.   Были и остаются многосимвольные константы, такие как 'abcd' или однако   многие поместятся в int. "

Другие советы

Первоначальный вопрос "почему?"

Причина в том, что определение литерального символа развивалось и изменялось, пытаясь оставаться обратно совместимым с существующим кодом.

В темные дни раннего C не было типов вообще. К тому времени, когда я впервые научился программировать на C, были введены типы, но у функций не было прототипов, чтобы сообщить вызывающей стороне, какие были типы аргументов. Вместо этого было стандартизировано, что все, что передается в качестве параметра, будет либо иметь размер типа int (включая все указатели), либо это будет двойное число.

Это означало, что когда вы писали функцию, все параметры, которые не были двойными, хранились в стеке как целые, независимо от того, как вы их объявили, и компилятор поместил код в функцию, чтобы обработать это для вас.

Это делало вещи несколько противоречивыми, поэтому, когда K & amp; R написали свою знаменитую книгу, они добавили правило, что символьный литерал всегда будет переводиться в int в любом выражении, а не просто в параметр функции.

Когда комитет ANSI впервые стандартизировал C, он изменил это правило так, чтобы символьный литерал просто представлял собой int, поскольку это казалось более простым способом достижения того же самого.

Когда разрабатывался C ++, все функции должны были иметь полные прототипы (это все еще не требуется в C, хотя это общепринятая практика). Из-за этого было решено, что символьный литерал может храниться в символе. Преимущество этого в C ++ состоит в том, что функция с параметром char и функция с параметром int имеют разные сигнатуры. Это преимущество не имеет место в C.

Вот почему они разные. Эволюция ...

Я не знаю конкретных причин, почему символьный литерал в C имеет тип int. Но в C ++ есть веская причина не идти по этому пути. Учтите это:

void print(int);
void print(char);

print('a');

Можно ожидать, что вызов для печати выберет вторую версию с символом. Наличие литерала персонажа как int делает это невозможным. Обратите внимание, что в C ++ литералы, имеющие более одного символа, все еще имеют тип int, хотя их значение определяется реализацией. Итак, 'ab' имеет тип int , а 'a' имеет тип char .

используя gcc на моем MacBook, я пытаюсь:

#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;
};

который при запуске дает:

'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

, который предполагает, что символ состоит из 8 битов, как вы подозреваете, но литерал символа является целым числом.

Еще во времена написания C язык ассемблера PDP-11 MACRO-11 имел:

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

Подобные вещи довольно распространены в языке ассемблера - младшие 8 бит будут содержать код символа, остальные биты будут очищены до 0.PDP-11 даже имел:

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

Это обеспечило удобный способ загрузки двух символов в младший и старший байты 16-битного регистра.Затем вы могли бы записать их в другом месте, обновив некоторые текстовые данные или память экрана.

Таким образом, идея увеличения размера символов до регистра вполне нормальна и желательна.Но, допустим, вам нужно получить 'A' в регистр не как часть жестко запрограммированного кода операции, а откуда-то из основной памяти, содержащей:

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

Если вы хотите прочитать только букву "А" из этой основной памяти в регистр, какой из них вы бы прочитали?

  • Некоторые процессоры могут напрямую поддерживать только чтение 16-битного значения в 16-битный регистр, что означало бы, что чтение на 20 или 22 затем потребовало бы удаления битов из 'X', и в зависимости от конечного байта процессора тому или иному потребуется сдвиг в младший байт.

  • Некоторым процессорам может потребоваться чтение с выравниванием по памяти, что означает, что наименьший задействованный адрес должен быть кратен размеру данных:возможно, вы сможете читать с адресов 24 и 25, но не с 27 и 28.

Таким образом, компилятор, генерирующий код для получения 'A' в регистр, может предпочесть потратить немного дополнительной памяти и закодировать значение как 0 'A' или 'A' 0 - в зависимости от порядкового номера, а также обеспечить его правильное выравнивание (т.Е.не по нечетному адресу памяти).

Я предполагаю, что C просто перенес этот уровень ориентированного на процессор поведения, думая о символьных константах, занимающих регистровые размеры памяти, подтверждая общую оценку C как "ассемблера высокого уровня".

(См. пункт 6.3.3 на стр. 6-25 http://www.dmv.net/dec/pdf/macro.pdf)

Я помню, как читал K & amp; R и видел фрагмент кода, который будет читать символ за раз, пока он не достигнет EOF. Поскольку все символы являются допустимыми символами в файле / входном потоке, это означает, что EOF не может иметь никакого значения char. То, что сделал код, заключался в том, чтобы поместить символ чтения в int, затем проверить EOF, а затем преобразовать в символ, если это не так.

Я понимаю, что это не совсем отвечает на ваш вопрос, но было бы разумно, чтобы остальные литералы символов были sizeof (int), если литерал EOF был.

int r;
char buffer[1024], *p; // don't use in production - buffer overflow likely
p = buffer;

while ((r = getc(file)) != EOF)
{
  *(p++) = (char) r;
}

Я не видел обоснования для этого (литералы C char являются типами int), но вот что сказал по этому поводу Страуструп (из Design and Evolution 11.2.1 - Fine-Grain Resolution):

  

В C типом символьного литерала, например 'a' , является int .   Удивительно, но использование 'a' type char в C ++ не вызывает проблем с совместимостью.   За исключением патологического примера sizeof ('a') , каждая конструкция, которая может быть выражена   как в C, так и в C ++ дает одинаковый результат.

Так что по большей части это не должно вызывать проблем.

Это правильное поведение, которое называется "интегральное продвижение". Это может произойти и в других случаях (в основном, бинарные операторы, если я правильно помню).

РЕДАКТИРОВАТЬ: просто чтобы быть уверенным, я проверил свою копию Expert C Programming: Deep Secrets и подтвердил, что литерал char не начинается с типа < сильный> Int . Первоначально он имеет тип char , но когда он используется в выражении , он повышается до int . Следующее цитируется из книги:

  

Символьные литералы имеют тип int и   они добираются туда, следуя правилам   для продвижения от типа char. Это   слишком кратко освещено в K & R 1, на странице   39 где сказано:

     

Каждый символ в выражении   преобразован в int .... Обратите внимание, что   все выражения в выражении   преобразуется в двойной .... Так как   аргумент функции является выражением,   преобразования типов также имеют место, когда   аргументы передаются в функции: в   в частности, char и short становятся int,   float становится двойным.

Историческая причина этого заключается в том, что C и его предшественник B изначально разрабатывались на различных моделях миникомпьютеров DEC PDP с различными размерами слов, которые поддерживали 8-битный ASCII, но могли выполнять арифметику только для регистров. (Однако не PDP-11; это было позже.) Ранние версии C определяли int как собственный размер слова машины и любое значение меньше, чем int необходимо было расширить до int , чтобы передать его в функцию или из функции, или использовать в побитовом, логическом или арифметическом выражении, потому что именно так работало базовое оборудование.

Именно поэтому правила целочисленного продвижения по-прежнему говорят, что любой тип данных, меньший чем int , повышается до int . Реализациям на C также разрешается использовать математику с одним дополнением вместо дополнения с двумя по аналогичным историческим причинам. Причиной того, что восьмеричные символьные символы и восьмеричные константы являются первоклассными гражданами по сравнению с шестнадцатеричными, также является то, что у тех ранних миникомпьютеров DEC размеры слова делились на трехбайтовые куски, а не на четырехбайтовые кусочки.

Я не знаю, но я собираюсь догадаться, что реализовать это было проще, и это не имело значения. Только в C ++, когда тип мог определить, какая функция будет вызвана, ее нужно исправить.

Я действительно не знал этого. До появления прототипов все, что меньше, чем int, преобразовывалось в int при использовании его в качестве аргумента функции. Это может быть частью объяснения.

Это касается только спецификации языка, но в аппаратном обеспечении процессор обычно имеет только один размер регистра - скажем, 32 бита - и поэтому всякий раз, когда он действительно работает с char (путем добавления, вычитания или сравнения), происходит неявное преобразование в int при загрузке в регистр.Компилятор заботится о правильной маскировке и сдвиге числа после каждой операции, так что если вы добавите, скажем, 2 к (unsigned char) 254, оно обернется 0 вместо 256, но внутри silicon это действительно int, пока вы не сохраните его обратно в память.

Это своего рода академический момент, потому что язык в любом случае мог бы указать 8-битный литеральный тип, но в данном случае языковая спецификация более точно отражает то, что на самом деле делает процессор.

(любители x86 могут заметить, что существует например встроенная операционная система addh, которая добавляет регистры малой ширины за один шаг, но внутри ядра RISC это сводится к двум шагам:добавьте цифры, затем расширьте знак, как в паре add / extsh на PowerPC)

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top