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

103

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

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

Джозеф Гарвин
источник
sizeof просто вернет размер байта, не так ли? Разве char и int не равны по размеру?
Джош Смитон,
1
Вероятно, это зависит от компилятора (и архитектуры). Не хотите сказать, что вы используете? Стандарт (по крайней мере, до 89 года) был очень рыхлый.
dmckee --- котенок экс-модератора
2
нет. char всегда имеет размер 1 байт, поэтому sizeof ('a') == 1 всегда (в c ++), тогда как int теоретически может быть sizeof равным 1, но для этого потребуется байт, имеющий не менее 16 бит, что очень маловероятно: ), поэтому sizeof ('a')! = sizeof (int) очень вероятно в C ++ в большинстве реализаций
Йоханнес Шауб - litb
2
... хотя в C. всегда неправильно
Йоханнес Шауб - litb
22
'a' - это int в C-периоде. C пришел первым - C установил правила. C ++ изменил правила. Вы можете утверждать, что правила C ++ имеют больше смысла, но изменение правил C принесет больше вреда, чем пользы, поэтому комитет по стандартизации C мудро не коснулся этого.
Джонатан Леффлер,

Ответы:

36

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

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

Malx
источник
Многосимвольные константы не переносимы даже между компиляторами на одной машине (хотя GCC, похоже, самосогласован на разных платформах). См .: stackoverflow.com/questions/328215
Джонатан Леффлер,
8
Я хотел бы отметить, что а) Эта цитата не указана; цитата просто говорит: "Вы не согласны с этим мнением, которое было опубликовано в прошлой ветке обсуждения рассматриваемой проблемы?" ... и б) Это нелепо , потому что charпеременная не является int, поэтому создание символьной константы равной единице - особый случай. И это легко использовать значение символа без его популяризации: c1 = c2;. OTOH c1 = 'x'- это понижающее преобразование. Самое главное, sizeof(char) != sizeof('x')это серьезная языковая лажа. Что касается многобайтовых символьных констант: они причина, но они устарели.
Джим Балтер
27

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

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

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

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

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

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

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

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

Джон Винсент
источник
2
+1 от меня за ответ «почему?». Но я не согласен с последним утверждением - «Преимущество этого в C ++ состоит в том, что функция с параметром char и функция с параметром int имеют разные сигнатуры» - в C ++ по-прежнему возможно, чтобы 2 функции имели параметры одинаковый размер и различные подписи, например , void f(unsigned char)Vs void f(signed char).
Peter K
3
@PeterK Джон мог бы выразиться лучше, но то, что он говорит, по сути, верно. Мотивация для изменения C ++ заключалась в том, что если вы пишете f('a'), вы, вероятно, захотите выбрать разрешение перегрузки f(char)для этого вызова, а не f(int). Относительные размеры intи char, как вы говорите, не имеют значения.
zwol
21

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

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

print('a');

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

Йоханнес Шауб - litb
источник
Да, в «Проектировании и эволюции C ++» говорится, что перегруженные процедуры ввода / вывода были основной причиной того, что C ++ изменил правила.
Макс Либберт,
5
Макс, да, я обманул. Я посмотрел стандарт в разделе совместимости :)
Йоханнес Шауб - litb
18

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

dmckee --- котенок экс-модератора
источник
7
+1 за то, что интересно. Люди часто думают, что sizeof ("a") и sizeof ("") являются char * и должны давать 4 (или 8). Но на самом деле в этот момент они char [] (sizeof (char [11]) дает 11). Ловушка для новичков.
paxdiablo
3
Символьный литерал не повышается до типа int, он уже является int. Никакого продвижения не происходит, если объект является операндом оператора sizeof. Если бы он был, то это противоречило бы цели sizeof.
Крис Янг,
@ Крис Янг: Я. Проверьте. Спасибо.
dmckee --- котенок экс-модератора
8

Когда писали 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 )

Тони Делрой
источник
5

