ременной. указатели очень широко используются в языке “C”.
Это происходит отчасти потому, что иногда они дают единст-
венную возможность выразить нужное действие, а отчасти пото-
му, что они обычно ведут к более компактным и эффективным
программам, чем те, которые могут быть получены другими спо-
собами.
Указатели обычно смешивают в одну кучу с операторами
GOTO, характеризуя их как чудесный способ написания прог-
рамм, которые невозможно понять. Это безусловно спрAведливо,
если указатели используются беззаботно; очень просто ввести
указатели, которые указывают на что-то совершенно неожидан-
ное. Однако, при определенной дисциплине, использование ука-
зателей помогает достичь ясности и простоты. Именно этот ас-
пект мы попытаемся здесь проиллюстрировать.
5.1. Указатели и адреса
Так как указатель содержит адрес объекта, это дает воз-
можность “косвенного” доступа к этому объекту через указа-
тель. Предположим, что х - переменная, например, типа INT, а
рх - указатель, созданный неким еще не указанным способом.
Унарная операция & выдает адрес объекта, так что оператор
рх = &х;
присваивает адрес х переменной рх; говорят, что рх “ука-
зывает” на х. Операция & применима только к переменным и
элементам массива, конструкции вида &(х-1) и &3 являются не-
законными. Нельзя также получить адрес регистровой перемен-
ной.
Унарная операция * рассматривает свой операнд как адрес
конечной цели и обращается по этому адресу, чтобы извлечь
содержимое. Следовательно, если Y тоже имеет тип INT, то
Y = *рх;
присваивает Y содержимое того, на что указывает рх. Так пос-
ледовательность
рх = &х;
Y = *рх;
присваивает Y то же самое значение, что и оператор
Y = X;
Переменные, участвующие во всем этом необходимо описать:
INT X, Y;
INT *PX;
·
99 -
с описанием для 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
·
100 -
полагает 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);
· 101 -
так как операция & выдает адрес переменной, то &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 полагает, что она работа-
ет именно с указателем.
· 102 -
Сама 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©;
RETURN©;
}
Выражение *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 - указатель целого, описанный как
· 103 -
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.
Очевидно существует очень тесное соответствие между ин-
дексацией и арифметикой указателей. в действительности ком-
пилятор преобразует ссылку на массив в указатель на начало
массива. В результате этого имя массива является указатель-
ным выражением. Отсюда вытекает несколько весьма полезных
следствий. Так как имя массива является синонимом местополо-