Часто требуется обрабатывать пачки однородных данных. Например, для осреднения измерений АЦП, преобразования их с помощью Фурье для получения спектра, для обработки изображений как набора точек, обработки звука (набор амплитуд), файлов (как набор байт), строки и всякое такое.
И чтобы их представлять и обрабатывать есть две штуки в языке: массивы и указатели.
Начнём с простого. Массив – это когда мы размещаем в памяти целую кучу однородных переменных одну за другой. Одну, две, десять, сто, тысячу, миллион – сколько памяти хватит и сколько нам надо.
uint8_t array[10]; // Так записывается массив из 10 элементов uint8_t
uint32_t array2[10]; // То же самое, но элементы другого типа.
читать дальшеПо сути, каждый элемент массива – отдельная переменная. Они никак друг с другом не связаны.
В качестве типа может быть всё то же, что и для обычных переменных – численные типы, структуры и всё, что угодно. Компилятор выделит под массив ровно столько памяти, сколько надо: array займёт, скорее всего, 10 байт (10 элементов по 1 байту), а array2 – 40 байт (10 элементов по 4 байта).
После того, как массивы объявлены, их можно использовать в программе:
array[1] = 10; // Поместить в 1 элемент массива число 10
Тут ещё такое дело, что элементы нумеруются с нуля =) Последний элемент имеет номер, на один меньший, чем общее количество элементов. То есть для array и array2 этот номер 9 (10 - 1).
array[0] = 0;
array[2] = array[1] + 1;
array2[9] = array[9] * 2;
// А вот так уже делать совсем не здорово:
array[10] = 0;
// Компилятор ругаться не будет – по синтаксису тут всё хорошо.
// Но этот элемент уже за пределами массива.
// Опять же, объясню чуть попозже, чем это грозит.
// Заполнить массив нулями:
for(i = 0; i < 10; i++) array[i] = 0;
Вот, как-то так. Попробую ближе к концу объяснить, как такое получается, что первый элемент имеет индекс 0, когда упомяну про указатели.
Дальше… Указатели – это такая хорошая штука, которая указывает (как неожиданно) куда-то в память. По сути, это переменная с адресом чего-то. Например, другой переменной.
Объявление выглядит так: <тип, на что указываем> * <название>;
uint8_t * pointer; // Объявили указатель на переменную типа uint8_t
// Если нам надо указатель на указатель, то так и пишем, не стесняясь:
uint8_t ** pointer_to_pointer;
// Указатель на указатель на переменную типа uint8_t.
// То есть указатель на переменную типа (uint8_t *).
// Только скобочки не обязательны.
Размер переменной-указателя зависит от платформы, для которой пишем код. Например, в AVR это 2 байта, для PC x86 и ARM это 4 байта, для PC x64 это 8 байт.
Массив – это не тип! Не надо указывать в объявлении указателя, что указываем на массив. Указателю пофиг, на что указывать.
Например:
pointer = &array[0]; // Теперь он указывает на начало массива array
Значок амперсанда является операцией получения адреса переменной. Почти все переменные находятся в памяти и, естественно, у них у всех есть свой адрес.
Так. Имея указатель на что-то, можно это что-то читать или записывать:
*pointer = 10; // По адресу, хранящемуся в pointer, запишем 10.
// То есть в array[0], так как сейчас pointer указывает на него.
Звёздочка сообщает, что мы работаем с переменной, адрес которой хранится в переменной, к которой этот оператор применяется (Оо).
Читается точно так же:
uint8_t a = *pointer; // Прочитали то, что записано по адресу...
Другой способ, аки доступ к массиву:
pointer[0] = 20; // В array[0] сидит теперь 20
Массив – это как раз по типу указатель и есть. Просто объявление массива ещё и выделяет память для него.
Адрес, который хранится в массиве, – это, по сути, обычное число. Например, 0x80123010. Потому можно применять к нему различного рода математические операции.
Начнём с простых и общеупотребительных:
Сложение. <указатель> + число (pointer + 3). Обратите внимание, что число трактуется как количество элементов, на сколько надо сдвинуть адрес! Если размер переменной 4 байта, а прибавляем к указателю три, то адрес увеличится на 12 (3 * 4 – три элемента по три байта).
Примеры:
pointer += 2; // Теперь он будет указывать на array[2],
// вне зависимости от того, какого типа array бы ни был.
// Компилятор адрес посчитает правильно.
pointer++; // Перейти на следующий элемент.
// То есть теперь указатель показывает на array[3].
Вычитание аналогично:
pointer -= 2; // Аналогично
pointer--;
Умножение и деление обычно прямо к указателю не применяют – это не имеет смысла, хотя и не запрещено.
Вот, как раз теперь можно показать, почему массивы нумеруются с нуля. Оператор [] может быть представлен через указатели таким образом:
// array[index] и *(array + index) — одно и то же.
А так как array уже указывает на начало массива уже, то, естественно, прибавлять к адресу ничего не нужно: array[0] = *array.
Отсюда же следует и то, почему не стоит переходить за границы массива.
uint16_t myarray[20]; // Какой-то массив
uint32_t myvar; // Какая-то переменная
Первая запись говорит компилятору о том, что нужно в памяти выделить массив на 20 элементов типа uint16_t и создать указатель myarray типа (uint16_t *), который, правда, не может менять адрес, на который указывает. Указателю всё равно, на что указывать и какого размера массив был создан!
Легко написать такой код:
for(i = 0; i < 30; i++) myarray[i] = 123;
При этом он честно запишет 123 в 20 элементов массива и ещё 10 элементов вне этого массива, где, скорей всего, будут располагаться какие-то другие переменные, как, например, myvar! Указатель о них ну ничего не знает. И поназаписывает, как если бы там продолжался массив. И хорошим это никак не закончится – программа обязательно где-то налажает, так как данные уже побиты.
Потому указателями надо пользоваться осторожно.
Пример копирования массивов через указатели:
// Два массива
uint16_t array1[10];
uint16_t array2[10];
// Указатели
uint16_t * p1;
uint16_t * p2;
// Счётчик
int i;
// Заполним массив array1 номерами элементов
for(i = 0; i < 10; i++) array1[i] = i;
// Скопируем поэлементно
for(i = 0; i < 10; i++)
{
array2[i] = array1[i];
}
// Сначала присвоим указателям адреса начала массивов
// Крутим, пока не дойдём до конца массива array1
// Прибавляем по 1 элементу к указателям
for(p1 = &array1[0], p2 = &array2[0]; p1 < &array1[10]; p1++, p2++)
{
*p2 = *p1;
}
// Иначе:
for(p1 = &array1[0], p2 = &array2[0]; p1 < &array1[10])
{
*p2++ = *p1++;
}
// В итоге у нас будет два одинаково заполненных массива