netlib.narod.ru | < Назад | Оглавление | Далее > |
Если p есть указатель на некоторый элемент массива, то p++ увеличивает p так, чтобы он указывал на следующий элемент, а p += i увеличивает его, чтобы он указывал на i-й элемент после того, на который указывал ранее. Эти и подобные конструкции — самые простые примеры арифметики над указателями, называемой также адресной арифметикой.
Си последователен и единообразен в своем подходе к адресной арифметике. Это соединение в одном языке указателей, массивов и адресной арифметики — одна из сильных его сторон. Проиллюстрируем сказанное построением простого распределителя памяти, состоящего из двух программ. Первая, alloc(n), возвращает указатель p на n последовательно расположенных ячеек типа char. Программа, обращающаяся к alloc, может использовать эти ячейки для запоминания символов. Вторая, afree(p), освобождает память для того, чтобы ее можно было снова использовать. Простота алгоритма обусловлена предположением, что обращения к afree делаются в обратном порядке по отношению к соответствующим обращениям к alloc. Таким образом, память, с которой работают alloc и afree, является стеком (списком, в основе которого лежит принцип «последним вошел, первым ушел»). В стандартной библиотеке имеются функции malloc и free, которые делают то же самое, только без упомянутых ограничений; в параграфе 8.7 мы покажем, как они выглядят.
Функцию alloc легче всего реализовать, если условиться, что она будет распределять куски некоторого большого массива типа char, который мы назовем allocbuf. Этот массив отдадим в личное пользование функциям alloc и afree. Так как они имеют дело с указателями, а не с индексами массива, то другим программам знать его имя не нужно. Кроме того, этот массив можно определить в том же исходном файле, что и alloc и afree, объявив его как static, благодаря чему он станет невидимым вне этого файла. На практике такой массив может и вовсе не иметь имени, поскольку его можно запростиь с помощью malloc у операционной системы и получить указатель на некоторый безымянный блок памяти.
Естественно, нам нужно знать, сколько элементов массива allocbuf уже занято. Мы введем указатель allocp, который будет указывать на первый свободный элемент. Если запрашивается память для n символов, то alloc возвращает текущее значение allocp (т.е. адрес начала свободного блока) и затем увеличивает его на n, чтобы указатель allocp указывал на следующую свободную область. Если же пространства нет, то alloc возвращает нуль. Функция afree(p) просто присваивает allocp переданный параметр p, если он не выходит за пределы массива allocbuf.
Перед вызовом alloc:
После вызова alloc:
#define ALLOCSIZE 10000 /* размер доступного пространства */ static char allocbuf[ALLOCSIZE]; /* память для alloc */ static char *allocp = allocbuf; /* указатель на свободное место */ char *alloc(int n) /* возвращает указатель на память для n символов*/ { if (allocbuf + ALLOCSIZE - allocp >= n) { allocp += n; /* пространство есть */ return allocp - n; /* старый указатель */ } else /* пространства нет */ return 0; } void afree(char *p) /* освобождает память, на которую указывает p */ { if (p >= allocbuf && p < allocbuf + ALLOCSIZE) allocp = p; }
В общем случае указатель, как и любую другую переменную, можно инициализировать, но только такими осмысленными для него значениями, как нуль или выражение, результатом которого является адрес ранее определенных данных соответствующего типа. Объявление
static char *allocp = allocbuf;
определяет allocp как указатель на char и инициализирует его адресом первого элемента массива allocbuf, поскольку перед началом работы программы массив allocbuf пуст. Указанное объявление могло бы иметь и такой вид:
static char *allocp = &allocbuf[0];
поскольку имя массива и есть адрес его нулевого элемента.
Проверка
if (allocbuf + ALLOCSIZE - allocp >= n) { /* годится */
контроллирует, достаточно ли пространства, чтобы удовлетворить запрос на n символов. Если памяти достаточно, то новое значение для allocp должно указывать не далее, чем на следующую позицию за последним элементом allocbuf. При выполнении этого требования alloc выдает указатель на начало выделенного блока символов (обратите внимание на объявление типа самой функции). Если требование не выполняется, функция alloc должна выдать какой-то сигнал о том, что памяти не хватает. Си гарантирует, что нуль никогда не будет правильным адресом для данных, поэтому мы будем использовать его в качестве признака аварийного события, в нашем случае нехватки памяти.
Указатели и целые не являются взаимозаменяемыми объектами. Константа нуль — единственное исключение из этого правила: ее можно присвоить указателю, и указатель можно сравнить с нулевой константой. Чтобы показать, что нуль — это специальное значение для указателя, вместо цифры нуль, как правило, записывают NULL — константу, определенную в файле <stdio.h>. С этого момента и мы будем ею пользоваться.
Проверки
if (allocbuf + ALLOCSIZE - allocp >= n) { /* годится */
и
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
демонстрируют несколько важных свойств арифметики с указателями. Во-первых, при соблюдении некоторых правил указатели можно сравнивать.
Если p и q указывают на элементы одного массива, то к ним можно применять операции отношения ==, !=, <, >= и т.д. Например, отношение вида
p < q<
истинно, еслиэлемент массива, на который указывает p расположен раньше, чем элемент на который указывает q. Любой указатель всегда можно сравнивать на равенство и неравенство с нулем. А вот для указателей, не указывающих на элементы одного массива, результат арифметических операций или сравнений не определен. (Существует одно исключение: в арифметике с указателями можно использовать адрес несуществующего «следующего за массивом» элемента, т.е. адрес того «элемента», который станет последним, если в массив добавить еще один элемент.)
Во-вторых, как вы уже, наверное, заметили, уазатели и целые можно складывать и вычитать. Конструкция
p + n
означает адрес объекта, занимающего n-е место после объекта, на который указывает p. Это справедливо безотносительно к типу объекта, на который указывает p; n автоматически умножается на коэффициент, соответствующий размеру объекта. Информация о размере неявно присутствует в объявлении p. Если, к примеру, int занимает четыре байта, то коэффициент умножения будет равен четырем.
Допускается также вычитание указателей. Например, если p и q указывают на элементы одного массива и p < q, то q - p + 1 есть число элементов, расположенных от p до q включительно. Этим фактом можно воспользоваться при написании еще одной версии strlen:
/* strlen: возвращает длину строки s */ int strlen(char *s) { char *p = s; while (*p != '\0') p++; return p - s; }
В своем объявлении p инициализируется значением s, т.е. вначале p указывает на первый символ строки. На каждом шаге цикла while проверяется очередной символ; цикл продолжается до тех пор, пока не встретится символ '\0'. Каждое продвижение указателя p на следующий символ выполняется инструкцией p++, и разность p - s дает число пройденных символов, т.е. длину строки. (Число символов в строке может быть слишком большим, чтобы хранить его в переменной типа int. Тип ptrdiff_t, достаточный для хранения разности (со знаком) двух указателей, определен в заголовочном файле <stddef.h>. Однако, если быть очень осторожными, нам следовало бы для возвращаемого результата использовать тип size_t, в этом случае наша программа соответствовала бы стандартной библиотечной версии. Тип size_t есть тип беззнакового целого, возвращаемого оператором sizeof.)
Арифметика с указателями учитывает тип: если она имеет дело со значениями float, занимающими больше памяти, чем char, и p — указатель на float, то p++ продвинет p на следующее значение типа float. Это значит, что другую версию alloc, которая имеет дело с элементами типа float, а не char, можно получить простой заменой в alloc и afree всех char на float. Все операции с указателями будут автоматически откорректированы в соответствии с размером объектов, на которые указывают указатели.
Можно производить следующие операции с указателями: присваивание значения указателя другому указателю того же типа, сложение и вычитание указателя и целого, вычитание и сравнение двух указателей, указывающих на элементы одного и того же массива, а также присваивание указателю нуля и сравнение указателя с нулем. Выполнение других операций с указателями не допускается. Нельзя складывать два указателя, перемножать их, делить, сдвигать, выделять разряды; указатель нельзя складывать со значением типа float или double; указателю одного типа нельзя даже присвоить указатель другого типа, не выполнив предварительно операции приведения (исключение составляют лишь указатели типа void *).
netlib.narod.ru | < Назад | Оглавление | Далее > |