Главная страница
Навигация по странице:

  • Упражнение 5-1. Напишите функцию GETFLOAT, аналог GETINT для чисел с плавающей точкой. Какой тип должна возвращать GETFLOAT в качестве значения функции5.3. Указатели и массивы

  • 5.4. Адресная арифметика

  • 5.5. Указатели символов и функции

  • Язык С (Керниган, Ричи). Язык сиБ. В. Керниган, Д. М. Ричи


    Скачать 1.46 Mb.
    НазваниеЯзык сиБ. В. Керниган, Д. М. Ричи
    АнкорЯзык С (Керниган, Ричи).pdf
    Дата23.04.2018
    Размер1.46 Mb.
    Формат файлаpdf
    Имя файлаЯзык С (Керниган, Ричи).pdf
    ТипДокументы
    #18413
    страница11 из 23
    1   ...   7   8   9   10   11   12   13   14   ...   23
    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 является

    «Язык С» Б.В. Керниган, Д.М. Ричи
    99
    указателем на 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;

    100
    «Язык С» Б.В. Керниган, Д.М. Ричи
    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]

    «Язык С» Б.В. Керниган, Д.М. Ричи
    101
    приводит к тому, что 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 будут незаконными.
    Когда имя массива передается функции, то на самом деле ей передается

    102
    «Язык С» Б.В. Керниган, Д.М. Ричи
    местоположение начала этого массива. Внутри вызванной функции такой аргумент является точно такой же переменной, как и любая другая, так что имя массива в качестве аргумента действительно является указателем, т.е. Пере- менной, содержащей адрес. мы можем использовать это обстоятельство для написания нового варианта функции 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[];

    «Язык С» Б.В. Керниган, Д.М. Ричи
    103
    или
    F(ARR)
    INT *ARR;
    Что касается функции F, то тот факт, что ее аргумент в действительности ссылается к части большего массива,не имеет для нее никаких последствий.
    5.4. Адресная арифметика
    Если 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. Так как они работают с указателями, а не с индексами массива, никакой другой функции не нужно знать имя этого массива. Он может быть описан как внешний статический, т.е. Он будет локальным по отношению к исходному файлу,

    104
    «Язык С» Б.В. Керниган, Д.М. Ричи
    содержащему 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, т.е. На первую свободную позицию при начале работы программы. Так как имя массива является адресом его нулевого элемента, то это можно было бы записать в виде

    «Язык С» Б.В. Керниган, Д.М. Ричи
    105
    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. например, на

    106
    «Язык С» Б.В. Керниган, Д.М. Ричи
    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.
    5.5. Указатели символов и функции
    Строчная константа, как, например,

    «Язык С» Б.В. Керниган, Д.М. Ричи
    107
    “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 с указателями.

    108
    «Язык С» Б.В. Керниган, Д.М. Ричи
    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 S0 IF S>T */
    CHAR S[], T[];
    INT I;

    «Язык С» Б.В. Керниган, Д.М. Ричи
    109
    IF (S[I++] == ‘\0’)
    RETURN(0);
    RETURN(S[I]-T[I]);
    Вот версия STRCMP с указателями:
    STRCMP(S, T) /* RETURN <0 IF S0 IF S>T */
    CHAR *S, *T;
    FOR ( ; *S == *T; S++, T++)
    IF (*S == ‘\0’)
    RETURN(0);
    RETURN(*S-*T);
    так как ++ и — могут быть как постфиксными, так и префиксными операциями, встречаются другие комбинации * и ++ и —, хотя и менее часто.
    Например
    *++P
    увеличивает P до извлечения символа, на который указывает
    P, а
    *—P
    сначала уменьшает P.
    1   ...   7   8   9   10   11   12   13   14   ...   23


    написать администратору сайта