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

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

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

Ответ 1

обсуждение тот же предмет

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

Ответ 2

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

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

print('a');

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

Ответ 3

используя 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.

Ответ 4

Оригинальный вопрос: "почему?"

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

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

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

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

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

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

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

Ответ 5

Назад, когда был написан 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'

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

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

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

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

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

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

Ответ 6

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

Я понимаю, что это точно не отвечает на ваш вопрос, но для остальной части литералов символов значение 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;
}

Ответ 7

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

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

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

Ответ 8

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

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

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

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

Ответ 9

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

Ответ 10

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

Ответ 11

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

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

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