Вся правда о целочисленных типах в C

Для начала несколько вопросов:

  1. Тип char по умолчанию знаковый или нет? А int?
  2. Законно ли неявное приведение (signed char *) к (char *)? А то же для int?
  3. Сколько бит в unsigned char?
  4. Какое максимальное число гарантированно можно поместить в int? А минимальное?
  5. Тип long определённо больше, чем char, не так ли?

Разумеется, экспериментально искать ответы на эти вопросы с помощью вашего любимого компилятора в вашей любимой системе на вашем любимом компьютере1) — не лучшая идея. Мы говорим о стандарте языка (С99 и новее).

Если вы уверенно сможете правильно ответить на эти вопросы, тогда эта статья не для вас. В противном случае десять минут, потраченные на её чтение, будут весьма полезны.

Предположу, что вы ответили
  1. Знаковые оба.
  2. Законны оба.
  3. 8.
  4. 2147483647. -2147483648.
  5. Конечно, Кэп.


А правильные ответы такие
  1. char — не регламентируется, int — знаковый.
  2. Для int — законно, а для char — нет.
  3. Не менее 8.
  4. 32767. -32767
  5. Вообще говоря, нет.




Про signed и unsigned


Все целочисленные типы кроме char, по умолчанию знаковые (signed).

С char ситуация сложнее. Стандарт устанавливает три различных типа: char, signed char, unsigned char. В частности, указатель типа (signed char *) не может быть неявно приведён к типу (char *).

Хотя формально это три разных типа, но фактически char эквивалентен либо signed char, либо unsigned char — на выбор компилятора (стандарт ничего конкретного не требует).

Подробнее про char я написал в комментариях.

О размере unsigned char


Тип unsigned char является абстракцией машинного байта. Важность этого типа проявляется в том, что С может адресовать память только с точностью до байта. На большинстве архитектур размер байта равен 8 бит, но бывают и исключения. Например, процессоры с 36-битной архитектурой как правило имеют 9-битный байт, а в некоторых DSP от Texas Instruments байты состоят из 16 или 32 бит. Древние архитектуры могут иметь короткие байты из 4, 5 или 7 бит.

Стандарт С вынужден отказаться от допотопных архитектур и требует, чтобы байты были как минимум 8-битные. Конкретное значение (CHAR_BIT2)) для данной платформы записано в заголовочном файле limits.h.

Размеры целочисленных типов в С


C переносимый, поэтому в нём базовые целочисленные типы (char, short, int и др.) не имеют строго установленного размера, а зависят от платформы. Однако эти типы не были бы переносимы, если бы
их размеры были совершенно произвольные: стандарт устанавливает минимальные диапазоны принимаемых значений для всех базовых целочисленные типов. А именно,

  • signed char: -127...127 (не -128...127; аналогично другие типы)
  • unsigned char: 0...255 (= 28−1)
  • signed short: -32767...32767
  • unsigned short: 0...65535 (= 216−1)
  • signed int: -32767...32767
  • unsigned int: 0...65535 (= 216−1)
  • signed long: -2147483647...2147483647
  • unsigned long: 0...4294967295 (= 232−1)
  • signed long long: -9223372036854775807...9223372036854775807
  • unsigned long long: 0...18446744073709551615 (= 264−1)

Стандарт требует, чтобы максимальное значение unsigned char было 2CHAR_BIT−1 (см. предыдущий пункт).

Стандарт требует sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long). Таким образом, вполне законны ситуации типа sizeof(char)=sizeof(long)=32. Для некоторых DSP от Texas Instruments так и есть.

Конкретные значения этих диапазонов для данной платформы указаны заголовочном файле limits.h.

Новые типы в С99


После того, как C99 добавил тип long long, целочисленных типов и путаницы стало ещё больше. Чтобы навести порядок, стандарт ввёл заголовочный файл stdint.h, где определяются типы вроде int16_t (равно 16 бит), int_least16_t (минимальный тип, способный вместить 16 бит), int_fast16_t (по крайней мере 16 бит, работа с этим типом наиболее быстрая на данной платформе) и т. п.

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

От типов вроде int16_t, со строгим указанием размера, страдает переносимость: скажем, на архитектуре с 9-битным байтом может просто не найтись 16-битного регистра. Поэтому стандарт тут явно говорит, что эти типы опциональны. Но учитывая, что какой бы код вы ни писали, чуть менее чем во всех случаях целевая архитектура фиксирована даже в худшем случае с точностью до семейства (скажем, x86 или AVR), внутри которого, размер байта не может вдруг поменяться, то переносимость фактически сохраняется. Более того, типы вроде int16_t оказались даже более популярными, чем int_least16_t и int_fast16_t, а при низкоуровневом программировании (микроконтроллеры, драйверы устройств) и подавно, ибо там зачастую неопределённость размера переменной просто непозволительна.



