Почему символьные литералы типа C вместо символов? - PullRequest
101 голосов
/ 12 января 2009

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

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

Ответы [ 12 ]

36 голосов
/ 12 января 2009

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

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

25 голосов
/ 23 апреля 2014

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

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

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

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

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

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

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

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

21 голосов
/ 12 января 2009

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

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

print('a');

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

18 голосов
/ 12 января 2009

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

7 голосов
/ 29 марта 2011

В момент написания C язык ассемблера MACRO-11 на PDP-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 - в зависимости от порядкового номера, а также обеспечить его выровнены правильно (т.е. не по нечетному адресу памяти).

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

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

6 голосов
/ 12 января 2009

Я помню, как читал 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;
}
5 голосов
/ 12 января 2009

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

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

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

1 голос
/ 10 июля 2018

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

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

1 голос
/ 12 января 2009

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

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

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

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

0 голосов
/ 12 января 2009

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

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

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

...