View Categories

Типы данных

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

Читать и писать данные размером в 1 бит тоже можно, но не "стандартными" средствами - об этом поговорим в следующих уроках

Размеры стандартных типов данных следуют за степенью двойки - 1, 2, 4 и 8 байт или 8, 16, 32 и 64 бит соответственно. Технически возможно хранить данные любого другого размера, например с целью экономии места, но опять же нестандартными средствами и менее эффективно.

Логический тип bool #

В C++ есть логический тип bool, который может хранить только 1 и 0, или же специальные константы true (правда, 1) и false (ложь, 0). Несмотря на свою двоичность, bool занимает 1 байт, то есть 8 бит. Этот тип добавлен для удобства работы с логическими выражениями и величинами.

В языке C этого типа нет, вместо него используют обычный целочисленный тип и костыли

Целочисленные типы #

Диапазон допустимых значений целочисленного типа зависит от его размера и поддержки отрицательных чисел. Чем больше места занимает тип - тем больше уникальных значений он может хранить, это количество равно 2^n, где n - размер типа в количестве бит. По сути это - количество возможных комбинаций нулей и единиц при заданном количестве бит. Например 3 бита - 2^3 = 8 возможных комбинаций: 000, 001, 010, 011, 100, 101, 110, 111. Или 8 бит: 2^8 = 256 комбинаций.

Беззнаковые числа #

Так как данные хранятся в двоичном виде, то они уже представляют собой число в диапазоне от 0 до 2^n - 1, так как счёт начинается с нуля. То есть 8 бит тип может хранить числа от 0 до 255, 16 бит - от 0 до 65535 и так далее по аналогии. Типы данных, которые хранят только положительные числа, называются беззнаковыми (unsigned).

Что будет, если превысить допустимое значение? Давайте возьмём ячейку 1 байт (8 бит) и посчитаем вверх с шагом 1 по обычным правилам сложения, начиная с числа 0b11111110 (254 DEC):

0b11111110  // 254
0b11111111  // 255
0b00000000  // 0 - 0b100000000, но старший разряд не уместится!
0b00000001  // 1

Всё верно, значение обнулится, так как старший разряд хранить уже негде - мы рассматриваем ячейку с размером 8 бит. В таком случае говорят, что ячейка переполнилась сверху.

При уменьшении числа это произойдёт в обратном порядке - из 0b00000000 получится 0b11111111, то есть ячейка переполнится снизу. Таким образом в рамках 1 байтного типа данных 255 + 1 = 0, а 0 - 1 = 255.

Знаковые числа #

Как хранить в памяти отрицательное число? Чтобы не потерять знак числа, нужно его тоже где-то хранить. У двоичных числел знак хранится в старшем бите: 0 - положительное число, 1 - отрицательное. Таким образом, на хранение самого числа остаётся на 1 бит меньше места и диапазон значений делится пополам - от -2^(n-1) до 2^(n-1) - 1. Например для 8 бит получится диапазон десятичных чисел от -128 до 127. Типы данных с поддержкой знака числа называются знаковыми (signed).

Давайте теперь посчитаем от числа 3 вниз с шагом 1 по обычным правилам вычитания, в ячейке 8 бит:

0b00000011  // 3
0b00000010  // 2
0b00000001  // 1
0b00000000  // 0
0b11111111  // -1
0b11111110  // -2

Данные у нас ограничены одним байтом, поэтому при переходе через 0 пришлось "добавить" слева старший разряд 1 и сразу его убрать, т.е. 100000000 - 1 = 11111111 - ячейка переполнилась снизу.

Вот и появилась слева единица, которая отвечает за знак числа! Да, старший бит не просто отвечает за знак, он получается автоматически, то есть число хранится ровно так, как оно и представлено в двоичной системе. Именно поэтому компилятору нужно конкретно указывать, как интерпретировать данные - как беззнаковое или как знаковое число, потому что одни и те же данные будут иметь разное значение: 0b11111110 как беззнаковое - это десятичное 254, а как знаковое - десятичное -2.

Математически такой тип хранения называется дополнительным кодом: чтобы взять положительное число с отрицательным знаком, нужно:

  • Поставить старший бит в 1
  • Инвертировать остальные биты (заменить 0 на 1 и 1 на 0)
  • Прибавить к ним 1

Например число 3 = 0b00000011 -> 0b11111100 -> 11111101 = -3. Как можно увидеть из примера с вычитанием выше - числа подчиняются этой формуле.

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

Endianness #

Данные хранятся в памяти побайтно, например если тип данных имеет размер 4 байта - значение будет разбито на 4 байта и записано в память друг за другом. Возьмём число 0x12345678 - имеем в памяти 4 соседних байта со значениями 0x12, 0x34, 0x56 и 0x78. В каком порядке они расположены? Это зависит от платформы, на которой выполняется код. Существует два варианта:

  • big-endian - старший байт числа хранится по меньшему адресу в памяти, то есть если представить, что адрес увеличивается слева направо, то наше число будет храниться так же, как записано: 12 34 56 78
  • little-endian - младший байт числа хранится по меньшему адресу в памяти, то есть наоборот: 78 56 34 12

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

Тип int #

Базовый целочисленный тип данных - int (integer). Размер этого типа зависит от платформы, на которой компилируется код, и может быть 2 или 4 байта.

Например на AVR это 2 байта, на ESP и STM32 - 4 байта

По умолчанию int - это знаковый тип, то есть поддерживает отрицательные числа. У int есть модификаторы:

  • short - занимать 2 байта
  • long - занимать 4 байта
  • unsigned - беззнаковый - не поддерживать отрицательные числа
  • signed - знаковый - поддерживать отрицательные числа

Из этих модификаторов, как из конструктора, можно собрать себе нужный целочисленный тип. Также модификаторы и сами по себе работают как самостоятельные типы, например unsigned будет размером с int, но беззнаковым. Я приведу все примеры для наглядности, потому что в чужом коде можно встретить всякое и нужно быть к этому готовым:

  • int, signed, signed int - синонимы int, знаковый
  • unsigned, unsigned int - беззнаковый int
  • short, signed short, signed short int - 2 байта знаковый
  • unsigned short, unsigned short int - 2 байта беззнаковый
  • long, signed long, signed long int - 4 байта знаковый
  • unsigned long, unsigned long int - 4 байта беззнаковый
  • long long, long long int, signed long long, signed long long int - 8 байт знаковый
  • unsigned long long, unsigned long long int - 8 байт беззнаковый

Довольно запутанная история!

Тип char #

Тип char весит 1 байт и хранит код символа из стандартной таблицы символов ASCII, подробнее об этом поговорим в уроке про строки. Тип char поддерживает модификаторы знака:

  • signed char - 1 байт знаковый
  • unsigned char - 1 байт беззнаковый

В отличие от signed int, который является синонимом int, signed char - отдельный тип данных! Таким образом в нём можно хранить целые числа размером до 1 байт, что позволяет сэкономить место.

Короткие названия #

К счастью, у целочисленных типов есть удобные короткие названия, которые сразу отражают знаковость и размер: intN_t - знаковые, uintN_t - беззнаковые, где N - размер в количестве бит: 8, 16, 32 или 64. Компилятор подставляет вместо этих типов стандартные типы в зависимости от платформы, на которой компилируется код (например вместо uint8_t подставит unsigned char). Я использую короткие названия, потому что:

  • Название более короткое в большинстве случаев
  • Проще ориентироваться в максимальных значениях
  • Проще изменить один тип на другой
  • Размер переменной задан конкретно и не зависит от платформы

Для использования коротких названий нужно подключить в код стандартный файл #include <stdint.h>

Тип Вес, байт От До
bool 1 0, false 1, true
int8_t (signed char) 1 -128 127
uint8_t (unsigned char) 1 0 255
int 2 или 4
int16_t (short) 2 -32 768 32 767
uint16_t (unsigned short) 2 0 65 535
int32_t (long) 4 -2 147 483 648 2 147 483 647
uint32_t (unsigned long) 4 0 4 294 967 295
int64_t (long long) 8 -9 223 372 036 854 775 808 9 223 372 036 854 775 807
uint64_t (unsigned long long) 8 0 18 446 744 073 709 551 615

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

  • UINT8_MAX
  • INT8_MAX
  • UINT16_MAX
  • INT16_MAX
  • UINT32_MAX
  • INT32_MAX
  • UINT64_MAX
  • INT64_MAX

Для использования этих констант на некоторых платформах нужно подключить в код стандартный файл #include <stdint.h> или #include <limits>

Тип size_t #

Есть ещё целочисленный беззнаковый тип данных size_t (размер), вес которого зависит от платформы, на которой выполняется код, и ограничен максимальным размером данных, которые могут существовать в программе (размер другого типа данных, массива, максимальный адрес в памяти). Например на МК AVR это 2 байта, на ESP - 4 байта.

size_t обычно используется там, где подразумевается размер данных, количество байт - для читаемости кода. Оператор sizeof (о нём ниже) также возвращает тип данных size_t.

Вещественные числа #

