Смекни!
smekni.com

ременной. указатели очень широко используются в языке “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()) == ' ' &bsol;!&bsol;! C == '&bsol;N' &bsol;!&bsol;! C == '&bsol;T'); /* SKIP WHITE SPACE */ SIGN = 1;

IF (C == '+' &bsol;!&bsol;! 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.

Очевидно существует очень тесное соответствие между ин-

дексацией и арифметикой указателей. в действительности ком-

пилятор преобразует ссылку на массив в указатель на начало

массива. В результате этого имя массива является указатель-

ным выражением. Отсюда вытекает несколько весьма полезных

следствий. Так как имя массива является синонимом местополо-