Почему с символьные литералы целых чисел вместо символов?


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

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

12 99

12 ответов:

обсуждение та же тема

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

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

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

print('a');

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

исходный вопрос "почему?"

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

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

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

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

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

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

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

используя 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 бит, как вы подозреваете, но символьный литерал-это int.

назад, когда 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' были очищены, и в зависимости от endianness CPU один или другой должен был бы переместиться в байт низкого порядка.

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

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

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

(см. 6.3.3 на стр. 6-25 из http://www.dmv.net/dec/pdf/macro.pdf)

Я помню, как читал K & 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), но вот что-то Stroustrup должен был сказать об этом (от Design and Evolution 11.2.1-мелкозернистое разрешение):

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

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

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

EDIT: просто чтобы быть уверенным, я проверил свою копию Expert C Programming: Deep Secrets, и я подтвердил, что char литерал не начнем с тип int. Это изначально типа char но когда он используется в выражение, это произведен к Ан int. Из книги цитируется следующее:

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

каждый символ в выражении является преобразован в int....Заметить это все поплавки в выражении являются преобразовано в двойной....Поскольку аргумент функции-это выражение, преобразования типов также принимают место, когда аргументы передаются в функции: в в частности, char и short становятся int, поплавок становится двойным.

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

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

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

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

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

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

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