Вещественные числа, они же десятичные дроби - числа, имеющие целую и дробную части. Во многих вычислениях удобно использовать дробные числа, так как не всегда работа идёт с целыми числами, банальный пример - температура воздуха. В C/C++ есть поддержка таких чисел, называются они числами с плавающей точкой (floating point). Подробно рассматривать хранение таких в памяти чисел мы не будем, оно довольно непростое. Достаточно знать, что число с плавающей точкой хранится не в явном виде, а "зашифровано" - хранится в экспоненциальном представлении согласно стандарту IEEE 754.

Поддержка таких чисел и операций с ними может быть аппаратной на уровне устройства процессора, если у него есть блок FPU (Floating Point Unit) - тогда работа с такими числами выполняется очень быстро и не утяжеляет программу. Если FPU отсутствует - компилятор автоматически будет использовать встроенную программную реализацию - она медленная и занимает много памяти, например около 1.5 КБ на контроллерах AVR.

Данные с плавающей точкой представлены типами float - обычной и double - двойной точности:

Тип Вес, байт Точность, знаков Диапазон значений
float 4 ~7 -3.4E+38… 3.4E+38
double 8* ~16 -1.7E+308.. 1.7E+308

* - на некоторых платформах, например AVR, double имеет вес 4 байта и полностью равноценен float

Есть ещё тип long double, его вес тоже зависит от компилятора и конкретной архитектуры. Обычно те же 8 байт, как у long

Формат записи #

В C/C++ поддерживается несколько форматов записи чисел с плавающей точкой:

Тип записи Пример Чему равно
Десятичная дробь 20.5 20.5
Научный 2.34E5 2.34*10^5 или 234000
Инженерный 67e-12 67*10^-12 или 0.000000000067

После десятичной точки может не быть цифры - число 12. корретно и равносильно 12.0

Точность #

Из таблицы выше можно заметить, что диапазон возможных значений очень широкий - благодаря хранению в экспоненциальном виде. В то же время это ограничивает возможную точность хранения чисел: примерно 7 знаков для float и 16 - для полноценного double. Из этого можно сделать важный вывод - в память запишется не ровно то число, которое мы указали, а близкое к нему в пределах точности. Пример для float, где в комментарии указано реальное значение переменной:

v = 123456.654321;    // 123456.656250
v = 0.0123456789;     // 0.0123456788
v = 0.0000123456789;  // 0.0000123456788
v = 123456789;        // 123456792.0

Обратите внимание на количество старших разрядов, которые совпадают с записанным числом - оно +- совпадает с точностью из таблицы выше.

NaN и inf #

Помимо численного значения данные с плавающей точкой могут иметь ещё два специальных - NaN и inf:

  • NaN - дословно - "не число" (Not a Number), ошибка данных, которая могла произойти в результате вычисления или присвоена вручную при помощи константы NAN
  • inf - бесконечно большое значение (infinity), которое могло получиться в результате вычислений (деление на 0) или присвоена вручную при помощи константы INFINITY

Текстовое представление этих значений можно получить, выведя переменную в текстовый формат для отладки или при печати в консоль. Чтобы определить в программе, имеет ли переменная одно из этих значений, есть соответствующие функции isnan() и isinf().

v = NAN;
isnan(v);   // true

v = INFINITY;
isinf(v);   // true

Размер типа #

Существует специальный оператор sizeof - он позволяет внутри программы узнать размер любой сущности, которая имеет вес, в том числе размер типа данных:

sizeof(int);    // 2 или 4 байта
sizeof(short);  // 2 байта
sizeof(char);   // 1 байт

Тип void #

В C/C++ есть специальный тип данных void (пустота) - служебный пустой тип данных. Нельзя создать переменную такого типа, но сам тип используется в некоторых особых случаях, о которых будет рассказано в следующих уроках.

Свой тип #

В C/C++ есть возможность создать тип данных со своим названием на основе существующего типа при помощи оператора typedef:

typedef int MyType;     // MyType - синоним для int

MyType var = 0;

Основные тезисы урока #

  • Минимальной адресуемой ячейкой памяти является 1 байт, т.е. 8 бит данных
  • Размеры стандартных типов данных идут по степени двойки - 1, 2, 4 и 8 байт
  • bool - логический тип, может хранить только 0 и 1, но занимает так же 1 байт
  • Unsigned - только положительные числа
  • Signed - поддержка отрицательных чисел
  • Данные типа с весом больше 1 байта хранятся в соседних байтах друг за другом
  • Big-endian - старший байт числа хранится по меньшему адресу в памяти
  • Little-endian младший байт числа хранится по меньшему адресу в памяти
  • Тип int занимает разный размер на разных платформах
  • Тип char является символьным, но в нём можно хранить и обычные числа
  • Типы float и double хранят числа с плавающей точкой
  • Хранение дробных чисел имеет конечную точность
  • Дробные числа имеют особые значения - NaN и inf
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх