RuLibrary.com

ГЛАВНАЯ | ПОИСК | ТОП | КАРТА САЙТА      

 
 


 

Керниган Ричи >> Язык C (страница 14)


#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. Адресная арифметика

Название книги: Язык C
Автор: Керниган Ричи
Просмотрено 45110 раз

123456789101112131415161718192021222324252627282930313233


 
Page generation 0.003 seconds