Адресная арифметика
Если P является указателем, то каков бы ни был сорт объекта, на который он указывает, операция P++ увеличивает P так, что он указывает на следующий элемент набора этих объектов, а операция P +=I увеличивает P так, чтобы он ука- зывал на элемент, отстоящий на I элементов от текущего эле- мента.эти и аналогичные конструкции представляют собой самые простые и самые распространенные формы арифметики указателей или адресной арифметики. Язык "C" последователен и постоянен в своем подходе к адресной арифметике; объединение в одно целое указателей, массивов и адресной арифметики является одной из наиболее сильных сторон языка. Давайте проиллюстрируем некоторые из соответствующих возможностей языка на примере элементарной (но полезной, несмотря на свою простоту) программы распреде- ления памяти. Имеются две функции: функция ALLOC(N) возвра- щает в качестве своего значения указатель P, который указы- вает на первую из N последовательных символьных позиций, ко- торые могут быть использованы вызывающей функцию ALLOC прог- раммой для хранения символов; функция FREE(P) освобождает приобретенную таким образом память, так что ее в дальнейшем можно снова использовать. программа является "элементарной", потому что обращения к FREE должны производиться в порядке, обратном тому, в котором производились обращения к ALLOC. Таким образом, управляемая функциями ALLOC и FREE память яв- ляется стеком или списком, в котором последний вводимый эле- мент извлекается первым. Стандартная библиотека языка "C" содержит аналогичные функции, не имеющие таких ограничений, и, кроме того, в главе 8 мы приведем улучшенные варианты. Между тем, однако, для многих приложений нужна только триви- альная функция ALLOC для распределения небольших участков памяти неизвестных заранее размеров в непредсказуемые момен- ты времени. Простейшая реализация состоит в том, чтобы функция раз- давала отрезки большого символьного массива, которому мы присвоили имя ALLOCBUF. Этот массив является собственностью функций ALLOC и FREE. Так как они работают с указателями, а не с индексами массива, никакой другой функции не нужно знать имя этого массива. Он может быть описан как внешний статический, т.е. Он будет локальным по отношению к исходно- му файлу, содержащему ALLOC и FREE, и невидимым за его пре- делами. При практической реализации этот массив может даже не иметь имени; вместо этого он может быть получен в резуль- тате запроса к операционной системе на указатель некоторого неименованного блока памяти. Другой необходимой информацией является то, какая часть массива ALLOCBUF уже использована. Мы пользуемся указателем первого свободного элемента, названным ALLOCP. Когда к функ- ции ALLOC обращаются за выделением N символов, то она прове- ряет, достаточно ли осталось для этого места в ALLOCBUF. Ес- ли достаточно, то ALLOC возвращает текущее значение ALLOCP (т.е. Начало свободного блока), затем увеличивает его на N, с тем чтобы он указывал на следующую свободную область. Фун- кция FREE(P) просто полагает ALLOCP равным P при условии, что P указывает на позицию внутри ALLOCBUF.
DEFINE NULL 0 /* POINTER VALUE FOR ERROR REPORT */ DEFINE ALLOCSIZE 1000 /* SIZE OF AVAILABLE SPACE */
TATIC CHAR ALLOCBUF[ALLOCSIZE];/* STORAGE FOR ALLOC */ TATIC CHAR *ALLOCP = ALLOCBUF; /* NEXT FREE POSITION */
HAR *ALLOC(N) /* RETURN POINTER TO N CHARACTERS */ INT N; ( IF (ALLOCP + N <= ALLOCBUF + ALLOCSIZE) { ALLOCP += N; RETURN(ALLOCP - N); /* OLD P */ } ELSE /* NOT ENOUGH ROOM */ RETURN(NULL); )
REE(P) /* FREE STORAGE POINTED BY P */ HAR *P; ( IF (P >= ALLOCBUF && P < ALLOCBUF + ALLOCSIZE) ALLOCP = P; )
Дадим некоторые пояснения. Вообще говоря, указатель мо- жет быть инициализирован точно так же, как и любая другая переменная, хотя обычно единственными осмысленными значения- ми являются NULL (это обсуждается ниже) или выражение, вклю- чающее адреса ранее определенных данных соответствующего ти- па. Описание
STATIC CHAR *ALLOCP = ALLOCBUF;
определяет ALLOCP как указатель на символы и инициализирует его так, чтобы он указывал на ALLOCBUF, т.е. На первую сво- бодную позицию при начале работы программы. Так как имя мас- сива является адресом его нулевого элемента, то это можно было бы записать в виде
STATIC CHAR *ALLOCP = &ALLOCBUF[0];
используйте ту запись, которая вам кажется более естествен- ной. С помощью проверки
IF (ALLOCP + N <= ALLOCBUF + ALLOCSIZE)
выясняется, осталось ли достаточно места, чтобы удовлетво- рить запрос на N символов. Если достаточно, то новое значе- ние ALLOCP не будет указывать дальше, чем на последнюю пози- цию ALLOCBUF. Если запрос может быть удовлетворен, то ALLOC возвращает обычный указатель (обратите внимание на описание самой функции). Если же нет, то ALLOC должна вернуть некото- рый признак, говорящий о том, что больше места не осталось. В языке "C" гарантируется, что ни один правильный указатель данных не может иметь значение нуль, так что возвращение ну- ля может служить в качестве сигнала о ненормальном событии, в данном случае об отсутствии места. Мы, однако, вместо нуля пишем NULL, с тем чтобы более ясно показать, что это специ- альное значение указателя. Вообще говоря, целые не могут ос- мысленно присваиваться указателям, а нуль - это особый слу- чай. Проверки вида
IF (ALLOCP + N <= ALLOCBUF + ALOOCSIZE) и IF (P >= ALLOCBUF && P < ALLOCBUF + ALLOCSIZE)
демонстрируют несколько важных аспектов арифметики указате- лей. Во-первых , при определенных условиях указатели можно сравнивать. Если P и Q указывают на элементы одного и того же массива, то такие отношения, как <, >= и т.д., работают надлежащим образом. Например,
P < Q
истинно, если P указывает на более ранний элемент массива, чем Q. Отношения == и != тоже работают. Любой указатель мож- но осмысленным образом сравнить на равенство или неравенство с NULL. Но ни за что нельзя ручаться, если вы используете сравнения при работе с указателями, указывающими на разные массивы. Если вам повезет, то на всех машинах вы получите очевидную бессмыслицу. Если же нет, то ваша программа будет правильно работать на одной машине и давать непостижимые ре- зультаты на другой. Во-вторых, как мы уже видели, указатель и целое можно складывать и вычитать. Конструкция
P + N
подразумевает N-ый объект за тем, на который P указывает в настоящий момент. Это справедливо независимо от того, на ка- кой вид объектов P должен указывать; компилятор сам масшта- бирует N в соответствии с определяемым из описания P разме- ром объектов, указываемых с помощью P. например, на PDP-11 масштабирующий множитель равен 1 для CHAR, 2 для INT и SHORT, 4 для LONG и FLOAT и 8 для DOUBLE. Вычитание указателей тоже возможно: если P и Q указывают на элементы одного и того же массива, то P-Q - количество элементов между P и Q. Этот факт можно использовать для на- писания еще одного варианта функции
STRLEN: STRLEN(S) /* RETURN LENGTH OF STRING S */ CHAR *S; { CHAR *P = S;
WHILE (*P != '\0') P++; RETURN(P-S); } При описании указатель P в этой функции инициализирован посредством строки S, в результате чего он указывает на пер- вый символ строки. В цикле WHILE по очереди проверяется каж- дый символ до тех пор, пока не появится символ конца строки \0. Так как значение \0 равно нулю, а WHILE только выясняет, имеет ли выражение в нем значение 0, то в данном случае яв- ную проверку можно опустить. Такие циклы часто записывают в виде
WHILE (*P) P++;
Так как P указывает на символы, то оператор P++ передви- гает P каждый раз так, чтобы он указывал на следующий сим- вол. В результате P-S дает число просмотренных символов,
т.е. Длину строки. Арифметика указателей последовательна: если бы мы имели дело с переменными типа FLOAT, которые за- нимают больше памяти, чем переменные типа CHAR, и если бы P был указателем на FLOAT, то оператор P++ передвинул бы P на следующее FLOAT. таким образом, мы могли бы написать другой вариант функции ALLOC, распределяющей память для FLOAT, вместо CHAR, просто заменив всюду в ALLOC и FREE описатель CHAR на FLOAT. Все действия с указателями автоматически учи- тывают размер объектов, на которые они указывают, так что больше ничего менять не надо. За исключением упомянутых выше операций (сложение и вы- читание указателя и целого, вычитание и сравнение двух ука- зателей), вся остальная арифметика указателей является неза- конной. Запрещено складывать два указателя, умножать, де- лить, сдвигать или маскировать их, а также прибавлять к ним переменные типа FLOAT или DOUBLE.