#DEFINE SQUARE(X) X * X
при обращении к ней, как SQUARE(Z+1)). Здесь возникают даже некоторые чисто лексические
проблемы: между именем макро и левой круглой скобкой, открывающей список ее аргументов,
не должно быть никаких пробелов.
Тем не менее аппарат макросов является весьма ценным. Один практический пример
дает описываемая в главе 7 стандар- тная библиотека ввода-вывода, в которой GETCHAR
и PUTCHAR определены как макросы (очевидно PUTCHAR должна иметь аргу- мент), что
позволяет избежать затрат на обращение к функции при обработке каждого символа.
Другие возможности макропроцессора описаны в приложении А.
Упражнение 4-9
Определите макрос SWAP(X, Y), который обменивает значе- ниями два своих аргумента
типа INT. (В этом случае поможет блочная структура).
* 5. Указатели и массивы *
Указатель - это переменная, содержащая адрес другой пе- ременной. указатели очень
широко используются в языке "C". Это происходит отчасти потому, что иногда они дают
единст- венную возможность выразить нужное действие, а отчасти пото- му, что они
обычно ведут к более компактным и эффективным программам, чем те, которые могут
быть получены другими спо- собами.
Указатели обычно смешивают в одну кучу с операторами GOTO, характеризуя их как
чудесный способ написания прог- рамм, которые невозможно понять. Это безусловно
спрAведливо, если указатели используются беззаботно; очень просто ввести указатели,
которые указывают на что-то совершенно неожидан- ное. Однако, при определенной дисциплине,
использование ука- зателей помогает достичь ясности и простоты. Именно этот ас-
пект мы попытаемся здесь проиллюстрировать.
5.1. Указатели и адреса
Так как указатель содержит адрес объекта, это дает воз- можность "косвенного"
доступа к этому объекту через указа- тель. Предположим, что х - переменная, например,
типа 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.
5.2. Указатели и аргументы функций
Так как в "с" передача аргументов функциям осуществляет- ся "по значению", вызванная
процедура не имеет непосредст- венной возможности изменить переменную из вызывающей
прог- раммы. Что же делать, если вам действительно надо изменить аргумент? например,
программа сортировки захотела бы поме- нять два нарушающих порядок элемента с помощью
функции с именем 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 в ка- честве значения функции?
5.3. Указатели и массивы
В языке "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, то тот факт, что ее аргумент в дейс- твительности ссылается
к части большего массива,не имеет для нее никаких последствий.
5.4. Адресная арифметика