64. Типы данных
Тип говорит компилятору три вещи: сколько байт занимает значение, как читать эти байты и какие операции над ними допустимы. Одни и те же четыре байта могут оказаться целым числом, числом с плавающей точкой или адресом — в памяти пометки нет, её несёт только тип (раздел 63.14).
Главное правило для микроконтроллера — брать типы с явной разрядностью
из stdint.h — уже объяснено в разделе 3.1, там же таблица размеров на
Cortex-M и ловушка знакового char. Здесь — то, что лежит под этим
правилом: откуда берутся размеры и диапазоны, как типы превращаются друг
в друга без спроса, почему 5 / 2 равно двум и что на самом деле
печатает printf.
Как это называется в документации
Оригинал Перевод object, variable объект, переменная declaration, definition объявление, определение initializer начальное значение identifier имя, идентификатор type specifier, qualifier спецификатор, квалификатор типа integer literal, suffix целая константа, суффикс implicit conversion неявное преобразование usual arithmetic conversions обычные арифметические преобразования cast явное приведение truncation toward zero отбрасывание дробной части, округление к нулю side effect побочный эффект sequence point точка следования undefined behavior неопределённое поведение format specifier спецификатор формата
64.1 Переменная: имя, адрес, значение
Переменная — это место в памяти, у которого есть имя, адрес и значение. Значение можно менять; новое записывается поверх старого, и старое пропадает.
int a; /* объявление: место выделено, значение не задано */
int five = 5; /* объявление с начальным значением */
float pi = 3.14f;
char c = 'A';
⚠️ Локальная переменная без начального значения содержит мусор — то, что осталось в этом месте стека. Глобальная и
static— гарантированно ноль: их обнуляет стартовый код доmain(раздел 11.2). Рассчитывать на ноль у локальной — одна из самых частых ошибок.
64.2 Имена
Имя может состоять из латинских букв, цифр и подчёркивания и не может
начинаться с цифры. Большие и маленькие буквы различаются: Speed и
speed — две разные переменные.
| Имя | Можно? | Почему |
|---|---|---|
super173, _my_string, ABCdf |
да | |
4you |
нет | начинается с цифры |
h&m, a*b |
нет | знаки операций |
Иван |
можно, но не стоит | gcc с 10-й версии и armclang примут, а не всякий инструмент и не всякий коллега |
[goodname] |
нет | скобки |
Отдельно: имена, которые начинаются с подчёркивания и заглавной буквы
или с двух подчёркиваний, зарезервированы за компилятором и
библиотекой. Их можно встретить в заголовках — __IO, _Bool, — но
своим переменным их давать не стоит: однажды совпадёт.
Как называть осмысленно — раздел 3.7.
64.3 Размер типа: гарантия и реальность
Стандарт языка не задаёт размеры базовых типов точно — он задаёт нижнюю границу. Всё, что сверх неё, решает платформа.
| Тип | Стандарт гарантирует не меньше | Cortex-M | Компьютер x86-64, Linux |
|---|---|---|---|
char |
8 бит | 1 байт | 1 |
short |
16 бит | 2 | 2 |
int |
16 бит | 4 | 4 |
long |
32 бита | 4 | 8 |
long long |
64 бита | 8 | 8 |
float |
4 | 4 | |
double |
8 | 8 | |
long double |
не меньше double |
8 | 16 |
указатель, size_t |
4 | 8 | |
_Bool |
1 | 1 |
В Windows на том же процессоре другая модель данных: long — 4 байта у
обоих компиляторов, Microsoft и MinGW, а long double у компилятора
Microsoft — 8. Выходит, long не переносится даже между компьютерами;
где нужна точная ширина, берут int32_t и int64_t.
Два вывода.
int не равен разрядности процессора. На 64-разрядном компьютере
int — 4 байта, как и на 32-разрядном Cortex-M. Гарантировано только
16 бит, и на 8-разрядных контроллерах int бывает именно таким. Код,
который молча рассчитывает на 32 бита, переносится с сюрпризами — отсюда
правило раздела 3.1.
long и указатель на компьютере вдвое длиннее. Программа, которая
хранит адрес в int или uint32_t или печатает size_t через %u, на
камне работает, а на компьютере теряет старшую половину. Для адреса в виде
числа есть uintptr_t из stdint.h, для печати size_t — %zu.
Узнать размер можно оператором sizeof: он вычисляется компилятором и
ничего не стоит во время работы.
64.4 Откуда берутся диапазоны
k бит дают 2ᵏ разных комбинаций. Для беззнакового типа это числа от
0 до 2ᵏ − 1, для знакового — в дополнительном коде (раздел 63.14) от
−2ᵏ⁻¹ до 2ᵏ⁻¹ − 1:
| Тип | Диапазон |
|---|---|
int8_t |
−128 … 127 |
uint8_t |
0 … 255 |
int16_t |
−32 768 … 32 767 |
uint16_t |
0 … 65 535 |
int32_t |
−2 147 483 648 … 2 147 483 647 |
uint64_t |
0 … 18 446 744 073 709 551 615 |
Почему −128, а не −127? Потому что нуль в дополнительном коде один,
и освободившаяся комбинация 1000 0000 достаётся ещё одному
отрицательному числу. Отрицательных оказывается на одно больше, чем
положительных.
В старых редакциях стандарта минимальный диапазон signed char был
записан как −127 … 127 — чтобы допустить машины с прямым кодом, где
нулей два. Ни одной такой машины среди микроконтроллеров нет, а в
последней редакции языка дополнительный код стал обязательным.
64.5 Константы тоже имеют тип
Это та часть, которую замечают последней. Число, написанное в тексте, — не «просто число»: у него есть тип, и он влияет на вычисление.
| Константа | Тип | Размер на Cortex-M |
|---|---|---|
3.14 |
double |
8 |
3.14f |
float |
4 |
'A' |
int |
4 |
100000 |
int |
4 |
3000000000 |
long long |
8 |
5u, 5ul |
unsigned, unsigned long |
4 |
Отсюда суффикс f: без него 0.1 — это double, и на Cortex-M4 всё
выражение уходит в медленную программную арифметику (раздел 3.1). С 0.5
компилятор при оптимизации обойдётся без неё — это число точно есть во float, —
но полагаться на такую удачу не стоит.
Символьная константа 'A' в C имеет тип int, а не char — это
историческая особенность языка. На практике она не мешает: код символа
просто оказывается целым числом.
Константа, которая не помещается в int, сама становится длиннее: три
миллиарда получили восьмибайтовый тип. Но это правило касается только
самой константы — результат выражения 100000 * 100000 останется
int и переполнится. Компилятор это заметит и предупредит, что вместо
десяти миллиардов получится 1 410 065 408 — столько выходит, если просто
отбросить старшие разряды. Но язык этого не обещает: переполнение знакового
— неопределённое поведение (раздел 123.3), и с переменными вместо констант
оптимизатор вправе считать, что его не бывает. Нужен 64-битный результат —
64-битным делают один из множителей: 100000LL * 100000.
64.6 Неявные преобразования
Когда в выражении встречаются разные типы, компилятор приводит их к общему — не спрашивая. Правила такие:
- Если один операнд вещественный, второй тоже становится вещественным.
- Типы короче
intрасширяются доint— это целочисленное продвижение, и его ловушки разобраны в разделе 3.5. - Если один операнд беззнаковый, а другой знаковый той же длины, знаковый становится беззнаковым.
Третье правило — источник самых странных результатов:
unsigned u = 5;
if (-10 < u) { ... } /* не выполнится: −10 стало 4 294 967 286 */
unsigned n = 50;
int s = -500;
int answer = s / n; /* 85 899 335, а не −10 */
В первой строке −10 превращается в беззнаковое 4 294 967 286, которое,
конечно, больше пяти. Во второй −500 становится 4 294 966 796, делится
на 50 и даёт 85 899 335. Ключ -Wsign-compare ловит только первый
случай — сравнение. Деление он пропускает молча; его замечает лишь
-Wsign-conversion, который предупреждает о каждой смене знаковости.
Правило из практики: в одном выражении — либо только знаковые, либо только беззнаковые.
Присваивание тоже преобразует. Значение приводится к типу левой части. И здесь самое важное: преобразование происходит после вычисления правой части.
float f;
f = 3 / 2; /* 1.0 — деление было целым, результат 1 превратился в 1.0 */
f = 3.0f / 2; /* 1.5 */
int a = 7;
f = a / 4; /* 1.00 */
f = 4 / a; /* 0.00 */
f = (float)a / 4; /* 1.75 */
То, что слева стоит float, на деление справа не влияет никак.
64.7 Явное приведение
Тип можно указать самому — именем типа в скобках: (float)a,
(uint8_t)x. Это нужно в двух случаях: когда неявное преобразование
даёт не то (как (float)a / 4 выше) и когда хочется показать читателю,
что потеря точности здесь осознанная.
Вещественное в целое не округляется, а отбрасывает дробную часть:
(int)3.99 — это 3, (int)−3.99 — это −3. Если нужно округление,
его делают явно: lroundf(x) или roundf(x) из math.h. Трюк «прибавить
половину и отбросить» годится только для неотрицательных:
(int)(-2.7f + 0.5f) даёт −2, а не −3 (раздел 69.5).
Приведение не спасает от переполнения задним числом. В
(int64_t)(a * b) умножение двух int32_t уже прошло в 32 битах и
переполнилось. Приводить нужно операнд: (int64_t)a * b — тогда
умножение идёт в 64 битах. Приведение к uint32_t на Cortex-M шире
ничего не делает: int там и так 32-битный, и (uint32_t)a * b теряет
те же старшие разряды.
64.8 Деление нацело и остаток
Для целых / даёт целую часть, % — остаток. Округление — к нулю,
а не вниз, и остаток получает знак делимого:
| Выражение | Результат |
|---|---|
27 / 5, 27 % 5 |
5 и 2 |
−27 / 5, −27 % 5 |
−5 и −2 |
−27 % −5 |
−2 |
27 % −5 |
2 |
−7 / 2, −7 % 2 |
−3 и −1 |
При этом всегда выполняется (a / b) * b + a % b == a.
Для отрицательных это не то, что ждёт математик: математическое «−7 на
2 — это −4 и остаток 1» здесь не работает. Отсюда ловушка при работе с
кольцевым буфером или углом: index % size при отрицательном index
даёт отрицательный результат.
Порядок действий в целых решает всё. 1 / 3 * 3 — это 0, потому что
1 / 3 уже ноль. А 1 * 3 / 3 — это 1. При масштабировании в
целочисленной арифметике сначала умножают, потом делят — следя, чтобы
не переполниться (раздел 3.5).
Деление и остаток на 10 разбирают число на цифры:
int n = 123;
int sum = n % 10 + (n / 10) % 10 + (n / 100) % 10; /* 3 + 2 + 1 = 6 */
Так же для 435 произведение цифр — 60, а для 102 — ноль.
64.9 Присваивание — это выражение
= в C — не команда, а операция, и у неё есть значение: то, что было
присвоено. Отсюда цепочки и приёмы вроде
a = b = 7; /* обе переменные — 7 */
x = (a = 3) + 1; /* a = 3, x = 4 */
Отсюда же знаменитая ошибка if (a = 5) вместо if (a == 5): условие
всегда истинно, а a испорчена (раздел 65).
Сокращённые формы a += b, a -= b, a *= b, a /= b, a %= b,
а также &=, |=, ^=, <<=, >>= означают a = a ⊕ b. Для регистров
это самая частая запись: GPIOA->ODR |= 1u << 5 (раздел 3а).
Инкремент и декремент бывают двух видов:
int x = 5, y = 5;
int pre = ++x; /* сначала увеличить, потом взять: pre = 6, x = 6 */
int post = y++; /* сначала взять, потом увеличить: post = 5, y = 6 */
64.10 Побочные эффекты и точки следования
Побочный эффект — это изменение памяти по ходу вычисления: присваивание,
++, --, вызов функции, которая что-то пишет. Проблема в том, что
порядок вычисления частей выражения не определён. Компилятор может
вычислить левую часть + раньше правой или наоборот.
Поэтому есть правило: между двумя точками следования одну переменную можно изменить не больше одного раза. Точки следования — это:
- конец полного выражения — точка с запятой;
&&,||и запятая — между левой и правой частью;? :— после условия;- вызов функции — после вычисления всех аргументов.
a = b++ + ++b; /* НЕЛЬЗЯ: b меняется дважды */
a = (c = 3) + (c = 2); /* НЕЛЬЗЯ: c меняется дважды */
printf("%d %d", i, i++); /* НЕЛЬЗЯ: чтение и изменение без точки следования */
Это не «непредсказуемый результат», а неопределённое поведение: стандарт не обещает ничего, и разные компиляторы или уровни оптимизации действительно дают разное. Такой код не исправляют — его переписывают в несколько строк.
И одна тонкость наоборот: sizeof не вычисляет своё выражение. После
sizeof(k++) значение k не изменится — компилятор посмотрел только на
тип.
64.11 Форматный вывод и ввод
printf и scanf описывают тип каждого значения спецификатором.
Компилятор не может проверить, что тип переменной ему соответствует, —
это ваша ответственность (хотя GCC-совместимые компиляторы
предупреждают, если строка формата записана прямо в вызове).
| Спецификатор | Тип |
|---|---|
%d, %i |
int |
%u |
unsigned |
%ld, %lu |
long, unsigned long |
%lld, %llu |
long long |
%zu |
size_t |
%x, %X |
беззнаковое в шестнадцатеричной |
%f, %e |
double (и float, он расширяется) |
%c |
символ |
%s |
строка |
%p |
указатель |
Полный вид — %[флаги][ширина][.точность][размер]тип. Что получается на
деле (вывод настоящего запуска, квадратные скобки показывают границы
поля):
| Вызов | Вывод |
|---|---|
printf("[%9d]", 1234) |
[ 1234] |
printf("[%f]", 123.4567f) |
[123.456703] |
printf("[%9.3f]", 123.4567f) |
[ 123.457] |
printf("[%e]", 123.4567f) |
[1.234567e+02] |
printf("[%10.2e]", 123.4567f) |
[ 1.23e+02] |
printf("[%x][%#x][%X]", 255, 255, 255) |
[ff][0xff][FF] |
printf("[%05d][%+d][%-5d]", 42, 5, 42) |
[00042][+5][42 ] |
Две строки заслуживают внимания. %f от float со значением 123,4567
печатает 123.456703 — хвост «03» и есть погрешность хранения: у
float около семи верных цифр (раздел 69). А %9.3f округляет
до 123.457, в отличие от приведения к int, которое отбрасывает.
scanf пишет в переменную, поэтому ему нужен адрес:
int a, b;
scanf("%d", &a); /* правильно */
scanf("%d", a); /* ошибка: передано значение, а не адрес */
scanf("%d%d", &a); /* ошибка: спецификаторов больше, чем адресов */
scanf("%f", &a); /* ошибка: %f для int */
Приводить типы внутри scanf нельзя — спецификатор обязан совпадать с
типом переменной.
На микроконтроллере printf работает после перенаправления вывода
(разделы 16.6 и 21.3). И учтите, что в урезанных вариантах библиотек
вывод вещественных чисел отключён для экономии Flash и включается
отдельной настройкой — иначе вместо числа будет пусто.
64.12 const и #define
Частый вопрос: чем отличается const int N = 10; от #define N 10?
#define N 10 |
const int N = 10; |
|
|---|---|---|
| что это | подстановка текста (раздел 63.4) | переменная, которую нельзя менять |
| тип | нет | есть, проверяется |
| адрес | нет | есть |
| видна в отладчике | нет | да |
| область видимости | до конца файла | как у переменной |
| размер массива вне функции | можно | нельзя по стандарту |
Последняя строка удивляет тех, кто пришёл из C++: в C const не
делает константу времени компиляции.
const int N = 10;
int table[N]; /* по стандарту нельзя: N не константное выражение */
И здесь компиляторы расходятся. GCC отказывается собирать: «variably
modified 'table' at file scope». armclang собирает, но предупреждает,
что свернул размер в константу как расширение, — и превращает это
в ошибку, если попросить строгое соответствие стандарту. Код, который
собирается только благодаря расширению, сломается при смене
компилятора.
С #define N 10 то же объявление собирается везде. Поэтому размеры массивов,
номера битов и адреса в C традиционно задают через #define, а
const берут для значений, которые нужны как переменные: таблицы во
Flash (раздел 3.6), параметры функций, которые функция обещает не
менять. Для целых констант есть и третий способ — enum { N = 10 };: это
настоящая константа времени компиляции, годится для размера массива и вне
функции, у неё есть область видимости, а отладчик обычно показывает её имя.
В C23 для того же появился constexpr.
64.13 Грабли
Локальная переменная «иногда» не ноль. Не задано начальное значение. Глобальные обнуляются, локальные — нет.
int на камне и на компьютере разного размера? Нет, оба по 4 байта.
Разные — long, указатели, size_t и long double.
f = 3 / 2 дало 1.0. Деление целое; тип левой части на него не
влияет. Нужно 3.0f / 2.
if (-10 < u) не срабатывает. Знаковое сравнивается с беззнаковым и
становится огромным числом.
Среднее отрицательных чисел округлилось не туда. Целое деление округляет к нулю: −7 / 2 — это −3.
(int)2.9999 дало 2. Приведение отбрасывает дробную часть.
Индекс по модулю ушёл в минус. % отрицательного числа
отрицательный.
b++ + ++b на двух компиляторах даёт разное. Неопределённое
поведение: одна переменная меняется дважды без точки следования.
printf печатает мусор. Спецификатор не совпадает с типом: %d для
long long, %u для size_t на компьютере.
scanf роняет программу. Передано значение вместо адреса.
printf("%f") на камне ничего не печатает. В урезанной библиотеке
вывод вещественных выключен.
Массив размером const int N собирается в Keil и не собирается в
GCC. В C это не константа времени компиляции; один компилятор
принимает её как расширение, другой — нет. #define подходит везде.
sizeof(void) дало единицу. Это расширение компилятора; по
стандарту так писать нельзя, и с ключом -pedantic компилятор
предупредит.
64.14 Итоги
- Тип задаёт размер, способ чтения байтов и допустимые операции; в памяти пометки о типе нет.
- Локальные переменные без начального значения содержат мусор, глобальные обнуляются.
- Стандарт гарантирует лишь нижнюю границу размера:
int— от 16 бит. На Cortex-M и на компьютереintодинаковый, аlong, указатели иsize_tразличаются вдвое. - Отрицательных чисел в знаковом типе на одно больше: у
int8_t−128 … 127. - У констант тоже есть тип:
3.14—double,'A'—int. - Знаковое вместе с беззнаковым становится беззнаковым:
−10 < 5u— ложь,−500 / 50u— 85 899 335. - Присваивание преобразует уже готовый результат:
f = 3 / 2— это 1.0. - Приведение к целому отбрасывает дробь; деление округляет к нулю, а остаток берёт знак делимого.
- Одну переменную нельзя менять дважды между точками следования — это неопределённое поведение.
- Спецификатор
printfиscanfобязан совпадать с типом;scanfнужен адрес. constв C — не константа времени компиляции: массив размеромconst intвне функции GCC не соберёт, аarmclangсоберёт лишь как расширение. Для размеров массивов нужен#define.
Дальше: раздел 65 «Ветвления и циклы».