1) Для удобства тройку архитектура+ОС+компилятор далее будем называть просто платформой.
2) Этот макрос правильнее было бы назвать UCHAR_BIT, но по причинам совместимости он называется так, как называется.
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 54
  • +6
    Не раскрыт вопрос про size_t, ptrdiff_t, intptr_t, uintptr_t.
    • +2
      Я и про классические int, short… не всё рассказал. Так и задумано. Целью поста было рассмотрение только скользких вопросов. Типы size_t, ptrdiff_t, intptr_t, uintptr_t самоописательны и довольно предсказуемы.

      Подробное и полное описание всех целочисленных типов есть в стандарте. Читайте его.
      • +3
        Напомнило дурацкий вопрос на собеседованиях и тестах, что возвращает sizeof():
        1. int
        2. unsigned int
        3. size_t
        4. ...
        • +1
          size_t — беззнаковый целочисленный, в который гарантированно может поместиться размер теоретически самого большого массива данных в системе, в частном случае является результатом sizeof();
          ptrdiff_t — знаковый целочисленный, в который гарантированно может поместиться размер теоретически самого большого массива данных в системе, является результатом операции вычитания одного указателя из другого;
          intptr_t, uintptr_t — знаковый и беззнаковый целочисленные типы, способные безопасно хранить указатель независимо от платформы.
          • 0
            А правда, что sizeof(void*) == sizeof(long)?
            В сорцах Linux ядра видел косвенное тому подтверждение.
            • +1
              Вообще говоря, нет.
              • 0
                void* — это все-таки указатель, и его размер в 32-разрядной системе составляет 4 байта, как написал автор статьи, long — это как минимум 4 байта. Поэтому правда, но для x86 и аналогичных платформ.
                • 0
                  По стандарту нет. По факту, в исходниках ядра Linux действительно считается что равны (наверно так исторически сложилось, т.к. в заголовках ядра присутствует тип uintptr_t, который к слову объявлен как unsigned long), соответственно это так для платформ (toolchain+архитектура процессора) на которых.работает Linux.
            • +4
              Стандарт — один из основных документов, которым я руководствуюсь в своей деятельности.

              Меня всегда удивляет как «чтение стандарта вслух» вызывает восторженные и удивлённые возгласы «да вы что?», «не может быть!» даже у senior программистов Си.
          • –1
            Далек я от С, но складывается стойкое ощущение что что-то и где-то в нем пошло не так. Ну слишком уж много в нем совершенно базовых особенностей «которые должен знать каждый пишущий на С программист»
            • +3
              Всё в нём так, просто не забывайте его нишу. Си должен быть, с одной стороны, крайне низкоуровневым, а с другой — максимально переносим. Это накладывает свой отпечаток.

              Кстати, давайте не будет продолжать оффтоп.
              • +3
                это неизбежная судьба языка, через который можно достучаться почти до аппаратного уровня и многокластерного сервера и бытового холодильника
                • +6
                  Это вы еще C++ не видели…
                • +5
                  sizeof(char)>sizeof(long)

                  Нет!
                  В стандарте сказано, что sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long). Раздел 6.3.1.1.
                  • 0
                    Спасибо, исправил.
                    • 0
                      Что ж, это ещё более наглядно показывает, как хитровыкручен стандарт.
                  • +3
                    Таким образом, вполне законны ситуации типа sizeof(char)>sizeof(long).


                    Не бывает так.

                    n1570, 6.2.5p8:
                    For any two integer types with the same signedness and different integer conversion rank
                    (see 6.3.1.1), the range of values of the type with smaller integer conversion rank is a
                    subrange of the values of the other type.


                    6.3.1.1p1:
                    The rank of long long int shall be greater than the rank of long int, which
                    shall be greater than the rank of int, which shall be greater than the rank of short
                    int, which shall be greater than the rank of signed char.

                    • 0
                      Да-да, я лох. Не заметил этого пункта.
                    • –3
                      C переносимый, поэтому в нём базовые целочисленные типы (char, short, int и др.) не имеют строго установленного размера, а зависят от платформы.

                      То, что C переносимый — это миф. Может раньше он и был таковым, но сейчас — увы. Вышепреведенная фраза очень похожа на следующую: «в целях заботы о пользователях мы поднимаем плату за обслуживание». Спасибо, не надо.

                      Интересно, много ли на свете программистов, которые хотели бы, чтобы они не знали размеры базовых типов? Наверно, это чтобы было быстро? Т.е. лучше быстро и глючно? Наверно поэтому практически любая библиотека включает в себя configure — чтобы переносилось, ведь это ж часть языка, т.к. язык все может, ведь он переносим, да? А обилие #define тоже наверно говорит о великолепной переносимости? А то, что стандартные библиотеки не могут кроссплатформенно открыть, например, pipe — это тоже говорит о великолепной переносимости. В общем, заявление о том, что для поддержки переносимости нет определенности — это, извините, к доктору. Если хочется переносимости, то тогда надо все четко специфицировать, как, например, в Java. И не надо лохматить бабушку.
                      • 0
                        ну, программисты знают размеры базовых типов — sizeof в помощь
                        • –3
                          Как sizeof поможет решить, какой тип использовать? Например, мне нужно проитерировать миллион элементов. Как, используя sizeof и без дефайнов мне написать такой цикл «переносимо»?
                          • 0
                            size_t — единственный правильный тип индексов.
                            • –4
                              Согласно стандарту size_t позволяет проитерировать миллион элементов?
                              • 0
                                Согласно стандарту верно одно из двух: или size_t позволяет проитерировать миллион элементов или такой большой массив не поместится в память вашей машины.
                                • –2
                                  А если данные итерируются на диске или еще где-нибудь? Ведь я не говорил, что данные в памяти.
                                  • +1
                                    мне кажется, вы передергиваете понятие переносимости
                                    • –3
                                      Из википедии:
                                      Портируемость (переносимость, англ. portability) обычно относится к одной из двух вещей:
                                      1. Портируемость — как возможность единожды откомпилировав код (обычно в некоторый промежуточный код, который затем интерпретируется или компилируется во время исполнения, «на лету», англ. Just-In-Time), затем запускать его на множестве платформ без каких-либо изменений.
                                      2. Портируемость — как свойство программного обеспечения, описывающее, насколько легко это ПО может быть портировано. По мере развития операционных систем, языков и техники программирования, становится всё проще портировать программы между различными платформами. Одной из изначальных целей создания языка Си и стандартной библиотеки этого языка — была возможность простого портирования программ между несовместимыми аппаратными платформами. Дополнительные преимущества в плане портируемости могут иметь программы, удовлетворяющие специальным стандартам и правилам написания (см., например: Smart Package Manager).

                                      Вот теперь мне хочется понять, какой из пунктов для C легко реализуется?
                                      • 0
                                        2
                                        • +5
                                          1 — не смешно
                                          2 — реализуется. настолько легко, насколько вообще можно учесть в одном коде огромное количество различий (да-да, в том числе размеров базовых типов) платформ.

                                          если вы ждете такой же «легкости», как у java или чисто интерпретируемых языков, мне жаль вас огорчать.
                                          а вообще azudem ниже прекрасно все расписал
                                      • +1
                                        Если данные на диске, то вы ничего не итерируете, а осуществляете random access к файлу. Поэтому по стандарту Си (чтобы быть максимально педантичным отвечая на троллинговый вопрос) вам нужно использовать fpos_t вместе со стандатрными функциями из stdio.h.

                                        Если быть более реалистичным и расширить кругозор до POSIX, то мы увидим off_t.
                                        • –3
                                          Ну вот пример: у меня есть файлы на диске file.1, file.2,… file.1000000. Мне надо пройтись по всем файлам и показать контент. Каждый файл, например, занимает 10 байт (хотя это не принципиально). Как это сделать переносимо по стандарту?
                                          • +2
                                            Вы пытаетесь придумать уже третий пример на эту тему, но пока получается только fail. Вы не думаете что вам нужно просто почитать стандарт?

                                            В данном случае, так как известно что файлов всегда ровно миллион, я бы применил uint32_t для номера файла. Но в общем случае стоит применить uintmax_t.
                                        • 0
                                          что бы работать с данными, их нужно загрузить в озу
                                      • 0
                                        на 32битной платформе ты как ни прыгай, но миллиард элементов ты не проитерируешь при помощи size_t эффективно.
                                • +4
                                  Нет особобенных проблем в написании переносимого кода. Даже между очень разными архитектурами (разный endian, размеры базовых типов и т.п.). Утыкивать код кучей #define для этого не требуется.

                                  configure из autotools используется для другого — отключить часть функционала, указать где библиотеки брать и заголовки, куда ставить бинарники.
                                  • +1
                                    Си переносим только в плане платформ процессоров.
                                  • +8
                                    Си ни в коем случае не является абсолютно переносимым языком. Такого языка не может быть в принципе.

                                    Си такой уродливый и костылистый лишь из-за того, что его долбят молота с двух сторон: низкоуровневость и переносимость. Если вы сможете придумать лучшую замену Си — дерзайте!

                                    1) Интересно, много ли на свете программистов, которые хотели бы, чтобы они не знали размеры базовых типов?

                                    Я один из них. Если мне нужна переменная для содержания любого целого числа от 1 до 100, мне важно лишь, чтобы используемый мной тип это позволял. А способен ли он вместить больше — меня не интересует.

                                    Если я пишу код, где мне нужно точно знать размер типов, то я воспользуюсь типами вроде uint8_t. Например, это основные типы данных, когда я пишу firmware для AVR.

                                    Если у меня массив из большого числа переменных и мне нужно минимизировать память, то мне нужен тип, который гарантировано вмещает числа от 0 до 100 и был минимального размера. Из stdint.h я выбираю int_least8_t. (Тип int8_t может быть не доступен на моей архитектуре, например если байты 9-битные; а int_least8_t в этом случае и будет 9 бит).

                                    Если мне нужно обеспечить быструю работу с моей переменной, то я выберу int_fast8_t. Пускай на каком-нибудь DSP он будет 32-битный, но работа с 32-битными регистрами на данной архитектурами будет наиболее эффективная.

                                    2) Наверно поэтому практически любая библиотека включает в себя configure. А обилие #define тоже наверно говорит о великолепной переносимости.

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

                                    3) то, что стандартные библиотеки не могут кроссплатформенно открыть

                                    Это проблема библиотек.

                                    4) Если хочется переносимости, то тогда надо все четко специфицировать, как, например, в Java.

                                    Java высоуровневый язык. На нём ОС или драйвер не напишешь (если только не смеха ради).
                                    • –3
                                      Безусловно, язык С справляется на 100% если речь идет про драйвера и низкоуровневые вещи. Я про другое. Про то, что на данный момент С является одним из самых непереносимых языков. Наверно, по непереносимости его переплюнет лишь ассемблер. Другие языки хорошо себя чувствуют на разных платформах. Просто С — он всеядный, т.е. доступен на огромном количестве платформ, и это иногда путают с переносимостью. Те же драйвера всегда пишутся под конкретную платформу и ось, что говорит о слабой переносимости. Каждый язык хорош для своих задач. Просто спецификация того, что char может быть 8-битным, а может быть и нет, говорит о том, что язык стараются адаптировать под различные платформы, а переносимость, т.е. портируемость этого кода на другие плафтормы, от этого лишь страдает.
                                      • +3
                                        Про то, что на данный момент С является одним из самых непереносимых языков. Другие языки хорошо себя чувствуют на разных платформах.

                                        Вы какую-то чушь написали. Компилятор для языка С для какой-нибудь платформы X — появляется самым первым. Либо это совсем уж экзотика какая-то.
                                        • +1
                                          Не могу с вами согласиться. Си предоставляет возможность писать переносимый код, но не обязывает эту возможность использовать. Большинство реального кода пишется под конкретную арлитектуру, зачастую с фиксированной разрядностю. В таком коде могут итерировать массивы int'ом, указатели приводить к long, делать бинарные операции с предположением о 8-битности байта или просто использовать типы со строгим размером типа uint8_t. Си позволяет это делать; кто-то (вроде Томсон) сказал, что Си как лезвие — если им неправильно пользоваться, можно создать кровавое месиво.

                                          Если же писать качественно, с учётом всех возможностей и рекомендаций стандарта, то получить непереносимый код не так-то просто. Даже в контексте рассматриваемой темы, где под переносимость в основном понимается переносимость между процессорами с различной разрядностю и набором регистров, придумать правильно написанный пример непереносимого кода у меня не получилось. Может вы такой пример предъявите, тогда мы перейдем от философствования к предметному разговору.
                                      • +1
                                        Автор молодец.

                                        Но вообще грустно очень. Писать переносимо на C/C++ (одна из задач, для которой язык создавался) умеют совсем немногие.
                                        • 0
                                          Эх! Помню как убил пару дней отлаживая программу, которая под х86 работала как надо, а под х86_64 — нет!
                                          Оказалось, что:
                                          на х86:
                                          sizeof(int)=4, sizeof(long)=4, sizeof(long long)=8
                                          на х86_64:
                                          sizeof(int)=4, sizeof(long)=8, sizeof(long long)=8

                                          А в коде много было переменных типа long.
                                          • +1
                                            Я ответил на все вопросы правильно! Как я так смог? Да в книге Страуструппа все есть =)
                                            Даже то, что char, signed char и unsigned char — Три разных типа!

                                            Особенно 3 вопрос простой — неужели никто в bc 3.1 не программировал с 16 битным int'ом? :) Неужели никто не читал основы такого языков, как Java, который постулирует одинаковый размер int'a в отличие от С\С++ ?!
                                            • 0
                                              Какая связь между вопросом 3 и 16-битным int? Я на (3) ответил «не менее 4» (более-менее наугад), а на (4) — «от -128 до 127» — не был уверен, что хотя бы 2 байта гарантируются. Но я стандарта не читал :(
                                              • 0
                                                Пардон, конечно же 4-ый вопрос! Видимо, было слегка поздновато, невнимательность!

                                                И отвечая товарищу azudem ниже (из-за кармы приходится группировать сообщения) — да, про подмножества мне очень хорошо известно, любимый пример некомпилируемой программы в С++ и компилируемой в С — int try;
                                                Но тут речь о приемственности стандартов! В возможности С++ так же входит низкоуровневая работа, поэтому почему бы не унаследовать приемы от языка, который себя прекрасно в этом зарекомендовал! И эту строчку со сравнениями я узнаю из книги страуструппа, могу предположить, что она попала в стандарт С++ простым (вдумчивым ?) копированием из стандарта С.
                                                • 0
                                                  Строчка про sizeof() ни в стандарте C, ни в стандарте C++ не встречается. Но та же информация про размеры типов в стандарте C++ выражена более явно:

                                                  [basic.fundamental]p2:
                                                  There are five standard signed integer types: “signed char”, “short int”, “int”, “long int”, and “long
                                                  long int”. In this list, each type provides at least as much storage as those preceding it in the list.
                                                  • 0
                                                    Про какой sizeof() вы говорите?
                                                    • 0
                                                      Я говорю про habrahabr.ru/post/156593/#comment_5349219

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

                                                Про 16-битный int многие знают из практики, но как правило считают, что нижняя граница диапазона на 1 меньше, чем гарантирует стандарт.
                                              • 0
                                                Программил на низкоуровневом Си где-то год под различные контроллеры, а доподлинно знал ответы только на 3 и 4 (и то из-за внимательного чтения K&R). Век живи, век познавай. Спасибо за статью, отличная!
                                                • 0
                                                  А правильно я понимаю, что unsigned char и signed char — это как бы байты и уж как минимум абстракции чисел, а char — это вообще не число, а символ алфавита и может занимать хоть 4 байта?
                                                  • +1
                                                    а char — это вообще не число, а символ алфавита

                                                    Вы слишком резко. Все рассмотренные в статье типы предназначены для хранения целых чисел. Их интерпретация на совести программиста. Но, действительно, char является естественным типом для хранения базовых символов.

                                                    Точнее, стандарт определяет базовый набор символов (basic character set), указывая минимальные требования к нему (в частности, он должен включать латинский алфавит в обоих регистрах), а тип char должен вмещать любой из базовых символов, причём так, чтобы их числовое значение было неотрицательным. (Конкретный пример: базовый набор — ASCII, а char — знаковый 8-битный целочисленный тип.)

                                                    Строки определяются как массивы из элементов типа char, заканчивающийся символом '\0', численное значение которого не обязано быть нулём. Стандарт определяет для строк синтаксический сахар через двойные кавычки. Но это я уже ушёл от темы.

                                                    Если же говорить о char как просто о целочисленном типе, то стандарт требует, чтобы диапазон его значений совпадал либо с signed char, либо с unsigned char, на выбор компилятора. То есть фактически тип char эквивалентен одному из этих типов, как я уже писал в статье.
                                                    • 0
                                                      А если basic character set — это, например, три миллиона символов, но при этом мы работаем на 8 или 16-битной машине, то тогда int будет обязан иметь размер больше, чем 3 байта?
                                                      • 0
                                                        char в данном случае должен вмещать любой из трёх миллионов символов, а int не может быть меньше char. То есть размер int не может быть меньше 22 бит.

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.