Стандартная библиотека
Стандартная библиотека — это коллекция наиболее часто используемых алгоритмов: работа с файлами, строками и т. д. Однако описывать ее, пожалуй, нет никакой необходимости. Во-первых, для встраиваемых систем она отличается (урезана, см. Newlib) от библиотеки для персональных компьютеров, а попытки прикрутить оригинальную вызовут ряд проблем, так как модель памяти устроена иначе. Во-вторых, как правило, ее использование в микроконтроллерной технике пытаются свести к нулю. Дело в том, что память, где хранится прошивка, довольно ценный ресурс, которого часто не хватает, а функции и все зависимости бывают довольно «тяжелыми». Зачастую приходится и вовсе писать свою реализацию алгоритма, опираясь на аппаратные возможности целевой платформы, вместо использования готовой из стандартной библиотеки. Тем не менее, мы рассмотрим наиболее важные функции, связанные с выделением и распределением памяти.
Ранее мы отметили, что работа с плавающей запятой — это не одна инструкция МК, и поэтому по возможности от вещественных чисел стараются отойти. Функция
printf(...)
из стандартной библиотеки, которую можно применять для отладки программы, использует в своей работе плавающую запятую. Поэтому данная функция значительно увеличит размер прошивки и будет отрабатывать долго.
Рассмотренные переменные и массивы располагались в стеке. Размер массивов в стеке определяется на этапе компиляции, и он не может быть изменен в процессе выполнения программы. К тому же максимальный размер массива в стеке меньше максимального размера массива в куче.
В языке Си за выделением и освобождением памяти должен следить сам программист с помощью четырех функций (определены в <stdlib.h>
):
malloc()
— название говорит само за себя (с англ. memory allocation) — данная функция выделяет (или нет!) запрашиваемое количество байт памяти и возвращает указатель на первый байт области;calloc()
— функция смежного выделения (англ. contiguous allocation), выделяет (или нет!) пространство для массива, инициализируя элементы нулями, и возвращает указатель;realloc()
— позволяет изменить размер (или нет!) выделенной ранее области функциейmalloc()
;free()
— освобождает ранее выделенную память.
В случае, если в памяти нет линейного свободного участка запрашиваемой длины, то функции выделения вернут так называемый null
-pointer, т.е. указатель на NULL
(обычно 0, но ничего по нулевому адресу находиться не может).
Мы уже говорили о таком явлении, как переполнение стека: по аналогии можно получить переполнение кучи (англ. heap overflow), если забывать освобождать выделенную память. Приведем пример:
void crashing_function(uint8_t x) {
uint8_t *y = malloc(x);
}
Внутри crashing_function()
выделяется память под x
байт, но по завершении работы она не освобождается. Вызывая функцию снова и снова, вы рано или поздно заполните всю память, что приведет к печальным последствиям.
Обратите внимание, malloc()
(и calloc()
тоже) возвращает указатель на тип void
, который необходимо привести (англ. casting) к нужному типу.
ptr = (uint32_t*) malloc(size);
Вкупе с malloc()
часто применяют другую функцию из стандартной библиотеки — sizeof()
. Если передать в качестве параметра тип данных, она вернет его размер в байтах. То есть для выделения памяти под 10 элементов uint32_t
данные записи будут эквивалентны (но вторая более понятна):
malloc(40);
malloc(10 * sizeof(uint32_t));
Единственное отличие calloc()
от malloc()
заключается в том, что она гарантировано выделит столько памяти, сколько нужно под n
элементов заданного типа, и проинициализирует их нулями, а не оставит это на откуп разработчику.
ptr = (uint32_t*)calloc(20, sizeof(uint32_t));
Данной строчкой мы выделили 20 элементов uint32_t
. По сути calloc()
сама производит умножение. Допустим, однако, что выделенного участка памяти не хватает — тогда память можно перераспределить с помощью realloc()
.
ptr = realloc(ptr, new_size);
Размер можно как уменьшить (часть данных будет отброшена), так и увеличить (если линейного участка не хватает после ранее выделенного, то данные копируются на новое место). Если в качестве размера будет передан 0
, то такая запись будет эквивалентна освобождению памяти, т.е. функции free()
, задача которой — подчищать «продукты жизнедеятельности» malloc()
/ calloc()
. Обратите внимание, данная функция не работает «сама по себе», это не сборщик мусора (англ. garbage collector) в Java: память, которую необходимо освободить, нужно указывать явным образом.
free(pr);
Приведем более сложный пример:
#include <stdlib.h>
uint32_t get_sum(void) {
uint32_t sum = 0;
uint32_t *ptr = 0;
uint32_t num = read_data(); // get buffer size
ptr = (uint32_t) malloc(num * sizeof(uint32_t));
if (ptr == NULL)
return 0; // can't create
for (uint32_t i = 0; i < num; i++) {
*(ptr + i) = read_data(); // read data from UART
sum += *(ptr + i);
}
free(ptr);
return sum;
}
При работе с calloc()
освобождение производится аналогичным образом.
При работе с многомерными массивами нужно не забывать вычищать всю память. Если вы создадите массив как один блок (через malloc()
), то никаких проблем не будет, а вот в случае с массивом массивов начинающие программисты, освобождая память под массив указателей, чаще всего забывают про сами элементы массива.
#define N 10
#define M 20
// matrix N x M
uint32_t **matrix = calloc(N, sizeof(uint32_t));
if (matrix) {
for (uint32_t i = 0; i < N; i++) {
matrix[i] = calloc(M, sizeof(uint32_t));
}
}
// some actions here
// ...
// release the memory
for (uint32_t i = 0; i < N; i++)
free(matrix[i]);
free(matrix)
Ряд полезных функций для работы с массивами (как правило, символьными) можно найти в заголовочном файле <string.h>
. Например, для копирования одного массива в другой можно прибегнуть к помощи функции memcpy()
. Ниже приведен ее прототип из стандартной библиотеки, предоставленной компанией IAR:
void* memcpy(void *dst, const void *src, size_t);
Со строками и функциям из <string.h>
следует быть поаккуратнее. В Си есть негласное правило, что все строки заканчиваются символом окончания, \0
. Передавая указатель в memcpy()
мы не указываем длину массива! она вычисляется автоматически по наличию символа \0
. Создадим два массива по 5 символов, один как строку, второй как обычный массив.
uint8_t str[] = "12345";
uint8_t arr[] = { '1', '2', '3', '4', '5' };
С первого взгляда массивы должны быть одинаковой длины, но в действительности это не так:
printf("%ld\n\r", sizeof(str)); // 6
printf("%ld\n\r", sizeof(arr)); // 5
Компилятор автоматически добавляет \0
к str
. Теперь вызовем функцию из <string.h>
для подсчёта длины для каждого из массивов.
printf("%ld\n\r", strlen(str)); // 5
printf("%ld\n\r", strlen(arr)); // dangerous
Со второй строчкой в лучшем случае вы получите не понятное число, в худшем залезете в несуществующую память и упадёте с HardFault
. Функция strlen()
будет итерироваться по памяти до тех пор пока не найдёт символ \0
.
uint32_t len(uint8_t* str) {
uint32_t len = 0;
while(*(str++))
len++;
return len;
}
Ознакомиться с остальными функциями вы можете самостоятельно.
Форматированный вывод
Любой текст в Си это массив символов (char
— целочисленное восьмибитное число). Каждому символу ставится в соответствие код согласно таблице ASCII. Кроме печатных символов, таких как буквы и цифры, присутствуют и непечатные. Так как для вывода непечатного символа его нужно напечатать, пришлось пойти на хитрость, кодировать символ двумя начиная со спецсимвола, обратного слеша, \
. Для того что бы вывести сам обратный слеш его стоит напечатать два раза
Стандартом не оговорено знаковое оно или беззнаковое, во избежание проблем лучше использовать
uint8_t
.
printf("\\");
// \
Часть непечатных символов помогают в форматировании текста, например перевод каретки (англ. carrige) на новую строку, \n
. Понятие каретки архаизм доставшийся в наследство от печатной машинки. Для удобства восприятия пробелы заменены _
.
printf("C for");
printf("embedded systems");
// C forembedded systems
printf("C for\n");
printf("embedded systems");
// C for
// _____embedded systems
Мало, однако, перевести каретку на новую строку, её ещё нужно вернуть в начало строки, \r
, по аналогии всё с той же печатной машинкой. Дзынь.
printf("C for\n\r");
printf("embedded systems\n\r");
// C for
// embedded systems
Ещё один полезный символ — это табуляций, \t
. По сути поле текста разбивается на равные «юниты» по 2, 4, или 8 символов (в зависимости от компилятора/настроек операционной системы). Допустим что один таб равен четырём пробелам, тогда:
printf("a\t: 10\n\r");
printf("ab\t: 11\n\r");
printf("abc\t: 12\n\r");
// a___: 10
// ab__: 10
// abc_: 10
Чуть выше, рассматривая переменное количество аргументов функции, мы описывали модуль логирования, который использовал стандартные механизмы форматирования printf()
и sprintf()
. Один из аргументов функции — строка с маркерами, на чьи места подставляются значение остальных аргументов. Так как каждый тип имеет собственное представление в памяти, маркеры для каждого из них отличаются.
Маркер | Примечания |
---|---|
%c | Символ (character) |
%d | Целочисленное (decimal) |
%e | Вещественное в экспоненциальной форме (exponential floating-point) |
%f | Вещественное число (floating-point) |
%g | Вещественное в общем виде (general-format floating-point) |
%i | Целочисленное с основанием 10 (decimal) |
%o | Целочисленное с основанием 8 (octal) |
%s | Строка, должна оканчиваться на \0 (string) |
%u | Беззнаковое целочисленное(unsigned integer) |
%x | Целочисленное с основанием 16 (hexidecimal) |
Для вывода знака процента можно написать %%
или \%
.
С выводом числа есть одна проблема — %d
, например, возьмёт места столько, сколько ему нужно для вывода числа, для 7
— один символ, для 42
— два символа и т.д. Но что, если мы хотим вывести форматированную таблицу? К %d
можно добавить опцию длины, но стоит помнить, что она гарантирует что выведенное число займёт минимум N символов в строке, пустые места будут заполнены пробелами.
printf("%d\n\r", 42); // 42
printf("%5d\n\r", 42); // ___42
printf("%5d\n\r", 123456789); // 123456789
Если вместо заполнения пробелами вам нужно заполнение нулями, например при выводе в hex-формате, то перед числом следует написать 0
.
printf("0x%02x\n\r", 15); // 0x0F
Вместо нуля может быть почти любой другой символ. Плюс и минус работают по-другому. Для выравнивания числа по по левому краю, а не по правому, перед числом нужно поставить знак минус.
printf("%5d.\n\r", -42); // __-42.
printf("%-5d.\n\r", -42); // -42__.
К отрицательному числу автоматически добавляется минус, но такое же поведение можно задать неотрицательному числу (включая ноль), добавив символ +
перед опцией ширины.
printf("%+5d.\n\r", 42); // __+42
printf("%5d.\n\r", -42); // __-42
Вместо знака плюса, можно ставить пробел:
printf("% -5d.\n\r", 42); // _42__
printf("% -5d.\n\r", -42); // -42__
Для вывода чисел с плавающей запятой приведённые выше опции так же работают, но добавляются и другие. Например, если нам нужно ограничиться выводом сотых, нужно написать точку и цифру 2. При чём обратите внимание число будет округлятся в большую сторону.
float pi = 3.1415926;
printf("%.0f\n\r", pi); // 3
printf("%.1f\n\r", pi); // 3.1
printf("%.2f\n\r", pi); // 3.14
printf("%.3f\n\r", pi); // 3.142
printf("%.4f\n\r", pi); // 3.1416
С выводом вещественного числа есть один нюанс. Запись %5.2f
может привести вас к мысли, что 5 символов отводится на целую часть и 2 на дробную. Это не так.
printf("%5.2f\n\r", pi); // _3.14