Я помню, как читал 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;
}
Кайл Кронин
источник
Я не думаю, что 0 - допустимый символ.
gbjbaanb
3
@gbjbaanb: Конечно. Это нулевой символ. Подумай об этом. Вы думаете, что в файле не должно быть нулевых байтов?
P Daddy
1
Прочтите википедию - «Фактическое значение EOF - это системно-зависимое отрицательное число, обычно -1, которое гарантированно не равно любому допустимому коду символа».
Malx
2
Как говорит Малкс, EOF - это не тип char - это тип int. getchar () и друзья возвращают int, который может содержать любой char, а также EOF без конфликтов. Это действительно не требует, чтобы буквальные символы имели тип int.
Майкл Берр,
2
EOF == -1 появилось намного позже символьных констант C, так что это не ответ и даже не имеет значения.
Джим Балтер,
5

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

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

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

Майкл Берр
источник
Интересный! Kinda противоречит тому, что другие говорили о том, что комитет по стандартам C «мудро» решил не удалять эту причуду из C.
j_random_hacker
2

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

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

Дэвислор
источник
... и charсостоял ровно из 3 восьмеричных цифр
Антти Хаапала
1

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

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

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

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

PolyThinker
источник
Если верить другим комментариям, выражение 'a' начинается с типа int - внутри sizeof () не выполняется продвижение типа. То, что 'a' имеет тип int, кажется, всего лишь причуда C.
j_random_hacker
2
Литерал char действительно имеет тип int. Стандарт ANSI / ISO 99 называет их «целочисленными символьными константами» (чтобы отличать их от «широких символьных констант», которые имеют тип wchar_t) и, в частности, говорит: «Целочисленная символьная константа имеет тип int».
Майкл Берр,
Я имел в виду, что он не начинается с типа int, а скорее преобразуется в int из char (ответ отредактирован). Конечно, это, вероятно, никого не касается, кроме разработчиков компилятора, поскольку преобразование выполняется всегда.
PolyThinker
3
Нет! Если вы читаете стандарт C ANSI / ISO 99, вы обнаружите, что в C выражение 'a' начинается с типа int. Если у вас есть функция void f (int) и переменная char c, тогда f (c) будет выполнять интегральное продвижение, но f ('a') не будет, поскольку тип 'a' уже int. Удивительно, но факт.
j_random_hacker
2
«Просто для уверенности» - вы можете быть более уверенными, прочитав на самом деле утверждение: «Символьные литералы имеют тип int». «Могу только предположить, что это было одно из негласных изменений», - ошибочно предполагаете вы. Символьные литералы в C всегда имели тип int.
Джим Балтер
0

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

Ролан Рабьен
источник
0

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

Blaisorblade
источник
1
Еще один неудачный «ответ». Автоматическое преобразование , charчтобы intбы сделать это совершенно ненужным для символьных констант быть Интсом. Важно то, что язык трактует символьные константы по-другому (присваивая им другой тип) char, чем переменные, и что необходимо, так это объяснение этой разницы.
Джим Балтер
Спасибо за объяснение, которое вы дали ниже. Возможно, вы захотите более полно описать свое объяснение в ответе, где оно принадлежит, может быть одобрено и легко просматривается посетителями. Кроме того, я никогда не говорил, что у меня здесь хороший ответ. Следовательно, ваше оценочное суждение бесполезно.
Blaisorblade
0

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

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

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

Crashworks
источник
1
Еще один неправильный ответ. Проблема здесь в том, почему символьные литералы и charпеременные имеют разные типы. Автоматические продвижения, которые отражают оборудование, не актуальны - они фактически анти-релевантны, потому что charпеременные автоматически продвигаются, так что это не причина, по которой символьные литералы не относятся к типу char. Настоящая причина - многобайтовые литералы, которые сейчас устарели.
Джим Балтер
@Jim Balter Многобайтовые литералы вовсе не устарели; есть многобайтовые символы Unicode и UTF.
Crashworks
@Crashworks Мы говорим о многобайтовых символьных литералах, а не о многобайтовых строковых литералах. Постарайтесь обратить внимание.
Джим Балтер
4
Chrashworks действительно писал персонажей . Вы должны были написать, что литералы широких символов (скажем, L'à ') занимают больше байтов, но не называются многобайтовыми литералами символов. Если вы будете менее высокомерны, это поможет вам быть более точным.
Blaisorblade
@Blaisorblade Широкие символьные литералы здесь не актуальны - они не имеют ничего общего с тем, что я написал. Я был точен, а вам не хватает понимания, и ваша ложная попытка исправить меня - вот что высокомерно.
Джим Балтер,