Если 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.
5.5. Указатели символов и функции
Строчная константа, как, например,
"I AM A STRING"
является массивом символов. Компилятор завершает внутреннее представление такого
массива символом \0, так что программы могут находить его конец. Таким образом,
длина массива в па- мяти оказывается на единицу больше числа символов между двойными
кавычками.
По-видимому чаще всего строчные константы появляются в качестве аргументов функций,
как, например, в
PRINTF ("HELLO, WORLD\N");
когда символьная строка, подобная этой, появляется в прог- рамме, то доступ к
ней осуществляется с помощью указателя символов; функция PRINTF фактически получает
указатель сим- вольного массива.
Конечно, символьные массивы не обязаны быть только аргу- ментами функций. Если
описать MESSAGE как
CHAR *MESSAGE;
то в результате оператора
MESSAGE = "NOW IS THE TIME";
переменная MESSAGE станет указателем на фактический массив символов. Это не копирование
строки; здесь участвуют только указатели. в языке "C" не предусмотрены какие-либо
операции для обработки всей строки символов как целого.
Мы проиллюстрируем другие аспекты указателей и массивов, разбирая две полезные
функции из стандартной библиотеки вво- да-вывода, которая будет рассмотрена в главе
7.
Первая функция - это STRCPY(S,T), которая копирует стро- ку т в строку S. Аргументы
написаны именно в этом порядке по аналогии с операцией присваивания, когда для того,
чтобы присвоить T к S обычно пишут
S = T
сначала приведем версию с массивами:
STRCPY(S, T) /* COPY T TO S */
CHAR S[], T[];
{
INT I;
I = 0;
WHILE ((S[I] = T[I]) != '\0')
I++;
}
Для сопоставления ниже дается вариант STRCPY с указате- лями.
STRCPY(S, T) /* COPY T TO S; POINTER VERSION 1 */ CHAR *S, *T; {
WHILE ((*S = *T) != '\0') {
S++;
T++;
} }
Так как аргументы передаются по значению, функция STRCPY может использовать S
и T так, как она пожелает. Здесь они с удобством полагаются указателями, которые
передвигаются вдоль массивов, по одному символу за шаг, пока не будет ско- пирован
в S завершающий в T символ \0.
На практике функция STRCPY была бы записана не так, как мы показали выше. Вот
вторая возможность:
STRCPY(S, T) /* COPY T TO S; POINTER VERSION 2 */ CHAR *S, *T; {
WHILE ((*S++ = *T++) != '\0')
; }
Здесь увеличение S и T внесено в проверочную часть. Зна- чением *T++ является
символ, на который указывал T до увели- чения; постфиксная операция ++ не изменяет
T, пока этот сим- вол не будет извлечен. Точно так же этот символ помещается в старую
позицию S, до того как S будет увеличено. Конечный результат заключается в том,
что все символы, включая завер- шающий \0, копируются из T в S.
И как последнее сокращение мы опять отметим, что сравне- ние с \0 является излишним,
так что функцию можно записать в виде
STRCPY(S, T) /* COPY T TO S; POINTER VERSION 3 */ CHAR *S, *T; {
WHILE (*S++ = *T++)
; }
хотя с первого взгляда эта запись может показаться загадоч- ной, она дает значительное
удобство. Этой идиомой следует овладеть уже хотя бы потому, что вы с ней будете
часто вст- речаться в "C"-программах.
Вторая функция - STRCMP(S, T), которая сравнивает сим- вольные строки S и т,
возвращая отрицательное, нулевое или положительное значение в соответствии с тем,
меньше, равно или больше лексикографически S, чем T. Возвращаемое значение получается
в результате вычитания символов из первой пози- ции, в которой S и T не совпадают.
STRCMP(S, T) /* RETURN <0 IF S<T, 0 IF S==T, >0 IF S>T */ CHAR S[], T[]; {
INT I;
I = 0;
WHILE (S[I] == T[I])
IF (S[I++] == '\0')
RETURN(0);
RETURN(S[I]-T[I]); }
Вот версия STRCMP с указателями:
STRCMP(S, T) /* RETURN <0 IF S<T, 0 IF S==T, >0 IF S>T */ CHAR *S, *T; {
FOR ( ; *S == *T; S++, T++)
IF (*S == '\0')
RETURN(0);
RETURN(*S-*T); }
так как ++ и -- могут быть как постфиксными, так и префиксными операциями, встречаются
другие комбинации * и ++ и --, хотя и менее часто.
Например
*++P
увеличивает P до извлечения символа, на который указывает
P, а
*--P
сначала уменьшает P.
Упражнение 5-2
Напишите вариант с указателями функции STRCAT из главы 2: STRCAT(S, T) копирует
строку T в конец S.
Упражнение 5-3
Напишите макрос для STRCPY.
Упражнение 5-4
Перепишите подходящие программы из предыдущих глав и уп- ражнений, используя
указатели вместо индексации массивов. Хорошие возможности для этого предоставляют
функции GETLINE /главы 1 и 4/, ATOI, ITOA и их варианты /главы 2, 3 и 4/, REVERSE
/глава 3/, INDEX и GETOP /глава 4/.
5.6. Указатели - не целые
Вы, возможно, обратили внимание в предыдущих "с"-прог- раммах на довольно непринужденное
отношение к копированию указателей. В общем это верно, что на большинстве машин
ука- затель можно присвоить целому и передать его обратно, не из- менив его; при
этом не происходит никакого масштабирования или преобразования и ни один бит не
теряется. к сожалению, это ведет к вольному обращению с функциями, возвращающими
указатели, которые затем просто передаются другим функциям, - необходимые описания
указателей часто опускаются. Рассмот- рим, например, функцию STRSAVE(S), которая
копирует строку S в некоторое место для хранения, выделяемое посредством обра- щения
к функции ALLOC, и возвращает указатель на это место. Правильно она должна быть
записана так: