Минимально адресуемым размером данных является, какправило, один байт (8 бит). Как правило - это значит, что не всегда, и бывают разные экзотические архитектуры, где "байт" - это 9 бит (PDP-10), или специализированные сигнальные процессоры с минимально адресуемым размером данных 16 бит (TMS32F28xx).
По стандарту языка Си определена константа CHAR_BIT
(в заголовочном файле <limits.h>
), для которой гарантируется, что CHAR_BIT >= 8
.
Тип данных, представляющий один байт, исторически называется "символ" - char
, который содержит ровно CHAR_BIT
количество бит.
Знаковость типа char
по стандарту не определена. Для
архитектуры x86 это знаковый тип данных, а, например,
для ARM - беззнаковый. Опции компилятора gcc -fsigned-char
и -funsigned-char
определяют это поведение.
Для остальных целочисленных типов данных: short
, int
,
long
, long long
, стандарт языка Си определяет минимальную разрядность:
Тип данных | Разрядность |
---|---|
short |
не менее 16 бит |
int |
не менее 16 бит, обычно 32 бит |
long |
не менее 32 бит |
long long |
не менее 64 бит, обычно 64 бит |
Таким образом, полагаться на количество разрядов в базовых
типах данных нельзя, и это нужно проверять с помощью
оператора sizeof
, который возвращает "количество байт", то
есть, в большинстве случает - сколько блоков размером
CHAR_BIT
помещается в типе данных.
С особой осторожностью нужно относиться к типу данных long
: на 64-разрядной системе Unix он является 64-битным, а, например, на 64-битной Windows - 32-битным. Поэтому, во избежание путаницы, использовать этот тип данных запрещено.
Перед целочисленными типами данных могут стоять модификаторы
unsigned
или signed
, которые указывают допустимость отрицательных чисел.
Для знаковых типов, старший бит определяет знак числа: значение 1
является признаком отрицательности.
Способ внутреннего представления отрицательных чисел не регламентирован стандартом языка Си, однако все современные компьютеры используют обратный дополнительный код. Более того, п.6.3.1.3.2 стандарта языка Си определяет способ приведения типов от знакового к беззнаковому таким способом, которые приводит к кодированию обратным дополнительным кодом.
Таким образом, значение -1
представляется как целое число,
все биты которого равны единице.
С точки зрения низкоуровневого программирования, и языка Си в частности, знаковость типов данных определяет только способ применения различных операций.
В заколовочных файлах файле <stdint.h>
(для Си99+) и
<cstdint>
(для C++11 и новее) определены типы данных, для
которых гарантируется фиксированное количесвто разрядов:
int8_t
, int16_t
, int32_t
, int64_t
, - для знаковых, и
uint8_t
, uint16_t
, uint32_t
, uint64_t
- для беззнаковых.
Ситуация целочисленного переполнения возникает, когда тип данных результата не имеет достаточно разрядов для того, чтобы хранить итоговый результат. Например, при сложении беззнаковых 8-разрядных целых чисел 255 и 1, получается результат, который не может быть представим 8-разрядным значением.
Для беззнаковых чисел ситуация переполнения является штатной, и эквивалентна операции "сложение по модулю".
Для знаковых типов данных - приводит к ситуации неопределенного поведения (Undefined Behaviour). В корректных программах такие ситуации встречаться не могут.
Пример:
int some_func(int x) {
return x+1 > x;
}
С точки зрения здравого смысла, такая программа должна всегда возвращать значение 1
(или true
), поскольку мы знаем, что x+1
всегда больше, чем x
. Компилятор может использовать этот факт для оптимизации кода, и всегда возвращать истинное значение. Таким образом, поведение программы зависит от того, какие опции оптимизации были использованы.
Свежие версии компиляторов clang
и gcc
(начиная с 6-й версии) умеют контролировать ситуации неопределенного поведения.
Можно включить генерацию управляемого кода программы, который использует дополнительные проверки во время выполнения. Естественно, это происходит ценой некоторого снижения производительности.
Такие инструменты называются ревизорами (sanitizers), предназначенными для разных целей.
Для включения ревизора, контролирующего ситуацию неопределенного поведения, используется опция -fsanitize=undefined
.
Целочисленное переполнение означает перенос старшего разряда, и многие процессоры, включая семейство x86, позволяют это диагностировать. Стандартами языков Си и C++ эта возможность не предусмотрена, однако компилятор gcc (начиная с 5-й версии) предоставляет нестандартные встроенные функции для выполнения операций с контролем переполнения.
// Операция сложения
bool __builtin_sadd_overflow (int a, int b, int *res);
bool __builtin_saddll_overflow (long long int a, long long int b, long long int *res);
bool __builtin_uadd_overflow (unsigned int a, unsigned int b, unsigned int *res);
bool __builtin_uaddl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res);
bool __builtin_uaddll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res);
// Операция вычитания
bool __builtin_ssub_overflow (int a, int b, int *res)
bool __builtin_ssubl_overflow (long int a, long int b, long int *res)
bool __builtin_ssubll_overflow (long long int a, long long int b, long long int *res)
bool __builtin_usub_overflow (unsigned int a, unsigned int b, unsigned int *res)
bool __builtin_usubl_overflow (unsigned long int a, unsigned long int b, unsigned long int *res)
bool __builtin_usubll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res)
// Операция умножения
bool __builtin_smul_overflow (int a, int b, int *res)
bool __builtin_smull_overflow (long int a, long int b, long int *res)
bool __builtin_smulll_overflow (long long int a, long long int b, long long int *res)
bool __builtin_umul_overflow (unsigned int a, unsigned int b, unsigned int *res)
bool __builtin_umull_overflow (unsigned long int a, unsigned long int b, unsigned long int *res)
bool __builtin_umulll_overflow (unsigned long long int a, unsigned long long int b, unsigned long long int *res)
Два основных типа вещественных с плавающей точкой, которые определены стандартом языка Си, - это float
(используется 4 байта для хранения) и double
(используется 8 байт).
Самый старший бит в представлении числа - это признак отрицательного значения. Далее, по старшинству бит, хранится значения смещенной экспоненциальной части (8 бит для float
или 11 бит для double
), а затем - значение мантиссы (23 или 52 бит).
Смещение экспоненциальной части необходимо для того, чтобы можно было в таком представлении хранить значения с отрицательной экспонентой. Смещение для типа float
равно 127
, для типа double
- 1023
.
Таким образом, итоговое значение может быть получено как:
Value = (-1)^S * 2^(E-B) * ( 1 + M / (2^M_bits - 1) )
где S
- бит знака, E
- значение смещенной экспоненты, B
- смещение (127 или 1023), а M
- значение мантиссы, M_bits
- количество бит в экспоненте.
Поразрядные операции относятся к целочисленной арифметике, и не предусмотрены для типов float
и double
. Таким образом, нужно сохранить вещественное число в памяти, и затем прочитать его, интерпретируя как целое число. В случае с языком C++ для этого предназначен оператор reinterpret_cast
. Для языка Си есть два способа: использовать аналог reinterpret_cast
- приведение указателей, либо использовать тип union
.
// У нас есть некоторое целое вещественное число, которое хранится в памяти
double a = 3.14159;
// Получаем указатель на это число
double* a_ptr_as_double = &a;
// Теряем информацию о типе, приведением его к типу void*
void* a_ptr_as_void = a_ptr_as_void;
// Указатель void* в языке Си можно присваивать любому указателю
uint64_t* a_ptr_as_uint = a_ptr_as_void;
// Ну а дальше просто разыменовываем указатель
uint64_t b = *a_as_uint;
Тип union
- это тип данных, который синтаксически очень похож на тип struct
, то есть там можно перечислить несколько именованных полей, но концептуально - это совершенно разные типы данных! Если в структуре или классе, для хранения каждого поля для предусмотрено отдельное место в памяти, то для union
этого не происходит, и все поля накладываются друг на друга при размещении в памяти.
Обычно тип union
используется в качестве вариантного типа данных (в С++ начиная с 17-го стандарта для этого предусмотрен std::variant
), но в качестве побочного эффекта - его удобно использовать приведения типов в стиле reinterpret_cast
, не используя при этом указатели.
// У нас есть некоторое целое вещественное число, которое хранится в памяти
double a = 3.14159;
// Используем тип union
typedef union {
double real_value;
uint64_t uint_value;
} real_or_uint;
real_or_uint u;
u.real_value = a;
uint64_t b = u.uint_value;
- Бесконечность:
E=0xFF...FF
,M=0
- Минус ноль (результат деления 1 на минус бесконечность):
S=1
,E=0
,M=0
- NaN (Not-a-Number):
S
- любое,E=0xFF...FF
,M <> 0
Некоторые процессоры, например архитектуры x86, поддерживают расширение стандарта, позволяющее более эффективно представлять множество чисел, значения которых близко к нулю. Такие числа называются денормализованными.
Признаком денормализованного числа является значение смещенной экспоненты E=0
. В этом случае, численное значение получается следующим образом:
Value = (-1)^S * ( M / (2^M_bits - 1) )
Некоторые процессоры, например Intel x86, различают два вида чисел NaN
- невалидное значение.
Значения sNaN
возникают при выполнении операций, которые сигнализируют об ошибке на уровне прерывания процессора. Например, деление на 0
. Обычно, чтобы получить такие значения, необходимо собирать программу с опцией -fno-signaling-nans
. Более подробно - см. FloatingPointMath - GCC Wiki
Пример битовой маски для типа double
на x86_64, определяющей значение sNaN
:
Номера битов 6 5 4 3 2 1 0
3210987654321098765432109876543210987654321098765432109876543210
----------------------------------------------------------------
Значения 0111111111110100000000000000000000000000000000000000000000000000
----------------------------------------------------------------
Регион SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
В отличии от sNaN
, значения Quiet NaN, которые получаются в результате вычислений, не приводят к прерыванию процессора и вызове обработчика исплючительной ситуации.
Примером является попытка сложить +inf
и -inf
.
Пример битовой маски для типа double
на x86_64, определяющей значение qNaN
:
Номера битов 6 5 4 3 2 1 0
3210987654321098765432109876543210987654321098765432109876543210
----------------------------------------------------------------
Значения 0111111111111000000000000000000000000000000000000000000000000000
----------------------------------------------------------------
Регион SEEEEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM