Стандартная библиотека

Стандартная библиотека — это коллекция наиболее часто используемых алгоритмов: работа с файлами, строками и т. д. Однако описывать ее, пожалуй, нет никакой необходимости. Во-первых, для встраиваемых систем она отличается (урезана, см. 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

Изменено: