Embedded HandbookПриложения
Все разделы › IX. Язык C: от основ до продвинутого
STM32F103, STM32F407, STM32G431 · уровень базовый · 12 мин чтения
Открыть в интерактивном справочнике

67. Указатели и адресная арифметика

Указатель — переменная, в которой лежит адрес. Ничего больше. Но на микроконтроллере это главный инструмент языка: регистры периферии — это адреса (раздел 3.3), буферы передаются в функции адресом, таблица векторов прерываний — массив адресов функций.

Раздел про то, как указатели устроены, почему p + 1 прибавляет не единицу, что происходит с массивом при передаче в функцию и какие указатели опасны. Всё, что сказано о шагах и смещениях, снято с настоящего кода компилятора.

Как это называется в документации

Оригинал Перевод
pointer указатель
address-of operator & взятие адреса
dereference, indirection * разыменование
pointer arithmetic адресная арифметика
array decay превращение массива в указатель
null pointer нулевой указатель
dangling pointer висячий указатель
wild, uninitialized pointer неинициализированный указатель
pointer to const, const pointer указатель на константу, константный указатель
function pointer, callback указатель на функцию, функция обратного вызова
vector table таблица векторов
memory-mapped register регистр, отображённый в память

67.1 Указатель — это адрес

Две операции:

int n = 0;
int *p = &n;        /* p хранит адрес n */
*p = 10;            /* записать 10 туда, куда указывает p: теперь n = 10 */

Звёздочка в объявлении и звёздочка в выражении — разные вещи. В int *p она говорит «p — указатель на int». В *p = 10 — «взять то, на что указывает p». Путаница пропадает, если читать объявление так: «*p — это int».

Указатель, который никуда не указывает, по договорённости равен нулю — NULL. Его проверяют перед использованием.

Размер указателя не зависит от того, на что он указывает. На Cortex-M любой указатель — на байт, на double, на структуру регистров, на функцию — занимает 4 байта. На 64-разрядном компьютере — 8 (раздел 64.3).

Указатель в окне памяти отладчика: p хранит адрес n, b смотрит на word побайтно


67.2 Зачем они нужны


67.3 Адресная арифметика

К указателю можно прибавить целое число — и тут первая неожиданность: p + 1 сдвигает адрес не на один байт, а на размер того, на что p указывает. Вот что компилятор делает на самом деле:

Выражение Машинный код Сдвиг адреса
p + 1, p — uint8_t * adds r0, #1 1 байт
p + 1, p — uint16_t * adds r0, #2 2 байта
p + 1, p — uint32_t * adds r0, #4 4 байта
p + 3, p — uint32_t * adds r0, #12 12 байт

Это не причуда, а смысл: p + 1 — это следующий элемент, а не следующий байт.

Так же работает разность указателей: она считается в элементах. Регистры CRL и ODR порта F1 лежат на расстоянии 12 байт, и

&GPIOC->ODR - &GPIOC->CRL                         /* 3 — три регистра */
(uint8_t *)&GPIOC->ODR - (uint8_t *)&GPIOC->CRL   /* 12 — байт */

Компилятор посчитал обе разности заранее и вставил в код готовые 3 и 12. Чтобы получить байты, указатели приводят к указателю на байт. Строго говоря, CRL и ODR — поля структуры, а не элементы одного массива, и по стандарту такая разность не определена (правило ниже): компилятор посчитал верно, но гарантии нет. Законный способ — offsetof(GPIO_TypeDef, ODR) - offsetof(GPIO_TypeDef, CRL), те же 12 байт.

Допустимые операции: прибавить и отнять целое, ++ и --, разность двух указателей в одном массиве, сравнение. Складывать два указателя смысла нет, и компилятор этого не позволит.

p + 1 — это следующий элемент: сдвиг на 1, 2 или 4 байта, смотря на что указывает p


67.4 Массив и указатель

Имя массива в выражении превращается в адрес его нулевого элемента. Отсюда тождество, на котором стоит вся работа с массивами:

a[i]   ≡   *(a + i)

Квадратные скобки — просто сокращённая запись адресной арифметики. Это тождество настолько буквально, что 2[a] тоже компилируется и даёт то же, что a[2] — сложение переставимо. Писать так не надо, но понимать, почему это работает, полезно.

Несколько задач на понимание — с int a[5] = {10, 20, 30, 40, 50};:

int *pa = a;
*(pa + 2)               /* 30 — элемент с индексом 2 */
*pa + 2                 /* 12 — сначала *pa, потом + 2: приоритет */

int *pa1 = &a[1], *qa = &a[3];
qa - pa1                /* 2 — между ними два элемента */

pa = a + 2;
*pa++ = 13;             /* записать в a[2], потом сдвинуть pa: *pa теперь 40 */
                        /* массив: 10 20 13 40 50 */
++*pa;                  /* увеличить значение, а не указатель */

*pa++ читается как *(pa++): постфиксный ++ сильнее звёздочки, но возвращает старый адрес. Это самая частая запись в циклах копирования (раздел 68).

Отличие массива от указателя одно, но важное: массиву нельзя присвоить — a = b не скомпилируется. Имя массива — не переменная, а адрес, закреплённый за ним навсегда. Указатель же можно перенаправлять куда угодно.


67.5 Массив в функцию: размер теряется

Когда массив передают в функцию, передаётся только адрес его начала. Сколько там элементов, функция не знает — даже если параметр записан как int a[5]:

int count(int a[5]) { return sizeof(a) / sizeof(a[0]); }

Внутри функции a — это указатель, и sizeof(a) возвращает размер указателя. Функция «насчитает» на Cortex-M один элемент (4 байта указателя на 4 байта int), а на компьютере — два (8 на 4). Пять не получится нигде.

Компилятор здесь предупреждает без всяких ключей: «sizeof on array function parameter will return size of 'int *' instead of 'int[5]'».

Правильно — передавать размер отдельным параметром. Тогда одна функция обрабатывает и весь массив, и любую его часть:

int sum(const int *a, int size)
{
    int s = 0;
    for (int i = 0; i < size; i++)
        s += a[i];
    return s;
}

sum(a, 5);          /* 150 — весь массив */
sum(a, 2);          /* 30  — первые два */
sum(a + 2, 3);      /* 120 — с третьего до конца */

Размер массива там, где он объявлен, считают так: sizeof(a) / sizeof(a[0]) — это работает, пока a — настоящий массив, а не параметр.


67.6 const и указатели

const может относиться к двум разным вещам:

Объявление Что нельзя Что можно
const int *p менять значение через p: *p = 5 перенаправить p
int *const p перенаправить p: p = q менять значение *p
const int *const p ни то, ни другое только читать

Читают справа налево: int *const p — «p — константный указатель на int», const int *p — «p — указатель на int, который константен». Нарушение любого запрета — ошибка компиляции, а не предупреждение.

Практическое применение — параметры функций:

void uart_send(const uint8_t *data, uint16_t len);

const здесь — обещание: функция данные не изменит. Компилятор проследит за обещанием, а заодно в такую функцию можно без предупреждений передать таблицу из Flash (раздел 3.6). Неконстантный параметр её тоже примет, но с предупреждением о потере const, и если функция попробует писать, запись во Flash не пройдёт или вызовет отказ.


67.7 Указатель на указатель

Указатель — тоже переменная, у неё тоже есть адрес:

int a = 77;
int *pa = &a;
int **ppa = &pa;

*pa = 88;           /* a = 88 */
**ppa = 99;         /* a = 99: через два адреса к тому же месту */

Нужен он, когда функция должна изменить сам указатель: сдвинуть позицию разбора в строке, вернуть адрес выделенного буфера. По той же логике, что и в разделе 66.3: чтобы изменить переменную, передают её адрес — а если переменная сама указатель, получается указатель на указатель. Встретится он и в аргументах командной строки (раздел 71).


67.8 Указатель на функцию

Функция тоже лежит в памяти, и её адрес можно хранить в переменной:

typedef int (*op_t)(int);           /* тип: указатель на функцию int → int */

int twice(int x)  { return 2 * x; }
int square(int x) { return x * x; }
int negate(int x) { return -x; }

op_t ops[3] = { twice, square, negate };
ops[1](7);                          /* 49 — вызов по номеру */

Без typedef запись читается тяжело — int (*ops[3])(int), — поэтому тип почти всегда дают через typedef. Скобки вокруг *op_t обязательны: без них получилось бы объявление функции, возвращающей указатель.

Применения на микроконтроллере:

И самое важное применение уже есть в каждом проекте. Таблица векторов прерываний — это массив адресов функций. Первое слово таблицы в стартовом файле F103 — начальное значение указателя стека, второе — адрес Reset_Handler, дальше — адреса обработчиков, по слову на каждый. Прерывание для ядра — это «взять слово из таблицы по номеру и перейти по этому адресу» (раздел 7). А если обработчик не написан, в таблице стоит адрес обработчика по умолчанию — того самого перехода на себя (раздел 65.9).

Все эти адреса нечётные: единица в младшем бите говорит ядру, что код — в режиме Thumb, а другого режима у Cortex-M нет. В стенде раздела 7в Reset_Handler лежит с адреса 0x08000040, а в таблице стоит 0x08000041; так же выглядит и указатель на функцию, взятый в C, — у good_func там же 0x080000D7. Переход по чётному адресу функции — отказ INVSTATE (раздел 7в.1).


67.9 Байты через указатель

Указатель на байт позволяет посмотреть на любое значение побайтно:

uint32_t word = 0x11223344;
uint8_t *b = (uint8_t *)&word;
/* b[0] = 44, b[1] = 33, b[2] = 22, b[3] = 11 */

Младший байт лежит первым — это порядок байтов Cortex-M и компьютера, разобранный в разделе 3а.8. Так разбирают пакеты и собирают числа из принятых байтов.

Одна оговорка. Смотреть на что угодно через указатель на байт язык разрешает всегда. А вот читать, скажем, float через указатель на uint32_t формально запрещено: оптимизатор вправе считать, что указатели разных типов не смотрят в одно место, и переставить обращения. Для такого превращения используют memcpy — компилятор сворачивает его в одну команду.


67.10 Опасные указатели

Неинициализированный. Локальный указатель без начального значения содержит мусор — случайный адрес. Запись по нему портит случайную память. Компилятор замечает это только с -Wall.

Висячий. Указатель на то, чего уже нет:

int *make(void) { int x = 5; return &x; }   /* x исчезнет при выходе */

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

Нулевой. На компьютере чтение по нулевому адресу мгновенно роняет программу — операционная система защищает память. На микроконтроллере такой защиты по умолчанию нет. На STM32 по нулевому адресу видна копия начала Flash (раздел 11.7), и чтение через нулевой указатель ничего не уронит: программа получит начальное значение стека из таблицы векторов и продолжит работу с мусором. Ошибка, которая на компьютере видна сразу, на камне тихо живёт месяцами. Так не у всех: при старте из системной памяти по нулю лежит загрузчик, а у H7 с адреса 0 начинается ОЗУ ITCM, и запись через NULL молча его портит. Поймать такие обращения можно областью MPU без доступа в начале адресного пространства (раздел 11а).

За пределами карты памяти. Обращение по адресу, за которым нет ни памяти, ни периферии, вызывает отказ процессора (раздел 35.2). Это, пожалуй, самый честный исход: по регистрам отказа видно, откуда было обращение.

За пределами массива. Индекс никто не проверяет: a[5] при пяти элементах прочитает то, что лежит в памяти следом. На камне там обычно другие переменные — и они молча портятся.


67.11 Грабли

Функция считает размер массива неправильно. sizeof от параметра — это размер указателя. Размер передают отдельным параметром.

*p++ увеличил не то. Постфиксный ++ относится к указателю; чтобы увеличить значение — (*p)++ или ++*p.

p + 1 перескочил несколько байт. Так и задумано: шаг — размер элемента.

Разность указателей «в четыре раза меньше». Она в элементах; для байтов приводят к uint8_t *.

Нельзя присвоить массив. Имя массива — не переменная. Копируют поэлементно или memcpy.

Запись через указатель испортила соседнюю переменную. Выход за границу массива или неинициализированный указатель.

Функция вернула указатель, а данные «поплыли». Висячий указатель на локальную переменную.

Нулевой указатель не вызвал ошибки. На STM32 по нулевому адресу читается Flash.

Программа ушла в отказ при обращении к переменной. Указатель смотрит за пределы карты памяти.

Предупреждение о потере const при передаче таблицы из Flash. Параметр объявлен без const: функция вправе писать туда, где лежит Flash.

Прерывание не вызывает обработчик. В таблице векторов адрес обработчика по умолчанию: имя функции написано не так, как в стартовом файле.


67.12 Итоги

Дальше: раздел 68 «Массивы и строки».

Открыть в интерактивном справочнике