- PVSM.RU - https://www.pvsm.ru -
В основу статьи легли мои собственные выработанные нелегким путем знания о принципах работы и правильном использовании целых чисел в C/C++. Помимо самих правил, я решил привести список распространенных заблуждений и сделать небольшое сравнение системы целочисленных типов в нескольких передовых языках. Все изложение строилось вокруг баланса между краткостью и полноценностью, чтобы не усложнять восприятие и при этом отчетливо передать важные детали.
Всякий раз, когда я читаю или пишу код на C/C++, мне приходится вспоминать и применять эти правила в тех или иных ситуациях, например при выборе подходящего типа для локальной переменной/элемента массива/поля структуры, при преобразовании типов, а также в любых арифметических операциях или сравнениях. Обратите внимание, что типы чисел с плавающей запятой мы затрагивать не будем, так как это большей частью относится к анализу и обработке ошибок аппроксимации, вызванных округлением. В противоположность этому, математика целых чисел лежит в основе как программирования, так и компьютерной науки в целом, и в теории вычисления здесь всегда точны (не считая проблем реализации вроде переполнения).
Целочисленные типы устанавливаются с помощью допустимой последовательности ключевых слов, взятых из набора {char, short, int, long, signed, unsigned}
.
Несмотря на то, что битовая ширина каждого базового целочисленного типа определяется реализацией (т.е. зависит от компилятора и платформы), стандартом закреплены следующие их свойства:
char
: минимум 8 бит в ширину;short
: минимум 16 бит и при этом не меньше char
;int
: минимум 16 бит и при этом не меньше short
;long
: минимум 32 бит и при этом не меньше int
;long long
: минимум 64 бит и при этом не меньше long
.сhar
может иметь знак или быть беззнаковым, что зависит от реализации.short
, int
, long
и long long
идут со знаком. Беззнаковыми их можно сделать, добавив ключевое слово unsigned
. signed
) int
в C, но (signed
или unsigned
) char
в C++. sizeof(char)
всегда равен 1, независимо от битовой ширины char
.char
, short
и int
, каждый шириной в 32 бита. int
может иметь ширину 36 бит.int
, signed
, signed int
, int signed
;short
, short int
, short signed
, short signed int
;unsigned long long
, long unsigned int long
, int long long unsigned
.
size_t [1]
(определен в stddef.h) является беззнаковым и содержит не менее 16 бит. При этом не гарантируется, что его ширина будет как минимум равна int
.ptrdiff_t [2]
(определен в stddef.h) является целочисленным типом со знаком. Вычитание двух указателей будет давать этот тип. При этом не стоит ожидать, что вычитание двух указателей даст int
.uint8_t
, int8_t
, 16
, 32
и 64
. Будьте внимательны к операциям, подразумевающим продвижение типов. Например, uint8_t + uint8_t
даст int
(со знаком и шириной не менее 16 бит), а не uint8_t
, как можно было предположить. Представим, что значение исходного целочисленного типа нужно преобразовать в значение целевого целочисленного типа. Такая ситуация может возникнуть при явном приведении, неявном приведении в процессе присваивания или при продвижении типов.
Как происходит преобразование?
Главный принцип в том, что, если целевой тип может содержать значение исходного типа, то это значение семантически сохраняется.
Говоря конкретнее:
signed char -> int
или unsigned short -> unsigned long
), каждое исходное значение после преобразования сохраняется.int
, содержащий значение в диапазоне [0, 255]
, будет без потерь преобразован в unsigned char
.В более точной форме эти правила звучат так:
-
, ~
.+
, *
, &
. <<
.bool
, char
или short
(как signed
, так и unsigned
), тогда он продвигается до int
(signed
), если int
может содержать все значения исходного типа. В противном случае он продвигается до unsigned int
. Процесс продвижения происходит без потерь. Примеры:
short
и 24-битный int
. Если переменные x
и y
имеют тип unsigned short
, то операцияx & y
продвигает оба операнда до signed int
. char
и 32-битный int
. Если переменные x
и y
имеют тип unsigned char
, то операцияx – y
продвигает оба операнда до unsigned int
.
int
, long
, long long
. Рангом общего типа считается старший ранг среди типов двух операндов. Если оба операнда являются signed/unsigned
, то их общий тип будет иметь ту же характеристику. Если же операнд с беззнаковым типом имеет старший или равный ранг по отношению ко второму операнду, то их общий тип будет беззнаковым. В случае, когда тип операнда со знаком может представлять все значения другого типа операнда, общий тип будет иметь знак. В противном случае общий тип получается беззнаковым. Примеры:
(long) + (long) → (long)
;(unsigned int) * (int) → (unsigned int)
;(unsigned long) / (int) → (unsigned long)
;int
является 32-битным, а long 64-битным: (unsigned int) % (long) → (long)
;int
и long
оба являются 32-битными: (unsigned int) % (long) → (unsigned long)
.
Знаковое переполнение:
UINT_MAX + 1 == 0
.uint16_t = unsigned short
, и int
равен 32-битам. Тогда uint16_t x=0xFFFF
, y=0xFFFF
, z=x*y
; x
и y
будут продвинуты до int
, и x * y
приведет к переполнению int
, вызвав неопределенное поведение.uint32_t = unsigned char
, и int
равен 33-битам. Тогда uint32_t x=0xFFFFFFFF
, y=0xFFFFFFFF
, z=x+y
; x
и y
будут продвинуты до int
, и x + y
приведет к переполнению int
, то есть неопределенному поведению.0U
, либо умножить на 1U
в качестве пустой операции. Например: 0U + x + y
или 1U * x * y
. Это гарантирует, что операнды будут продвинуты как минимум до ранга int
и при этом останутся без знаков.
Деление/остаток:
INT_MIN / -1
.Битовые сдвиги:
Предположим, что у нас есть массив, в котором нужно обработать каждый элемент последовательно. Длина массива хранится в переменной len
типа T0
. Как нужно объявить переменную счетчика цикла i
типа T1
?
uint8_t len = (...);
for (uint8_t i = 0; i < len; i++) { ... }
T1
будет работать верно, если диапазон T1
будет являться (не строго) надмножетсвом диапазона T0
. Например, если len
имеет тип uint16_t
, тогда отсчет с использованием signed long
(не менее 32 бит) сработает. len
типа int
гарантированно будет иметь значение в диапазоне [3,50]
(обусловленное логикой приложения), тогда допустимо отсчитывать цикл, используя char
без знака или со знаком (в котором однозначно можно представить диапазон [0,127]
).size_t len = (...); // Unsigned
for (int i = 0; i < len; i++) { ... }
Для циклов, ведущих отсчет вниз, более естественным будет использовать счетчик со знаком, потому что тогда можно написать:
for (int i = len - 1; i >= 0; i--) {
process(array[i]);
}
При этом для беззнакового счетчика код будет таким:
for (unsigned int i = len; i > 0; i--) {
process(array[i - 1]);
}
Примечание: сравнение i >= 0
имеет смысл только, когда i
является числом со знаком, но всегда будет давать true
, если оно будет беззнаковым. Поэтому, когда это выражение встречается в беззнаковом контексте, значит, автор кода скорее всего допустил ошибку в логике.
Все пункты приведенного ниже списка являются мифами. Не опирайтесь на эти ложные убеждения, если хотите писать корректный и портируемый код.
char
всегда равен 8 битам. int
всегда равен 32 битам.sizeof(T)
представляет число из 8-битных байтов (октетов), необходимых для хранения переменной типа T
. (Это утверждение ложно, потому что если, скажем, char
равняется 32 битам, тогда sizeof(T)
измеряется в 32-битных словах).int
в любой части программы и игнорировать более точные типы вроде size_t
, uint32_t
и т.д.INT_MAX + 1 == INT_MIN
).‘A’ == 65
. (Согласно EBCDIC это утверждение ложно).int
и обратно в указатель происходит без потерь.{указателя на один целочисленный тип}
в {указатель на другой целочисленный тип}
безопасно. Например, int *p (…); long *q = (long*)p;
. (см. каламбур типизации и строгий алиасинг).uint8_t x; uint8_t y; uint32_t z;
, тогда операция x + y
должна дать тип вроде uint8_t
, беззнаковый int
, или другой разумный вариант, а +z
по-прежнему будет uint32_t
. (Это не так, потому что при продвижении типов предпочтение отдается типам со знаком). int
может иметь 16, 32, 64 бита или другое их количество. Всегда нужно выбирать тип с достаточным диапазоном. Но иногда использование слишком обширного типа (например, необычного 128-битного int
) может вызвать сложности или даже внести уязвимости. Усугубляется это тем, что такие типы из стандартных библиотек, как size_t
, не имеют связи с другими типами вроде беззнакового int
или uint32_t
; стандарт позволяет им быть шире или уже.int
, продвигаются автоматически, вызывая труднопонимаемое поведение с диапазонами и переполнение. Когда операнды отличаются знаковостью и рангами, они преобразуются в общий тип способом, который зависит от определяемой реализацией битовой ширины. Например, выполнение арифметики над двумя операндами, как минимум один из которых имеет беззнаковый тип, приведет к преобразованию их обоих либо в знаковый, либо в беззнаковый тип в зависимости от реализации.add/sub/mul/div
, деление на нуль, битовые сдвиги. Не сложно создать такие условия неопределенного поведения по случайности, но сложно вызвать их намеренно или обнаружить при выполнении, равно как выявить их причины. Необходима повышенная внимательность и усилия для проектирования и реализации арифметического кода, исключающего переполнение/UB. Стоит учитывать, что в последствии становится сложно отследить и исправить код, при написании которого не соблюдались принципы защиты от переполнения/UB. signed
и unsigned
версии каждого целочисленного типа удваивает количество доступных вариантов. Это создает дополнительную умственную нагрузку, которая не особо оправдывается, так как типы со знаком способны выполнять практически все те же функции, что и беззнаковые. char
), числа со знаком должны находится в дополнительном коде, неявные преобразования допускают только их варианты без потерь, а вся арифметика и преобразования определяются точно и не вызывают неоднозначного поведения. Целочисленные типы в Java поддерживают быстрое вычисление и эффективное упаковывание массивов в сравнении с языками вроде Python, где есть только bigint
переменного размера. int
, особенно для перебора массивов. Это означает, что этот язык не может эффективно работать на малопроизводительных 16-битных ЦПУ (часто используемых во встраиваемых микроконтроллерах), а также не может непосредственно работать с большими массивами в 64-битных системах. К сравнению, C/C++ позволяет писать код, эффективно работающий на 16, 32 и/или 64-битных ЦПУ, но при этом требует от программиста особой осторожности.signed bigint
. В сравнении с C/C++ это сводит на нет все рассуждения на тему битовой ширины, знаковости и преобразований, так как во всем коде правит один тип. Тем не менее за это приходится платить низкой скоростью выполнения и несогласованным потреблением памяти.float64
(double
в C/C++). Из-за этого битовая ширина и числовой диапазон оказываются фиксированными, числа всегда имеют знаки, преобразования отсутствуют, а переполнение считается нормальным.
Автор: Дмитрий Брайт
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/363220
Ссылки в тексте:
[1] size_t: https://en.cppreference.com/w/c/types/size_t
[2] ptrdiff_t: https://en.cppreference.com/w/c/types/ptrdiff_t
[3] stdint.h: https://en.cppreference.com/w/c/types/integer
[4] мышление: http://www.braintools.ru
[5] Wikipedia: C data types: https://en.wikipedia.org/wiki/C_data_types
[6] cppreference.com: C++ — Fundamental types: https://en.cppreference.com/w/cpp/language/types
[7] cppreference.com: C — Implicit conversions — Integer conversions: https://en.cppreference.com/w/c/language/conversion#Integer_conversions
[8] cppreference.com: C — Implicit conversions — Usual arithmetic conversions: https://en.cppreference.com/w/c/language/conversion#Usual_arithmetic_conversions
[9] C in a Nutshell: Chapter 4. Type Conversions: https://www.oreilly.com/library/view/c-in-a/0596006977/ch04.html
[10] Stack Overflow: Implicit type promotion rules: https://stackoverflow.com/questions/46073295/implicit-type-promotion-rules
[11] Stack Overflow: 32 bit unsigned multiply on 64 bit causing undefined behavior?: https://stackoverflow.com/questions/27001604/32-bit-unsigned-multiply-on-64-bit-causing-undefined-behavior
[12] Stack Overflow: What’s the best C++ way to multiply unsigned integers modularly safely?: https://stackoverflow.com/questions/24795651/whats-the-best-c-way-to-multiply-unsigned-integers-modularly-safely
[13] Stack Overflow: Is masking before unsigned left shift in C/C++ too paranoid?: https://stackoverflow.com/questions/39964651/is-masking-before-unsigned-left-shift-in-c-c-too-paranoid
[14] Image: http://ruvds.com/ru-rub?utm_source=habr&utm_medium=perevod&utm_campaign=bright-translate&utm_content=svod_pravil_po_rabote_s_celymi_chislami_v_c#order
[15] Источник: https://habr.com/ru/post/551216/?utm_source=habrahabr&utm_medium=rss&utm_campaign=551216
Нажмите здесь для печати.