расширение MAX, вы заметите определенные недостатки. Выраже-
ния вычисляются дважды; это плохо, если они влекут за собой
побочные эффекты, вызванные, например, обращениями к функци-
ям или использованием операций увеличения. Нужно позаботить-
ся о правильном использовании круглых скобок, чтобы гаранти-
ровать сохранение требуемого порядка вычислений. (Рассмотри-
те макрос
#DEFINE SQUARE(X) X * X
при обращении к ней, как SQUARE(Z+1)). Здесь возникают даже
некоторые чисто лексические проблемы: между именем макро и
левой круглой скобкой, открывающей список ее аргументов, не
должно быть никаких пробелов.
Тем не менее аппарат макросов является весьма ценным.
Один практический пример дает описываемая в главе 7 стандар-
тная библиотека ввода-вывода, в которой GETCHAR и PUTCHAR
определены как макросы (очевидно PUTCHAR должна иметь аргу-
мент), что позволяет избежать затрат на обращение к функции
при обработке каждого символа.
Другие возможности макропроцессора описаны в приложении
А.
Упражнение 4-9
---------------
Определите макрос SWAP(X, Y), который обменивает значе-
ниями два своих аргумента типа INT. (В этом случае поможет
блочная структура).
Указатель - это переменная, содержащая адрес другой пе-
ременной. указатели очень широко используются в языке "C".
Это происходит отчасти потому, что иногда они дают единст-
венную возможность выразить нужное действие, а отчасти пото-
му, что они обычно ведут к более компактным и эффективным
программам, чем те, которые могут быть получены другими спо-
собами.
Указатели обычно смешивают в одну кучу с операторами
GOTO, характеризуя их как чудесный способ написания прог-
рамм, которые невозможно понять. Это безусловно спрAведливо,
если указатели используются беззаботно; очень просто ввести
указатели, которые указывают на что-то совершенно неожидан-
ное. Однако, при определенной дисциплине, использование ука-
зателей помогает достичь ясности и простоты. Именно этот ас-
пект мы попытаемся здесь проиллюстрировать.
Так как указатель содержит адрес объекта, это дает воз-
можность "косвенного" доступа к этому объекту через указа-
тель. Предположим, что х - переменная, например, типа INT, а
рх - указатель, созданный неким еще не указанным способом.
Унарная операция & выдает адрес объекта, так что оператор
рх = &х;
присваивает адрес х переменной рх; говорят, что рх "ука-
зывает" на х. Операция & применима только к переменным и
элементам массива, конструкции вида &(х-1) и &3 являются не-
законными. Нельзя также получить адрес регистровой перемен-
ной.
Унарная операция * рассматривает свой операнд как адрес
конечной цели и обращается по этому адресу, чтобы извлечь
содержимое. Следовательно, если Y тоже имеет тип INT, то
Y = *рх;
присваивает Y содержимое того, на что указывает рх. Так пос-
ледовательность
рх = &х;
Y = *рх;
присваивает Y то же самое значение, что и оператор
Y = X;
Переменные, участвующие во всем этом необходимо описать:
INT X, Y;
INT *PX;
с описанием для X и Y мы уже неодонократно встречались.
Описание указателя
INT *PX;
является новым и должно рассматриваться как мнемоническое;
оно говорит, что комбинация *PX имеет тип INT. Это означает,
что если PX появляется в контексте *PX, то это эквивалентно
переменной типа INT. Фактически синтаксис описания перемен-
ной имитирует синтаксис выражений, в которых эта переменная
может появляться. Это замечание полезно во всех случаях,
связанных со сложными описаниями. Например,
DOUBLE ATOF(), *DP;
говорит, что ATOF() и *DP имеют в выражениях значения типа
DOUBLE.
Вы должны также заметить, что из этого описания следу-
ет, что указатель может указывать только на определенный вид
объектов.
Указатели могут входить в выражения. Например, если PX
указывает на целое X, то *PX может появляться в любом кон-
тексте, где может встретиться X. Так оператор
Y = *PX + 1
присваивает Y значение, на 1 большее значения X;
PRINTF("%D\N", *PX)
печатает текущее значение X;
D = SQRT((DOUBLE) *PX)
получает в D квадратный корень из X, причем до передачи фун-
кции SQRT значение X преобразуется к типу DOUBLE. (Смотри
главу 2).
В выражениях вида
Y = *PX + 1
унарные операции * и & связаны со своим операндом более
крепко, чем арифметические операции, так что такое выражение
берет то значение, на которое указывает PX, прибавляет 1 и
присваивает результат переменной Y. Мы вскоре вернемся к то-
му, что может означать выражение
Y = *(PX + 1)
Ссылки на указатели могут появляться и в левой части
присваиваний. Если PX указывает на X, то
*PX = 0
полагает X равным нулю, а
*PX += 1
увеличивает его на единицу, как и выражение
(*PX)++
Круглые скобки в последнем примере необходимы; если их опус-
тить, то поскольку унарные операции, подобные * и ++, выпол-
няются справа налево, это выражение увеличит PX, а не ту пе-
ременную, на которую он указывает.
И наконец, так как указатели являются переменными, то с
ними можно обращаться, как и с остальными переменными. Если
PY - другой указатель на переменную типа INT, то
PY = PX
копирует содержимое PX в PY, в результате чего PY указывает
на то же, что и PX.
Так как в "с" передача аргументов функциям осуществляет-
ся "по значению", вызванная процедура не имеет непосредст-
венной возможности изменить переменную из вызывающей прог-
раммы. Что же делать, если вам действительно надо изменить
аргумент? например, программа сортировки захотела бы поме-
нять два нарушающих порядок элемента с помощью функции с
именем SWAP. Для этого недостаточно написать
SWAP(A, B);
определив функцию SWAP при этом следующим образом:
SWAP(X, Y) /* WRONG */
INT X, Y;
{
INT TEMP;
TEMP = X;
X = Y;
Y = TEMP;
}
из-за вызова по значению SWAP не может воздействовать на
агументы A и B в вызывающей функции.
К счастью, все же имеется возможность получить желаемый
эффект. Вызывающая программа передает указатели подлежащих
изменению значений:
SWAP(&A, &B);
так как операция & выдает адрес переменной, то &A является
указателем на A. В самой SWAP аргументы описываются как ука-
затели и доступ к фактическим операндам осуществляется через
них.
SWAP(PX, PY) /* INTERCHANGE *PX AND *PY */
INT *PX, *PY;
{
INT TEMP;
TEMP = *PX;
*PX = *PY;
*PY = TEMP;
}
Указатели в качестве аргументов обычно используются в
функциях, которые должны возвращать более одного значения.
(Можно сказать, что SWAP вOзвращает два значения, новые зна-
чения ее аргументов). В качестве примера рассмотрим функцию
GETINT, которая осуществляет преобразование поступающих в
своболном формате данных, разделяя поток символов на целые
значения, по одному целому за одно обращение. Функция GETINT
должна возвращать либо найденное значение, либо признак кон-
ца файла, если входные данные полностью исчерпаны. Эти зна-
чения должны возвращаться как отдельные объекты, какое бы
значение ни использовалось для EOF, даже если это значение
вводимого целого.
Одно из решений, основывающееся на описываемой в главе 7
функции ввода SCANF, состоит в том, чтобы при выходе на ко-
нец файла GETINT возвращала EOF в качестве значения функции;
любое другое возвращенное значение говорит о нахождении нор-
мального целого. Численное же значение найденного целого
возвращается через аргумент, который должен быть указателем
целого. Эта организация разделяет статус конца файла и чис-
ленные значения.
Следующий цикл заполняет массив целыми с помощью обраще-
ний к функции GETINT:
INT N, V, ARRAY[SIZE];
FOR (N = 0; N < SIZE && GETINT(&V) != EOF; N++)
ARRAY[N] = V;
В результате каждого обращения V становится равным следующе-
му целому значению, найденному во входных данных. Обратите
внимание, что в качестве аргумента GETINT необходимо указать
&V а не V. Использование просто V скорее всего приведет к
ошибке адресации, поскольку GETINT полагает, что она работа-
ет именно с указателем.
Сама GETINT является очевидной модификацией написанной
нами ранее функции ATOI:
GETINT(PN) /* GET NEXT INTEGER FROM INPUT */
INT *PN;
{
INT C,SIGN;
WHILE ((C = GETCH()) == ' ' \!\! C == '\N'
\!\! C == '\T'); /* SKIP WHITE SPACE */
SIGN = 1;
IF (C == '+' \!\! C == '-') { /* RECORD
SIGN */
SIGN = (C == '+') ? 1 : -1;
C = GETCH();
}
FOR (*PN = 0; C >= '0' && C <= '9'; C = GETCH())
*PN = 10 * *PN + C - '0';
*PN *= SIGN;
IF (C != EOF)
UNGETCH(C);
RETURN(C);
}
Выражение *PN используется всюду в GETINT как обычная пере-
менная типа INT. Мы также использовали функции GETCH и
UNGETCH (описанные в главе 4) , так что один лишний символ,
кототрый приходится считывать, может быть помещен обратно во
ввод.
Упражнение 5-1
---------------
Напишите функцию GETFLOAT, аналог GETINT для чисел с
плавающей точкой. Какой тип должна возвращать GETFLOAT в ка-
честве значения функции?
В языке "C" существует сильная взаимосвязь между указа-
телями и массивами , настолько сильная, что указатели и мас-
сивы действительно следует рассматривать одновременно. Любую
операцию, которую можно выполнить с помощью индексов масси-
ва, можно сделать и с помощью указателей. вариант с указате-
лями обычно оказывается более быстрым, но и несколько более
трудным для непосредственного понимания, по крайней мере для
начинающего. описание
INT A[10]
определяет массив размера 10, т.е. Набор из 10 последова-
тельных объектов, называемых A[0], A[1], ..., A[9]. Запись
A[I] соответствует элементу массива через I позиций от нача-
ла. Если PA - указатель целого, описанный как
INT *PA
то присваивание
PA = &A[0]
приводит к тому, что PA указывает на нулевой элемент массива
A; это означает, что PA содержит адрес элемента A[0]. Теперь
присваивание
X = *PA
будет копировать содержимое A[0] в X.
Если PA указывает на некоторый определенный элемент мас-
сива A, то по определению PA+1 указывает на следующий эле-
мент, и вообще PA-I указывает на элемент, стоящий на I пози-
ций до элемента, указываемого PA, а PA+I на элемент, стоящий
на I позиций после. Таким образом, если PA указывает на
A[0], то
*(PA+1)
ссылается на содержимое A[1], PA+I - адрес A[I], а *(PA+I) -
содержимое A[I].
Эти замечания справедливы независимо от типа переменных
в массиве A. Суть определения "добавления 1 к указателю", а
также его распространения на всю арифметику указателей, сос-
тоит в том, что приращение масштабируется размером памяти,
занимаемой объектом, на который указывает указатель. Таким
образом, I в PA+I перед прибавлением умножается на размер
объектов, на которые указывает PA.
Очевидно существует очень тесное соответствие между ин-
дексацией и арифметикой указателей. в действительности ком-
пилятор преобразует ссылку на массив в указатель на начало
массива. В результате этого имя массива является указатель-
ным выражением. Отсюда вытекает несколько весьма полезных
следствий. Так как имя массива является синонимом местополо-
жения его нулевого элемента, то присваивание PA=&A[0] можно
записать как
PA = A
Еще более удивительным, по крайней мере на первый взг-
ляд, кажется тот факт, что ссылку на A[I] можно записать в
виде *(A+I). При анализировании выражения A[I] в языке "C"
оно немедленно преобразуется к виду *(A+I); эти две формы
совершенно эквивалентны. Если применить операцию & к обеим
частям такого соотношения эквивалентности, то мы получим,
что &A[I] и A+I тоже идентичны: A+I - адрес I-го элемента от
начала A. С другой стороны, если PA является указателем, то
в выражениях его можно использовать с индексом: PA[I] иден-
тично *(PA+I). Короче, любое выражение, включающее массивы и
индексы, может быть записано через указатели и смещения и
наоборот, причем даже в одном и том же утверждении.
Имеется одно различие между именем массива и указателем,
которое необходимо иметь в виду. указатель является перемен-
ной, так что операции PA=A и PA++ имеют смысл. Но имя масси-
ва является константой, а не переменной: конструкции типа
A=PA или A++,или P=&A будут незаконными.
Когда имя массива передается функции, то на самом деле
ей передается местоположение начала этого массива. Внутри
вызванной функции такой аргумент является точно такой же пе-
ременной, как и любая другая, так что имя массива в качестве
аргумента действительно является указателем, т.е. Перемен-
ной, содержащей адрес. мы можем использовать это обстоятель-
ство для написания нового варианта функции STRLEN, вычисляю-
щей длину строки.
STRLEN(S) /* RETURN LENGTH OF STRING S */
CHAR *S;
{
INT N;
FOR (N = 0; *S != '\0'; S++)
N++;
RETURN(N);
}
Операция увеличения S совершенно законна, поскольку эта
переменная является указателем; S++ никак не влияет на сим-
вольную строку в обратившейся к STRLEN функции, а только
увеличивает локальную для функции STRLEN копию адреса. Опи-
сания формальных параметров в определении функции в виде
CHAR S[];
CHAR *S;
совершенно эквивалентны; какой вид описания следует предпо-
честь, определяется в значительной степени тем, какие выра-
жения будут использованы при написании функции. Если функции
передается имя массива, то в зависимости от того, что удоб-
нее, можно полагать, что функция оперирует либо с массивом,
либо с указателем, и действовать далее соответвующим обра-
зом. Можно даже использовать оба вида операций, если это ка-
жется уместным и ясным.
Можно передать функции часть массива, если задать в ка-
честве аргумента указатель начала подмассива. Например, если
A - массив, то как
F(&A[2])
как и
F(A+2)
передают функции F адрес элемента A[2], потому что и &A[2],
и A+2 являются указательными выражениями, ссылающимися на
третий элемент A. внутри функции F описания аргументов могут
присутствовать в виде:
F(ARR)
INT ARR[];
{
...
}
или
F(ARR)
INT *ARR;
{
...
}
Что касается функции F, то тот факт, что ее аргумент в дейс-
твительности ссылается к части большего массива,не имеет для
нее никаких последствий.
Если 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.
Строчная константа, как, например,
"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/.
Вы, возможно, обратили внимание в предыдущих "с"-прог-
ния вычисляются дважды; это плохо, если они влекут за собой
побочные эффекты, вызванные, например, обращениями к функци-
ям или использованием операций увеличения. Нужно позаботить-
ся о правильном использовании круглых скобок, чтобы гаранти-
ровать сохранение требуемого порядка вычислений. (Рассмотри-
те макрос
#DEFINE SQUARE(X) X * X
при обращении к ней, как SQUARE(Z+1)). Здесь возникают даже
некоторые чисто лексические проблемы: между именем макро и
левой круглой скобкой, открывающей список ее аргументов, не
должно быть никаких пробелов.
Тем не менее аппарат макросов является весьма ценным.
Один практический пример дает описываемая в главе 7 стандар-
тная библиотека ввода-вывода, в которой GETCHAR и PUTCHAR
определены как макросы (очевидно PUTCHAR должна иметь аргу-
мент), что позволяет избежать затрат на обращение к функции
при обработке каждого символа.
Другие возможности макропроцессора описаны в приложении
А.
Упражнение 4-9
---------------
Определите макрос SWAP(X, Y), который обменивает значе-
ниями два своих аргумента типа INT. (В этом случае поможет
блочная структура).
Указатель - это переменная, содержащая адрес другой пе-
ременной. указатели очень широко используются в языке "C".
Это происходит отчасти потому, что иногда они дают единст-
венную возможность выразить нужное действие, а отчасти пото-
му, что они обычно ведут к более компактным и эффективным
программам, чем те, которые могут быть получены другими спо-
собами.
Указатели обычно смешивают в одну кучу с операторами
GOTO, характеризуя их как чудесный способ написания прог-
рамм, которые невозможно понять. Это безусловно спрAведливо,
если указатели используются беззаботно; очень просто ввести
указатели, которые указывают на что-то совершенно неожидан-
ное. Однако, при определенной дисциплине, использование ука-
зателей помогает достичь ясности и простоты. Именно этот ас-
пект мы попытаемся здесь проиллюстрировать.
Так как указатель содержит адрес объекта, это дает воз-
можность "косвенного" доступа к этому объекту через указа-
тель. Предположим, что х - переменная, например, типа INT, а
рх - указатель, созданный неким еще не указанным способом.
Унарная операция & выдает адрес объекта, так что оператор
рх = &х;
присваивает адрес х переменной рх; говорят, что рх "ука-
зывает" на х. Операция & применима только к переменным и
элементам массива, конструкции вида &(х-1) и &3 являются не-
законными. Нельзя также получить адрес регистровой перемен-
ной.
Унарная операция * рассматривает свой операнд как адрес
конечной цели и обращается по этому адресу, чтобы извлечь
содержимое. Следовательно, если Y тоже имеет тип INT, то
Y = *рх;
присваивает Y содержимое того, на что указывает рх. Так пос-
ледовательность
рх = &х;
Y = *рх;
присваивает Y то же самое значение, что и оператор
Y = X;
Переменные, участвующие во всем этом необходимо описать:
INT X, Y;
INT *PX;
с описанием для X и Y мы уже неодонократно встречались.
Описание указателя
INT *PX;
является новым и должно рассматриваться как мнемоническое;
оно говорит, что комбинация *PX имеет тип INT. Это означает,
что если PX появляется в контексте *PX, то это эквивалентно
переменной типа INT. Фактически синтаксис описания перемен-
ной имитирует синтаксис выражений, в которых эта переменная
может появляться. Это замечание полезно во всех случаях,
связанных со сложными описаниями. Например,
DOUBLE ATOF(), *DP;
говорит, что ATOF() и *DP имеют в выражениях значения типа
DOUBLE.
Вы должны также заметить, что из этого описания следу-
ет, что указатель может указывать только на определенный вид
объектов.
Указатели могут входить в выражения. Например, если PX
указывает на целое X, то *PX может появляться в любом кон-
тексте, где может встретиться X. Так оператор
Y = *PX + 1
присваивает Y значение, на 1 большее значения X;
PRINTF("%D\N", *PX)
печатает текущее значение X;
D = SQRT((DOUBLE) *PX)
получает в D квадратный корень из X, причем до передачи фун-
кции SQRT значение X преобразуется к типу DOUBLE. (Смотри
главу 2).
В выражениях вида
Y = *PX + 1
унарные операции * и & связаны со своим операндом более
крепко, чем арифметические операции, так что такое выражение
берет то значение, на которое указывает PX, прибавляет 1 и
присваивает результат переменной Y. Мы вскоре вернемся к то-
му, что может означать выражение
Y = *(PX + 1)
Ссылки на указатели могут появляться и в левой части
присваиваний. Если PX указывает на X, то
*PX = 0
полагает X равным нулю, а
*PX += 1
увеличивает его на единицу, как и выражение
(*PX)++
Круглые скобки в последнем примере необходимы; если их опус-
тить, то поскольку унарные операции, подобные * и ++, выпол-
няются справа налево, это выражение увеличит PX, а не ту пе-
ременную, на которую он указывает.
И наконец, так как указатели являются переменными, то с
ними можно обращаться, как и с остальными переменными. Если
PY - другой указатель на переменную типа INT, то
PY = PX
копирует содержимое PX в PY, в результате чего PY указывает
на то же, что и PX.
Так как в "с" передача аргументов функциям осуществляет-
ся "по значению", вызванная процедура не имеет непосредст-
венной возможности изменить переменную из вызывающей прог-
раммы. Что же делать, если вам действительно надо изменить
аргумент? например, программа сортировки захотела бы поме-
нять два нарушающих порядок элемента с помощью функции с
именем SWAP. Для этого недостаточно написать
SWAP(A, B);
определив функцию SWAP при этом следующим образом:
SWAP(X, Y) /* WRONG */
INT X, Y;
{
INT TEMP;
TEMP = X;
X = Y;
Y = TEMP;
}
из-за вызова по значению SWAP не может воздействовать на
агументы A и B в вызывающей функции.
К счастью, все же имеется возможность получить желаемый
эффект. Вызывающая программа передает указатели подлежащих
изменению значений:
SWAP(&A, &B);
так как операция & выдает адрес переменной, то &A является
указателем на A. В самой SWAP аргументы описываются как ука-
затели и доступ к фактическим операндам осуществляется через
них.
SWAP(PX, PY) /* INTERCHANGE *PX AND *PY */
INT *PX, *PY;
{
INT TEMP;
TEMP = *PX;
*PX = *PY;
*PY = TEMP;
}
Указатели в качестве аргументов обычно используются в
функциях, которые должны возвращать более одного значения.
(Можно сказать, что SWAP вOзвращает два значения, новые зна-
чения ее аргументов). В качестве примера рассмотрим функцию
GETINT, которая осуществляет преобразование поступающих в
своболном формате данных, разделяя поток символов на целые
значения, по одному целому за одно обращение. Функция GETINT
должна возвращать либо найденное значение, либо признак кон-
ца файла, если входные данные полностью исчерпаны. Эти зна-
чения должны возвращаться как отдельные объекты, какое бы
значение ни использовалось для EOF, даже если это значение
вводимого целого.
Одно из решений, основывающееся на описываемой в главе 7
функции ввода SCANF, состоит в том, чтобы при выходе на ко-
нец файла GETINT возвращала EOF в качестве значения функции;
любое другое возвращенное значение говорит о нахождении нор-
мального целого. Численное же значение найденного целого
возвращается через аргумент, который должен быть указателем
целого. Эта организация разделяет статус конца файла и чис-
ленные значения.
Следующий цикл заполняет массив целыми с помощью обраще-
ний к функции GETINT:
INT N, V, ARRAY[SIZE];
FOR (N = 0; N < SIZE && GETINT(&V) != EOF; N++)
ARRAY[N] = V;
В результате каждого обращения V становится равным следующе-
му целому значению, найденному во входных данных. Обратите
внимание, что в качестве аргумента GETINT необходимо указать
&V а не V. Использование просто V скорее всего приведет к
ошибке адресации, поскольку GETINT полагает, что она работа-
ет именно с указателем.
Сама GETINT является очевидной модификацией написанной
нами ранее функции ATOI:
GETINT(PN) /* GET NEXT INTEGER FROM INPUT */
INT *PN;
{
INT C,SIGN;
WHILE ((C = GETCH()) == ' ' \!\! C == '\N'
\!\! C == '\T'); /* SKIP WHITE SPACE */
SIGN = 1;
IF (C == '+' \!\! C == '-') { /* RECORD
SIGN */
SIGN = (C == '+') ? 1 : -1;
C = GETCH();
}
FOR (*PN = 0; C >= '0' && C <= '9'; C = GETCH())
*PN = 10 * *PN + C - '0';
*PN *= SIGN;
IF (C != EOF)
UNGETCH(C);
RETURN(C);
}
Выражение *PN используется всюду в GETINT как обычная пере-
менная типа INT. Мы также использовали функции GETCH и
UNGETCH (описанные в главе 4) , так что один лишний символ,
кототрый приходится считывать, может быть помещен обратно во
ввод.
Упражнение 5-1
---------------
Напишите функцию GETFLOAT, аналог GETINT для чисел с
плавающей точкой. Какой тип должна возвращать GETFLOAT в ка-
честве значения функции?
В языке "C" существует сильная взаимосвязь между указа-
телями и массивами , настолько сильная, что указатели и мас-
сивы действительно следует рассматривать одновременно. Любую
операцию, которую можно выполнить с помощью индексов масси-
ва, можно сделать и с помощью указателей. вариант с указате-
лями обычно оказывается более быстрым, но и несколько более
трудным для непосредственного понимания, по крайней мере для
начинающего. описание
INT A[10]
определяет массив размера 10, т.е. Набор из 10 последова-
тельных объектов, называемых A[0], A[1], ..., A[9]. Запись
A[I] соответствует элементу массива через I позиций от нача-
ла. Если PA - указатель целого, описанный как
INT *PA
то присваивание
PA = &A[0]
приводит к тому, что PA указывает на нулевой элемент массива
A; это означает, что PA содержит адрес элемента A[0]. Теперь
присваивание
X = *PA
будет копировать содержимое A[0] в X.
Если PA указывает на некоторый определенный элемент мас-
сива A, то по определению PA+1 указывает на следующий эле-
мент, и вообще PA-I указывает на элемент, стоящий на I пози-
ций до элемента, указываемого PA, а PA+I на элемент, стоящий
на I позиций после. Таким образом, если PA указывает на
A[0], то
*(PA+1)
ссылается на содержимое A[1], PA+I - адрес A[I], а *(PA+I) -
содержимое A[I].
Эти замечания справедливы независимо от типа переменных
в массиве A. Суть определения "добавления 1 к указателю", а
также его распространения на всю арифметику указателей, сос-
тоит в том, что приращение масштабируется размером памяти,
занимаемой объектом, на который указывает указатель. Таким
образом, I в PA+I перед прибавлением умножается на размер
объектов, на которые указывает PA.
Очевидно существует очень тесное соответствие между ин-
дексацией и арифметикой указателей. в действительности ком-
пилятор преобразует ссылку на массив в указатель на начало
массива. В результате этого имя массива является указатель-
ным выражением. Отсюда вытекает несколько весьма полезных
следствий. Так как имя массива является синонимом местополо-
жения его нулевого элемента, то присваивание PA=&A[0] можно
записать как
PA = A
Еще более удивительным, по крайней мере на первый взг-
ляд, кажется тот факт, что ссылку на A[I] можно записать в
виде *(A+I). При анализировании выражения A[I] в языке "C"
оно немедленно преобразуется к виду *(A+I); эти две формы
совершенно эквивалентны. Если применить операцию & к обеим
частям такого соотношения эквивалентности, то мы получим,
что &A[I] и A+I тоже идентичны: A+I - адрес I-го элемента от
начала A. С другой стороны, если PA является указателем, то
в выражениях его можно использовать с индексом: PA[I] иден-
тично *(PA+I). Короче, любое выражение, включающее массивы и
индексы, может быть записано через указатели и смещения и
наоборот, причем даже в одном и том же утверждении.
Имеется одно различие между именем массива и указателем,
которое необходимо иметь в виду. указатель является перемен-
ной, так что операции PA=A и PA++ имеют смысл. Но имя масси-
ва является константой, а не переменной: конструкции типа
A=PA или A++,или P=&A будут незаконными.
Когда имя массива передается функции, то на самом деле
ей передается местоположение начала этого массива. Внутри
вызванной функции такой аргумент является точно такой же пе-
ременной, как и любая другая, так что имя массива в качестве
аргумента действительно является указателем, т.е. Перемен-
ной, содержащей адрес. мы можем использовать это обстоятель-
ство для написания нового варианта функции STRLEN, вычисляю-
щей длину строки.
STRLEN(S) /* RETURN LENGTH OF STRING S */
CHAR *S;
{
INT N;
FOR (N = 0; *S != '\0'; S++)
N++;
RETURN(N);
}
Операция увеличения S совершенно законна, поскольку эта
переменная является указателем; S++ никак не влияет на сим-
вольную строку в обратившейся к STRLEN функции, а только
увеличивает локальную для функции STRLEN копию адреса. Опи-
сания формальных параметров в определении функции в виде
CHAR S[];
CHAR *S;
совершенно эквивалентны; какой вид описания следует предпо-
честь, определяется в значительной степени тем, какие выра-
жения будут использованы при написании функции. Если функции
передается имя массива, то в зависимости от того, что удоб-
нее, можно полагать, что функция оперирует либо с массивом,
либо с указателем, и действовать далее соответвующим обра-
зом. Можно даже использовать оба вида операций, если это ка-
жется уместным и ясным.
Можно передать функции часть массива, если задать в ка-
честве аргумента указатель начала подмассива. Например, если
A - массив, то как
F(&A[2])
как и
F(A+2)
передают функции F адрес элемента A[2], потому что и &A[2],
и A+2 являются указательными выражениями, ссылающимися на
третий элемент A. внутри функции F описания аргументов могут
присутствовать в виде:
F(ARR)
INT ARR[];
{
...
}
или
F(ARR)
INT *ARR;
{
...
}
Что касается функции F, то тот факт, что ее аргумент в дейс-
твительности ссылается к части большего массива,не имеет для
нее никаких последствий.
Если 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.
Строчная константа, как, например,
"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/.
Вы, возможно, обратили внимание в предыдущих "с"-прог-