Арнольд Роббинс - Linux программирование в примерах
Наконец, упомянем потоки (threads), которые представляют несколько цепочек исполнения в рамках единственного адресного пространства. Обычно у каждого потока имеется свой собственный стек, а также способ получения локальных данных потока, т.е. динамически выделяемых данных для персонального использования этим потоком. Мы больше не будем рассматривать в данной книге потоки, поскольку это является продвинутой темой.
3.2. Выделение памяти
Четыре библиотечные функции образуют основу управления динамической памятью С Мы опишем сначала их, затем последуют описания двух системных вызовов, поверх которых построены эти библиотечные функции. Библиотечные функции С, в свою очередь, обычно используются для реализации других выделяющих память библиотечных функций и операторов C++ new и delete.
Наконец, мы обсудим функцию, которую часто используют, но которую мы не рекомендуем использовать.
3.2.1. Библиотечные вызовы: malloc(), calloc(), realloc(), free()
Динамическую память выделяют с помощью функций malloc() или calloc(). Эти функции возвращают указатели на выделенную память. Когда у вас есть блок памяти определенного первоначального размера, вы можете изменить его размер с помощью функции realloc(). Динамическая память освобождается функцией free().
Отладка использования динамической памяти сама по себе является важной темой. Инструменты для этой цели мы обсудим в разделе 15.5.2 «Отладчики выделения памяти».
3.2.1.1. Исследование подробностей на языке С
Вот объявления функций из темы справки GNU/Linux malloc(3):
#include <stdlib.h> /* ISO С */
void *calloc(size_t nmemb, size_t size);
/* Выделить и инициализировать нулями */
void *malloc(size_t size);
/* Выделить без инициализации */
void free(void *ptr);
/* Освободить память */
void *realloc(void *ptr, size_t size);
/* Изменить размер выделенной памяти */
Функции выделения памяти возвращают тип void*. Это бестиповый или общий указатель, все, что с ним можно делать — это привести его к другому типу и назначить типизированному указателю. Примеры впереди.
Тип size_t является беззнаковым целым типом, который представляет размер памяти. Он используется для динамического выделения памяти, и далее в книге мы увидим множество примеров его использования. На большинстве современных систем size_t является unsigned long, но лучше явно использовать size_t вместо простого целого типа unsigned.
Тип ptrdiff_t используется для вычисления адреса в арифметике указателей, как в случае вычисления указателя в массиве:
#define MAXBUF ...
char *p;
char buf[MAXBUF];
ptrdiff_t where;
p = buf;
while (/* некоторое условие */) {
...
p += something;
...
where = p - buf; /* какой у нас индекс? */
}
Заголовочный файл <stdlib.h> объявляет множество стандартных библиотечных функций С и типов (таких, как size_t), он определяет также константу препроцессора NULL, которая представляет «нуль» или недействительный указатель. (Это нулевое значение, такое, как 0 или '((void*)0)'. Явное использование 0 относится к стилю С++; в С, однако, NULL является предпочтительным, мы находим его гораздо более читабельным для кода С.)
3.2.1.2. Начальное выделение памяти: malloc()
Сначала память выделяется с помощью malloc(). Передаваемое функции значение является общим числом затребованных байтов. Возвращаемое значение является указателем на вновь выделенную область памяти или NULL, если память выделить невозможно. В последнем случае для обозначения ошибки будет установлен errno. (errno является специальной переменной, которую системные вызовы и библиотечные функции устанавливают для указания произошедшей ошибки. Она описывается в разделе 4.3 «Определение ошибок».) Например, предположим, что мы хотим выделить переменное число некоторых структур. Код выглядит примерно так:
struct coord { /* 3D координаты */
int x, y, z;
} *coordinates;
unsigned int count; /* сколько нам нужно */
size_t amount; /* общий размер памяти */
/* ... как-нибудь определить нужное число... */
amount = count * sizeof(struct coord); /* сколько байт выделить */
coordinates = (struct coord*)malloc(amount); /* выделить память */
if (coordinates == NULL) {
/* сообщить об ошибке, восстановить или прервать */
}
/* ... использовать координаты... */
Представленные здесь шаги являются стереотипными. Порядок следующий:
1. Объявить указатель соответствующего типа для выделенной памяти.
2. Вычислить размер выделяемой памяти в байтах. Для этого нужно умножить число нужных объектов на размер каждого из них. Последний получается с помощью оператора С sizeof, который для этой цели и существует (наряду с другими). Таким образом, хотя размер определенной структуры среди различных компиляторов и архитектур может различаться, sizeof всегда возвращает верное значение, а исходный код остается правильным и переносимым.
При выделении массивов для строк символов или других данных типа char нет необходимости умножения на sizeof(char), поскольку последнее по определению всегда равно 1. Но в любом случае это не повредит.
3. Выделить память с помощью malloc(), присвоив возвращаемое функцией значение переменной указателя. Хорошей практикой является приведение возвращаемого malloc() значения к типу переменной, которой это значение присваивается. В С этого не требуется (хотя компилятор может выдать предупреждение). Мы настоятельно рекомендуем всегда приводить возвращаемое значение.
Обратите внимание, что на C++ присвоение знамения указателя одного типа указателю другого типа требует приведения типов, какой бы ни был контекст. Для управления динамической памятью программы C++ должны использовать new и delete, а не malloc() и free(), чтобы избежать проблем с типами.
4. Проверить возвращенное значение. Никогда не предполагайте, что выделение памяти было успешным. Если выделение памяти завершилось неудачей, malloc() возвращает NULL. Если вы используете значение без проверки, ваша программа может быть немедленно завершена из-за нарушения сегментации (segmentation violation), которое является попыткой использования памяти за пределами своего адресного пространства.
Если вы проверите возвращенное значение, вы можете по крайней мере выдать диагностическое сообщение и корректно завершить программу. Или можете попытаться использовать какой-нибудь другой способ восстановления.
Выделив блок памяти и установив в coordinates указатель на него, мы можем затем интерпретировать coordinates как массив, хотя он в действительности указатель:
int cur_x, cur_y, cur_z;
size_t an_index;
an_index = something;
cur_x = coordinates[an_index].x;
cur_y = coordinates[an_index].y;
cur_z = coordinates[an_index].z;
Компилятор создает корректный код для индексирования через указатель при получении доступа к членам структуры coordinates[an_index].
ЗАМЕЧАНИЕ. Блок памяти, возвращенный malloc(), не инициализирован. Он может содержать любой случайный мусор. Необходимо сразу же инициализировать память нужными значениями или хотя бы нулями. В последнем случае используйте функцию memset() (которая обсуждается в разделе 12.2 «Низкоуровневая память, функции memXXX()):
memset(coordinates, ' ', amount);
Другой возможностью является использование calloc(), которая вскоре будет описана.
Джефф Колье (Geoff Collyer) рекомендует следующую методику для выделения памяти:
some_type *pointer;
pointer = malloc(count * sizeof(*pointer));
Этот подход гарантирует, что malloc() выделит правильное количество памяти без необходимости смотреть объявление pointer. Если тип pointer впоследствии изменится, оператор sizeof автоматически гарантирует, что выделяемое число байтов остается правильным. (Методика Джеффа опускает приведение типов, которое мы только что обсуждали. Наличие там приведения типов также гарантирует диагностику, если тип pointer изменится, а вызов malloc() не будет обновлен.)
3.2.1.3. Освобождение памяти: free()
Когда вы завершили использование памяти, «верните ее обратно», используя функцию free(). Единственный аргумент является указателем, предварительно полученным с использованием другой функции выделения. Можно (хотя это бесполезно) передать функции free() пустой указатель: