Лекция №3. Типы данных, переменные
Измерение информации
Прежде чем перейти к переменным и их типам, нужно вспомнить школьный курс информатики, а именно - как хранятся данные в "цифровом" мире. Любая память состоит из элементарных ячеек, которые имеют всего два состояния: 0 и 1. Эта единица информации называется бит (bit). Минимальным блоком памяти, к которому можно обратиться из программы по имени или адресу, является байт (byte), который в Arduino (и в большинстве других платформ и процессоров) состоит из 8 бит, таким образом любой тип данных будет кратен 1 байту.
Максимальное количество значений, которое можно записать в один байт, составляет 2^8 = 256. В программировании счёт всегда начинается с нуля, поэтому один байт может хранить число от 0 до 255. Более подробно о двоичном представлении информации и битовых операциях мы поговорим в отдельном уроке.
Стандартные типы переменных в Arduino по своему размеру кратны степени двойки, давайте их распишем:
1 байт = 8 бит = 256
2 байта = 16 бит = 65 536
4 байта = 32 бита = 4 294 967 296
Типы данных
Переменные разных типов имеют разные особенности и позволяют хранить числа в разных диапазонах.
Название | Альт. название | Вес | Диапазон | Особенность |
---|---|---|---|---|
boolean | bool | 1 байт * | 0 или 1, true или false | Логический тип |
char | - | 1 байт | -128… 127 (AVR), 0.. 255 (ESP) | Символ (код символа) из таблицы ASCII |
int8_t | - | 1 байт | -128… 127 | Целые числа |
byte | uint8_t | 1 байт | 0… 255 | Целые числа |
int | int16_t, short | 2 байта | -32 768… 32 767 | Целые числа (На ESP8266/ESP32 - 4 байта!) |
unsigned int | uint16_t, word | 2 байта | 0… 65 535 | Целые числа (На ESP8266/ESP32 - 4 байта!) |
long | int32_t | 4 байта | -2 147 483 648… 2 147 483 647 | Целые числа |
unsigned long | uint32_t | 4 байта | 0… 4 294 967 295 | Целые числа |
float | - | 4 байта | -3.4E+38… 3.4E+38 | Числа с плавающей точкой, точность: 6-7 знаков |
double | - | 4/8 байт | -1.7E+308… 1.7E+308 | Для AVR то же самое, что float. На ESP и 32-битных МК - 8 байт, точность: 15-16 знаков |
int64_t | - | 8 байт *** | -(2^64)/2... (2^64)/2-1 | Целые числа |
uint64_t | - | 8 байт *** | 2^64-1 | Целые числа |
Таблица 1. Типы данных
(*) - да, bool занимает 1 байт (8 бит), так как это минимальная адресуемая ячейка памяти. Есть способы запаковать логические переменные в 1 бит, о них поговорим в другом уроке.
(**) - на ESP8266/ESP32 int и unsigned int занимает 4 байта, то есть является аналогами типов long и unsigned long!
(***) - Компилятор также поддерживает 64 битные числа. Стандартные Arduino-библиотеки с переменными этого типа не работают, поэтому можно использовать только в своём коде.
Целочисленные типы
Переменные целочисленных типов нужны для хранения целых чисел. В своей программе рекомендуется использовать альтернативное название типов (второй столбец в таблице выше), потому что:
Проще ориентироваться в максимальных значениях
Легче запомнить
Название более короткое
Проще изменить один тип на другой
Размер переменной задан жёстко и не зависит от платформы (например int на AVR это 2 байта, а на esp8266 - 4 байта)
Максимальные значения хранятся в константах, которые можно использовать в коде. Иногда это помогает избавиться от лишних вычислений:
UINT8_MAX - 255
INT8_MAX - 127
UINT16_MAX - 65 535
INT16_MAX - 32 767
UINT32_MAX- 4 294 967 295
INT32_MAX - 2 147 483 647
UINT64_MAX - 18 446 744 073 709 551 615
INT64_MAX -
Логический тип
bool - логический, он же булевый (придуман Джорджем Булем) тип данных, принимает значения 0 и 1 или false и true - ложь и правда. Используется для хранения состояний, например включено/выключено, а также для работы в условных конструкциях.
Также переменная типа bool принимает значение true, если присвоить ей любое отличное от нуля число.
bool a = 0; // false
bool b = 1; // true
bool c = 25; // true
Символьный тип
char - тип данных для хранения символов, символ указывается в одинарных кавычках: char var = 'a';. По факту это целочисленный тип данных, а переменная хранит номер (код) символа в таблице ASCII:

Рисунок 5. Таблица кодирования
Отдельный символьный тип данных нужен для удобства работы, чтобы программа могла понять разницу между числом и символом, например для вывода на дисплей (чтобы вывести именно букву A, а не число 65). Из символов можно составлять строки, об этом более подробно поговорим в уроках про символьные строки и String-строки.
Символы и числа
Несмотря на то, что в языке Си символ это по сути целое число, значения например '3' и 3 не равны между собой, потому что символ '3' с точки зрения программы является числом 51. На практике иногда бывает нужно конвертировать символы чисел в соответствующие им целые числа и наоборот (при работе со строками и буферами вручную), для этого распространены следующие алгоритмы:
Из символа в число - взять младший ниббл (4 бита): symbol & 0xF
Из символа в число - вычесть символ 0: symbol - '0'
Из числа в символ - прибавить символ 0: symbol + '0'
Дробные числа
Float (англ. float - плавающий) - тип данных для чисел с плавающей точкой, т.е. десятичных дробей. Arduino поддерживает три типа ввода чисел с плавающей точкой:
Тип записи | Пример | Чему равно |
---|---|---|
Десятичная дробь | 20.5 | 20.5 |
Научный | 2.34E5 | 2.34 * 105 или 234000 |
Инженерный | 67e-12 | 67 * 10-12 или 0.000000000067 |
Таблица 2. Дробные числа
Выше в таблице есть пометка "точность: 6-7 знаков" - это означает, что в этом типе можно хранить числа, размер которых не больше 6-7 цифр, остальные цифры будут утеряны! Причём целой части отдаётся приоритет. Вот так это выглядит в числах (в комментарии - реальное число, которое записалось в переменную):
float v;
v = 123456.654321; // 123456.656250
v = 0.0123456789; // 0.0123456788
v = 0.0000123456789; // 0.0000123456788
v = 123456789; // 123456792.0
Объявление и инициализация
Объявление переменной - резервирование ячейки памяти указанного типа на имя: тип_данных имя;
Присваивание - задание переменной значения при помощи оператора = (равно): имя = значение;
Инициализация переменной - объявление и присваивание начального значения: тип_данных имя = значение;
Можно объявить и инициализировать несколько переменных через запятую:
byte myVal;
int sensorRead = 10;
byte val1, val2, val3 = 10;
Переменная должна быть объявлена до использования, буквально выше по коду. Иначе вы получите ошибку Not declared in this scope - переменная не объявлена.
Нельзя объявить две и более переменных с одинаковым именем в одной области определения.
Константы
Что такое константа понятно из её названия - что-то, значение чего мы можем только прочитать и не можем изменить: при попытке изменить получим ошибку компиляции. Задать константу можно двумя способами:
Как переменную, указав перед типом данных слово const: const тип_данных имя = значение;. Пример: const byte myConst = 10;. По сути это будет обычная переменная, но её значение нельзя поменять. Особенности:
Занимает место в оперативной памяти, но может быть оптимизирована (вырезана) компилятором, если используется просто как значение.
Имеет адрес в памяти, по которому к ней можно обратиться.
Вычисления с ней не оптимизируются и чаще всего выполняются точно так же, как с обычными переменными.
Компилятор выдаст ошибку, если имя константы совпадает с именем другой переменной в программе.
При помощи директивы #define, без знака равенства и точки с запятой в конце: #define имя значение. Пример: #define BTN_PIN 10. Работает так: указанное имя буквально заменяется в тексте программы на указанное значение. Такая дефайн-константа:
Не занимает места в оперативной памяти, а хранится во Flash памяти как часть кода программы.
Не имеет адреса в оперативной памяти.
Вычисления с такими константами оптимизируются и выполняются быстрее, так как это просто цифры.
Если имя дефайн-константы совпадёт с именем другого "объекта" в программе или даже в библиотеке - работа может быть непредсказуемой: можно получить невнятную ошибку компиляции, либо программа может просто работать некорректно! Дефайн буквально заменяет текст в коде программы, это довольно опасная штука.
Статические переменные
Вспомним, как работает обычная локальная переменная: при входе в свой блок кода локальная переменная создаётся заново, а при выходе - удаляется из памяти и теряет своё значение. Если локальная переменная объявлена как static - она будет сохранять своё значение на всём протяжении работы программы, но область видимости останется локальной: взаимодействовать с переменной можно будет только внутри блока кода, где она создана (и во всех вложенных в него).
void setup() {
}
void loop() {
byte varL = 0;
varL++;
static byte varS = 0;
varS++;
// здесь varL всегда будет равна 1
// а varS - постоянно увеличиваться
}
Статические переменные позволяют более красиво организовывать свой код, избавляясь от лишних глобальных переменных.
Преобразование типов
Иногда требуется преобразовать один тип данных в другой: например, функция принимает int, а вы хотите передать ей byte. В большинстве случаев компилятор сам разберётся и преобразует byte в int, но иногда вылетает ошибка в стиле "попытка передать byte туда, где ждут int". В таком случае можно преобразовать тип данных, для этого достаточно указать нужный тип данных в скобках перед преобразуемой переменной (тип_данных)переменная, иногда можно встретить запись тип_данных(переменная). Результат вернёт переменную с новым типом данных, сам же тип данной у переменной не изменится. Например:
// переменная типа byte
byte val = 10;
// передаём какой-то функции, которая ожидает int
sendVal( (int)val );
И всё! val будет обрабатываться как int, а не как byte.