Назад | Оглавление | Вперед |
Указатель - это переменная, содержащая адрес другой переменной. указатели очень широко используются в языке C. Это происходит отчасти потому, что иногда они дают единственную возможность выразить нужное действие, а отчасти потому, что они обычно ведут к более компактным и эффективным программам, чем те, которые могут быть получены другими способами.
Указатели обычно смешивают в одну кучу с операторами goto, характеризуя их как чудесный способ написания программ, которые невозможно понять. Это безусловно справедливо, если указатели используются беззаботно; очень просто ввести указатели, которые указывают на что-то совершенно неожиданное. Однако, при определенной дисциплине, использование указателей помогает достичь ясности и простоты. Именно этот аспект мы попытаемся здесь проиллюстрировать.
Так как указатель содержит адрес объекта, это дает возможность "косвенного" доступа к этому объекту через указатель. Предположим, что х - переменная, например, типа int, а рх - указатель, созданный неким еще не указанным способом. Унарная операция & выдает адрес объекта, так что оператор
Унарная операция * рассматривает свой операнд как адрес конечной цели и обращается по этому адресу, чтобы извлечь содержимое. Следовательно, если y тоже имеет тип int, то
Описание указателя
Вы должны также заметить, что из этого описания следует, что указатель может указывать только на определенный вид объектов.
Указатели могут входить в выражения. Например, если px указывает на целое x, то *px может появляться в любом контексте, где может встретиться x. Так оператор
В выражениях вида
Ссылки на указатели могут появляться и в левой части присваиваний. Если px указывает на x, то
Круглые скобки в последнем примере необходимы; если их опустить, то поскольку унарные операции, подобные * и ++, выполняются справа налево, это выражение увеличит px, а не ту переменную, на которую он указывает.
И наконец, так как указатели являются переменными, то с ними можно обращаться, как и с остальными переменными. Если py - другой указатель на переменную типа int, то
Так как в C передача аргументов функциям осуществляется "по значению", вызванная процедура не имеет непосредственной возможности изменить переменную из вызывающей программы. Что же делать, если вам действительно надо изменить аргумент? например, программа сортировки захотела бы поменять два нарушающих порядок элемента с помощью функции с именем swap. Для этого недостаточно написать
К счастью, все же имеется возможность получить желаемый эффект. Вызывающая программа передает указатели подлежащих изменению значений:
Указатели в качестве аргументов обычно используются в функциях, которые должны возвращать более одного значения. (Можно сказать, что swap возвращает два значения, новые значения ее аргументов). В качестве примера рассмотрим функцию
getint, которая осуществляет преобразование поступающих в свободном формате данных, разделяя поток символов на целые значения, по одному целому за одно обращение. Функция getint должна возвращать либо найденное значение, либо признак конца файла, если входные данные полностью исчерпаны. Эти значения должны возвращаться как отдельные объекты, какое бы значение ни использовалось для EOF, даже если это значение вводимого целого.
Одно из решений, основывающееся на описываемой в главе 7 функции ввода scanf, состоит в том, чтобы при выходе на конец файла getint возвращала EOF в качестве значения функции; любое другое возвращенное значение говорит о нахождении нормального целого. Численное же значение найденного целого возвращается через аргумент, который должен быть указателем целого. Эта организация разделяет статус конца файла и численные значения.
Следующий цикл заполняет массив целыми с помощью обращений к функции getint:
В результате каждого обращения v становится равным следующему целому значению, найденному во входных данных. Обратите внимание, что в качестве аргумента getint необходимо указать &v а не v. Использование просто v скорее всего приведет к ошибке адресации, поскольку getint полагает, что она работает именно с указателем.
Сама getint является очевидной модификацией написанной нами ранее функции atoi:
Выражение *pn используется всюду в getint как обычная переменная типа int. Мы также использовали функции getch и ungetch (описанные в главе 4) , так что один лишний символ, кототрый приходится считывать, может быть помещен обратно во ввод.
Упражнение 5-1. Напишите функцию getfloat, аналог getint для чисел с плавающей точкой. Какой тип должна возвращать getfloat в качестве значения функции?
В языке C существует сильная взаимосвязь между указателями и массивами , настолько сильная, что указатели и массивы действительно следует рассматривать одновременно. Любую операцию, которую можно выполнить с помощью индексов масси-
ва, можно сделать и с помощью указателей. вариант с указателями обычно оказывается более быстрым, но и несколько более трудным для непосредственного понимания, по крайней мере для начинающего. описание
Если pa указывает на некоторый определенный элемент массива a, то по определению pa+1 указывает на следующий элемент, и вообще pa-i указывает на элемент, стоящий на i позиций до элемента, указываемого pa, а pa+i на элемент, стоящий на i позиций после. Таким образом, если pa указывает на a[0], то
Эти замечания справедливы независимо от типа переменных в массиве a. Суть определения "добавления 1 к указателю", а также его распространения на всю арифметику указателей, состоит в том, что приращение масштабируется размером памяти, занимаемой объектом, на который указывает указатель. Таким образом, i в pa+i перед прибавлением умножается на размер объектов, на которые указывает pa.
Очевидно существует очень тесное соответствие между индексацией и арифметикой указателей. в действительности компилятор преобразует ссылку на массив в указатель на начало массива. В результате этого имя массива является указательным выражением. Отсюда вытекает несколько весьма полезных следствий. Так как имя массива является синонимом местоположения его нулевого элемента, то присваивание pa=&a[0] можно записать как
Еще более удивительным, по крайней мере на первый взгляд, кажется тот факт, что ссылку на 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++,или pa=&a будут незаконными.
Когда имя массива передается функции, то на самом деле ей передается местоположение начала этого массива. Внутри вызванной функции такой аргумент является точно такой же переменной, как и любая другая, так что имя массива в качестве аргумента действительно является указателем, т.е. Переменной, содержащей адрес. мы можем использовать это обстоятельство для написания нового варианта функции strlen, вычисляющей длину строки.
Операция увеличения s совершенно законна, поскольку эта переменная является указателем; s++ никак не влияет на символьную строку в обратившейся к strlen функции, а только увеличивает локальную для функции strlen копию адреса. Описания формальных параметров в определении функции в виде
Можно передать функции часть массива, если задать в качестве аргумента указатель начала подмассива. Например, если a - массив, то как
Что касается функции f, то тот факт, что ее аргумент в действительности ссылается к части большего массива, не имеет для нее никаких последствий.
Если 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. Так как они работают с указателями, а не с индексами массива, никакой другой функции не нужно знать имя этого массива. Он может быть описан как внешний статический, т.е. Он будет локальным по отношению к исходному файлу, содержащему alloc и free, и невидимым за его пределами. При практической реализации этот массив может даже не иметь имени; вместо этого он может быть получен в результате запроса к операционной системе на указатель некоторого неименованного блока памяти.
Другой необходимой информацией является то, какая часть массива allocbuf уже использована. Мы пользуемся указателем первого свободного элемента, названным allocp. Когда к функции alloc обращаются за выделением n символов, то она проверяет, достаточно ли осталось для этого места в allocbuf. Если достаточно, то alloc возвращает текущее значение allocp (т.е. Начало свободного блока), затем увеличивает его на n, с тем чтобы он указывал на следующую свободную область. Функция free(p) просто полагает allocp равным p при условии, что p указывает на позицию внутри allocbuf.
Дадим некоторые пояснения. Вообще говоря, указатель может быть инициализирован точно так же, как и любая другая переменная, хотя обычно единственными осмысленными значениями являются NULL (это обсуждается ниже) или выражение, включающее адреса ранее определенных данных соответствующего типа. Описание
Проверки вида
Во-вторых, как мы уже видели, указатель и целое можно складывать и вычитать. Конструкция
Вычитание указателей тоже возможно: если p и q указывают на элементы одного и того же массива, то p-q - количество элементов между p и q. Этот факт можно использовать для написания еще одного варианта функции
При описании указатель p в этой функции инициализирован посредством строки s, в результате чего он указывает на первый символ строки. В цикле while по очереди проверяется каждый символ до тех пор, пока не появится символ конца строки \0. Так как значение \0 равно нулю, а while только выясняет, имеет ли выражение в нем значение 0, то в данном случае явную проверку можно опустить. Такие циклы часто записывают в виде
Так как p указывает на символы, то оператор p++ передвигает p каждый раз так, чтобы он указывал на следующий символ. В результате p-s дает число просмотренных символов, т.е. Длину строки. Арифметика указателей последовательна: если бы мы имели дело с переменными типа float, которые занимают больше памяти, чем переменные типа char, и если бы p был указателем на float, то оператор p++ передвинул бы p на следующее float. Таким образом, мы могли бы написать другой вариант функции alloc, распределяющей память для float, вместо char, просто заменив всюду в alloc и free описатель char на float. Все действия с указателями автоматически учитывают размер объектов, на которые они указывают, так что больше ничего менять не надо.
За исключением упомянутых выше операций (сложение и вычитание указателя и целого, вычитание и сравнение двух указателей), вся остальная арифметика указателей является незаконной. Запрещено складывать два указателя, умножать, делить, сдвигать или маскировать их, а также прибавлять к ним переменные типа float или double.
Строчная константа, как, например,
По-видимому чаще всего строчные константы появляются в качестве аргументов функций, как, например, в
Конечно, символьные массивы не обязаны быть только аргументами функций. Если описать message как
Мы проиллюстрируем другие аспекты указателей и массивов, разбирая две полезные функции из стандартной библиотеки ввода-вывода, которая будет рассмотрена в главе 7.
Первая функция - это strcpy(s,t), которая копирует строку т в строку s. Аргументы написаны именно в этом порядке по
аналогии с операцией присваивания, когда для того, чтобы
присвоить t к s обычно пишут
Для сопоставления ниже дается вариант strcpy с указателями.
Так как аргументы передаются по значению, функция strcpy может использовать s и t так, как она пожелает. Здесь они с удобством полагаются указателями, которые передвигаются вдоль массивов, по одному символу за шаг, пока не будет скопирован в s завершающий в t символ \0.
На практике функция strcpy была бы записана не так, как мы показали выше. Вот вторая возможность:
Здесь увеличение s и t внесено в проверочную часть. Значением *t++ является символ, на который указывал t до увеличения; постфиксная операция ++ не изменяет t, пока этот символ не будет извлечен. Точно так же этот символ помещается в старую позицию s, до того как s будет увеличено. Конечный результат заключается в том, что все символы, включая завершающий \0, копируются из t в s.
И как последнее сокращение мы опять отметим, что сравнение с \0 является излишним, так что функцию можно записать в виде
Вторая функция - strcmp(s, t), которая сравнивает символьные строки s и т, возвращая отрицательное, нулевое или положительное значение в соответствии с тем, меньше, равно или больше лексикографически s, чем t. Возвращаемое значение получается в результате вычитания символов из первой позиции, в которой s и t не совпадают.
Вот версия strcmp с указателями:
Так как ++ и -- могут быть как постфиксными, так и префиксными операциями, встречаются другие комбинации * и ++ и --, хотя и менее часто.
Например
Упражнение 5-2. Напишите вариант с указателями функции strcat из главы 2: strcat(s, t) копирует строку t в конец s.
Упражнение 5-3. Напишите макрос для strcpy.
Упражнение 5-4. Перепишите подходящие программы из предыдущих глав и упражнений, используя указатели вместо индексации массивов. Хорошие возможности для этого предоставляют функции getline (главы 1 и 4), atoi, itoa и их варианты (главы 2, 3 и 4), reverse (глава 3), index и getlop (глава 4).
Вы, возможно, обратили внимание в предыдущих C-программах на довольно непринужденное отношение к копированию указателей. В общем это верно, что на большинстве машин указатель можно присвоить целому и передать его обратно, не изменив его; при этом не происходит никакого масштабирования или преобразования и ни один бит не теряется. К сожалению, это ведет к вольному обращению с функциями, возвращающими указатели, которые затем просто передаются другим функциям, - необходимые описания указателей часто опускаются. Рассмотрим, например, функцию strsave(s), которая копирует строку s в некоторое место для хранения, выделяемое посредством обращения к функции alloc, и возвращает указатель на это место. Правильно она должна быть записана так:
Эта программа будет правильно работать на многих машинах, потому что по умолчанию функции и аргументы имеют тип int, а указатель и целое обычно можно безопасно пересылать туда и обратно. Однако такой стиль программирования в своем существе является рискованным, поскольку зависит от деталей реализации и архитектуры машины и может привести к неправильным результатам на конкретном используемом вами компиляторе. Разумнее всюду использовать полные описания. (Отладочная программа LINT предупредит о таких конструкциях, если они по неосторожности все же появятся).
В языке C предусмотрены прямоугольные многомерные массивы, хотя на практике существует тенденция к их значительно более редкому использованию по сравнению с массивами указателей. В этом разделе мы рассмотрим некоторые их свойства.
Рассмотрим задачу преобразования дня месяца в день года и наоборот. Например, 1-ое марта является 60-м днем невисокосного года и 61-м днем високосного года. Давайте введем две функции для выполнения этих преобразований: day_of_year преобразует месяц и день в день года, а month_day преобразует день года в месяц и день. Так как эта последняя функция возвращает два значения, то аргументы месяца и дня должны быть указателями:
Обе эти функции нуждаются в одной и той же информационной таблице, указывающей число дней в каждом месяце. Так как число дней в месяце в високосном и в невисокосном году отличается, то проще представить их в виде двух строк двумерного массива, чем пытаться прослеживать во время вычислений, что именно происходит в феврале. Вот этот массив и выполняющие эти преобразования функции:
Массив day_tab должен быть внешним как для day_of_year, так и для month_day, поскольку он используется обеими этими функциями.
Массив day_tab является первым двумерным массивом, с которым мы имеем дело. По определению в C двумерный массив по существу является одномерным массивом, каждый элемент которого является массивом. Поэтому индексы записываются как
Массив инициализируется с помощью списка начальных значений, заключенных в фигурные скобки; каждая строка двумерного массива инициализируется соответствующим подсписком. Мы поместили в начало массива day_tab столбец из нулей для того, чтобы номера месяцев изменялись естественным образом от 1 до 12, а не от 0 до 11. Так как за экономию памяти у нас пока не награждают, такой способ проще, чем подгонка индексов.
Если двумерный массив передается функции, то описание соответствующего аргумента функции должно содержать количество столбцов; количество строк несущественно, поскольку, как и прежде, фактически передается указатель. В нашем конкретном случае это указатель объектов, являющихся массивами из 13 чисел типа int. Таким образом, если бы требовалось передать массив day_tab функции f, то описание в f имело бы вид:
Так как указатели сами являются переменными, то вы вполне могли бы ожидать использования массива указателей. Это действительно так. Мы проиллюстрируем это написанием программы сортировки в алфавитном порядке набора текстовых строк, предельно упрощенного варианта утилиты sort операционной систем UNIX.
В главе 3 мы привели функцию сортировки по Шеллу, которая упорядочивала массив целых. Этот же алгоритм будет работать и здесь, хотя теперь мы будем иметь дело со строчками текста различной длины, которые, в отличие от целых, нельзя сравнивать или перемещать с помощью одной операции. Мы нуждаемся в таком представлении данных, которое бы позволяло удобно и эффективно обрабатывать строки текста переменной длины.
Здесь и возникают массивы указателей. Если подлежащие сортировке сроки хранятся одна за другой в длинном символьном массиве (управляемом, например, функцией alloc), то к каждой строке можно обратиться с помощью указателя на ее первый символ. Сами указатели можно хранить в массиве. две строки можно сравнить, передав их указатели функции strcmp. Если две расположенные в неправильном порядке строки должны быть переставлены, то фактически переставляются указатели в массиве указателей, а не сами тексты строк. Этим исключаются сразу две связанные проблемы: сложного управления памятью и больших дополнительных затрат на фактическую перестановку строк.
Процесс сортировки включает три шага:
Как обычно, лучше разделить программу на несколько функций в соответствии с естественным делением задачи и выделить ведущую функцию, управляющую работой всей программы.
Давайте отложим на некоторое время рассмотрение шага сортировки и сосредоточимся на структуре данных и вводе-выводе. Функция, осуществляющая ввод, должна извлечь символы каждой строки, запомнить их и построить массив указателей строк. Она должна также подсчитать число строк во вводе, так как эта информация необходима при сортировке и выводе. Так как функция ввода в состоянии справиться только с конечным числом вводимых строк, в случае слишком большого их числа она
может возвращать некоторое число, отличное от возможного числа строк, например -1. Функция осуществляющая вывод, должна печатать строки в том порядке, в каком они появляются в массиве указателей.
Символ новой строки в конце каждой строки удаляется, так что он никак не будет влиять на порядок, в котором сортируются строки.
Существенно новым в этой программе является описание
Так как сам lineptr является массивом, который передается функции writelines, с ним можно обращаться как с указателем точно таким же образом, как в наших более ранних примерах. Тогда последнюю функцию можно переписать в виде:
Справившись с вводом и выводом, мы можем перейти к сортировке. программа сортировки по Шеллу из главы 3 требует очень небольших изменений: должны быть модифицированы описания, а операция сравнения выделена в отдельную функцию. Основной алгоритм остается тем же самым, и это дает нам определенную уверенность, что он по-прежнему будет работать.
Так как каждый отдельный элемент массива v (имя формального параметра, соответствующего lineptr) является указателем на символы, то и temp должен быть указателем на символы, чтобы их было можно копировать друг в друга.
Мы написали эту программу по возможности более просто с тем, чтобы побыстрее получить работающую программу. Она могла бы работать быстрее, если, например, вводить строки непосредственно в массив, управляемый функцией readlines, а не копировать их в line, а затем в скрытое место с помощью функции alloc. но мы считаем, что будет разумнее первоначальный вариант сделать более простым для понимания, а об "эффективности" позаботиться позднее. Все же, по-видимому, способ, позволяющий добиться заметного ускорения работы программы, состоит не в исключении лишнего копирования вводимых строк. Более вероятно, что существенной разницы можно достичь за счет замены сортировки по Шеллу на нечто лучшее, например, на метод быстрой сортировки.
В главе 1 мы отмечали, что поскольку в циклах while и for проверка осуществляется до того, как тело цикла выполнится хотя бы один раз, эти циклы оказываются удобными для обеспечения правильной работы программы при граничных значениях, в частности, когда ввода вообще нет. Очень полезно просмотреть все функции программы сортировки, разбираясь, что происходит, если вводимый текст отсутствует.
Упражнение 5-5. Перепишите функцию readlines таким образом, чтобы она помещала строки в массив, предоставляемый функцией main, а не в память, управляемую обращениями к функции alloc. Насколько быстрее стала программа?
Рассмотрим задачу написания функции month_name(n), которая возвращает указатель на символьную строку, содержащую имя n-го месяца. Это идеальная задача для применения внутреннего статического массива. Функция month_name содержит локальный массив символьных строк и при обращении к ней возвращает указатель нужной строки. Тема настоящего раздела - как инициализировать этот массив имен.
Описание массива указателей на символы name точно такое же, как аналогичное описание lineptr в примере с сортировкой. Инициализатором является просто список символьных строк; каждая строка присваивается соответствующей позиции в массиве. Более точно, символы i-ой строки помещаются в какое-то иное место, а ее указатель хранится в name[i]. Поскольку размер массива name не указан, компилятор сам подсчитывает количество инициализаторов и соответственно устанавливает правильное число.
Начинающие изучать язык C иногда становятся в тупик перед вопросом о различии между двумерным массивом и массивом указателей, таким как name в риведенном выше примере. Если имеются описания
Хотя мы вели это обсуждение в терминах целых, несомненно, чаще всего массивы указателей используются так, как мы продемонстрировали на функции month_name, - для хранения символьных строк различной длины.
Упражнение 5-6. Перепишите функции day_of_year и month_day, используя вместо индексации указатели.
Системные средства, на которые опирается реализация языка C, позволяют передавать командную строку аргументов или параметров начинающей выполняться программе. Когда функция main вызывается к исполнению, она вызывается с двумя аргументами. Первый аргумент (условно называемый argc) указывает число аргументов в командной строке, с которыми происходит обращение к программе; второй аргумент (argv) является указателем на массив символьных строк, содержащих эти аргументы, по одному в строке. Работа с такими строками - это обычное использование многоуровневых указателей.
Самую простую иллюстрацию этой возможности и необходимых при этом описаний дает программа echo, которая просто печатает в одну строку аргументы командной строки, разделяя их пробелами. Таким образом, если дана команда
Поскольку argv является указателем на массив указателей, то существует несколько способов написания этой программы, использующих работу с указателем, а не с индексацией массива. Мы продемонстрируем два варианта.
Так как argv является указателем на начало массива строк-аргументов, то, увеличив его на 1 (++argv), мы вынуждаем его указывать на подлинный аргумент argv[1], а не на argv[0]. Каждое последующее увеличение передвигает его на следующий аргумент; при этом *argv становится указателем на этот аргумент. одновременно величина argc уменьшается; когда она обратится в нуль, все аргументы будут уже напечатаны.
Другой вариант:
Эта версия показывает, что аргумент формата функции printf может быть выражением, точно так же, как и любой другой. Такое использование встречается не очень часто, но его все же стоит запомнить.
Как второй пример, давайте внесем некоторые усовершенствования в программу отыскания заданной комбинации символов из главы 4. Если вы помните, мы поместили искомую комбинацию глубоко внутрь программы, что очевидно является совершенно неудовлетворительным. Следуя утилите GREP системы UNIX, давайте изменим программу так, чтобы эта комбинация указывалась в качестве первого аргумента строки.
Теперь может быть развита основная модель, иллюстрирующая дальнейшее использование указателей. Предположим, что нам надо предусмотреть два необязательных аргумента. Один утверждает: "напечатать все строки за исключением тех, которые содержат данную комбинацию", второй гласит: "перед каждой выводимой строкой должен печататься ее номер".
Общепринятым соглашением в C-программах является то, что аргумент, начинающийся со знака минус, вводит необязательный признак или параметр. Если мы, для того, чтобы сообщить об инверсии, выберем -x, а для указания о нумерации нужных строк выберем -n("номер"), то команда
Нужно, чтобы необязательные аргументы могли располагаться в произвольном порядке, и чтобы остальная часть программы не зависела от количества фактически присутствующих аргументов. в частности, вызов функции index не должен содержать ссылку на argv[2], когда присутствует один необязательный аргумент, и на argv[1], когда его нет. Более того, для пользователей удобно, чтобы необязательные аргументы можно было объединить в виде:
Аргумент argv увеличивается перед каждым необязательным аргументом, в то время как аргумент argc уменьшается. если нет ошибок, то в конце цикла величина argc должна равняться 1, а *argv должно указывать на заданную комбинацию. Обратите внимание на то, что *++argv является указателем аргументной строки; (*++argv)[0] - ее первый символ. Круглые скобки здесь необходимы, потому что без них выражение бы приняло совершенно отличный (и неправильный) вид *++(argv[0]). Другой правильной формой была бы **++argv.
Упражнение 5-7. Напишите программу add, вычисляющую обратное польское выражение из командной строки. Например,
Упражнение 5-8. Модифицируйте программы entab и detab (указанные в качестве упражнений в главе 1) так, чтобы они получали список табуляционных остановок в качестве аргументов. Если аргументы отсутствуют, используйте стандартную установку табуляций.
Упражнение 5-9. Расширьте entab и detab таким образом, чтобы они воспринимали сокращенную нотацию
Упражнение 5-10. В языке C сами функции не являются переменными, но имеется возможность определить указатель на функцию, который можно обрабатывать, передавать другим функциям, помещать в массивы и т.д. Мы проиллюстрируем это, проведя модификацию написанной ранее программы сортировки так, чтобы при задании необязательного аргумента -n она бы сортировала строки ввода численно, а не лексикографически.
Сортировка часто состоит из трех частей - сравнения, которое определяет упорядочивание любой пары объектов, перестановки, изменяющей их порядок, и алгоритма сортировки, осуществляющего сравнения и перестановки до тех пор, пока объекты не расположатся в нужном порядке. Алгоритм сортировки не зависит от операций сравнения и перестановки, так что, передавая в него различные функции сравнения и перестановки, мы можем организовать сортировку по различным критериям. Именно такой подход используется в нашей новой программе
сортировки.
Как и прежде, лексикографическое сравнение двух строк осуществляется функцией strcmp, а перестановка функцией swap; нам нужна еще функция numcmp, сравнивающая две строки на основе численного значения и возвращающая условное указание того же вида, что и strcmp. Эти три функции описываются в main и указатели на них передаются в sort. В свою очередь функция sort обращается к этим функциям через их указатели. Мы урезали обработку ошибок в аргументах с тем, чтобы сосредоточиться на главных вопросах.
Здесь strcmp, numcmp и swap - адреса функций; так как известно, что это функции, операция & здесь не нужна совершенно аналогично тому, как она не нужна и перед именем массива. Передача адресов функций организуется компилятором. Второй шаг состоит в модификации sort:
Здесь следует обратить определенное внимание на описания. Описание
Использование comp в строке
Мы уже приводили функцию strcmp, сравнивающую две строки по первому численному значению:
Заключительный шаг состоит в добавлении функции swap, переставляющей два указателя. Это легко сделать, непосредственно используя то, что мы изложили ранее в этой главе.
Имеется множество других необязательных аргументов, которые могут быть включены в программу сортировки: некоторые из них составляют интересные упражнения.
Упражнение 5-11. Модифицируйте sort таким образом, чтобы она работала с меткой -r, указывающей на сортировку в обратном (убывающем) порядке. Конечно, -r должна работать с -n.
Упражнение 5-12. Добавьте необязательный аргумент -f, объединяющий вместе прописные и строчные буквы, так чтобы различие регистров не учитывалось во время сортировки: данные из верхнего и нижнего регистров сортируются вместе, так что буква 'A' прописное и 'а' строчное оказываются соседними , а не разделенными целым алфавитом.
Упражнение 5-13. Добавьте необязательный аргумент -d ("словарное упорядочивание"), при наличии которого сравниваются только буквы, числа и пробелы. Позаботьтесь о том, чтобы эта функция работала и вместе с -f.
Упражнение 5-14. Добавьте возможность обработки полей, так чтобы можно было сортировать поля внутри строк. Каждое поле должно сортироваться в соответствии с независимым набором необязательных аргументов. (предметный указатель этой книги сортировался с помощью аргументов -df для категории указателя и с -n для номеров страниц).
5.1. Указатели и адреса
рх = &х;
присваивает адрес х переменной рх; говорят, что рх "указывает" на х. Операция & применима только к переменным и элементам массива, конструкции вида &(х-1) и &3 являются незаконными. Нельзя также получить адрес регистровой переменной.
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.
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 = 0
полагает x равным нулю, а
*px += 1
увеличивает его на единицу, как и выражение
(*px)++
py = px
копирует содержимое px в py, в результате чего py указывает на то же, что и px.
5.2. Указатели и аргументы функций
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;
}
int n, v, array[size];
for (n = 0; n < size && getint(&v) != EOF; n++)
array[n] = v;
getint(pn) /* get next integer from input */
int *pn;
{
int c,sign;
while ((c = getch()) == ' ' || c == '\n'
|| c == '\t'); /* skip white space */
sign = 1;
(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);
}
5.3. Указатели и массивы
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+1)
ссылается на содержимое a[1], pa+i - адрес a[i], а *(pa+i) - содержимое a[i].
pa = a
strlen(s) /* return length of string s */
char *s;
{
int n;
for (n = 0; *s != '\0'; s++)
n++;
return(n);
}
char s[];
char *s;
совершенно эквивалентны; какой вид описания следует предпочесть, определяется в значительной степени тем, какие выражения будут использованы при написании функции. Если функции передается имя массива, то в зависимости от того, что удобнее, можно полагать, что функция оперирует либо с массивом, либо с указателем, и действовать далее соответствующим образом. Можно даже использовать оба вида операций, если это кажется уместным и ясным.
f(&a[2])
как и
f(a+2)
передают функции f адрес элемента a[2], потому что и &a[2], и a+2 являются указательными выражениями, ссылающимися на третий элемент a. внутри функции f описания аргументов могут присутствовать в виде:
f(arr)
int arr[];
{
...
}
или
f(arr)
int *arr;
{
...
}
5.4. Адресная арифметика
#define NULL 0 /* pointer value for error report */
#define ALLOCSIZE 1000 /* size of available space */
static char allocbuf[ALLOCSIZE];/* storage for alloc */
static char *allocp = allocbuf; /* next free position */
char *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);
)
free(p) /* free storage pionted by p */
char *p;
(
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
allocp = p;
)
static char *allocp = allocbuf;
определяет allocp как указатель на символы и инициализирует его так, чтобы он указывал на allocbuf, т.е. На первую свободную позицию при начале работы программы. Так как имя массива является адресом его нулевого элемента, то это можно было бы записать в виде
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. Например, на PDP-11 масштабирующий множитель равен 1 для char, 2 для int и short, 4 для long и float и 8 для double.
strlen:
strlen(s) /* return length of string s */
char *s;
{
char *p = s;
while (*p != '\0')
p++;
return(p-s);
}
while (*p)
p++;
5.5. Указатели символов и функции
"i am a string"
является массивом символов. Компилятор завершает внутреннее представление такого массива символом \0, так что программы могут находить его конец. Таким образом, длина массива в памяти оказывается на единицу больше числа символов между двойными кавычками.
printf ("hello, world\n");
когда символьная строка, подобная этой, появляется в программе, то доступ к ней осуществляется с помощью указателя символов; функция printf фактически получает указатель символьного массива.
char *message;
то в результате оператора
message = "now is the time";
переменная message станет указателем на фактический массив символов. Это не копирование строки; здесь участвуют только указатели. в языке C не предусмотрены какие-либо операции для обработки всей строки символов как целого.
s = t
сначала приведем версию с массивами:
strcpy(s, t) /* copy t to s */
char s[], t[];
{
int i;
i = 0;
while ((s[i] = t[i]) != '\0')
i++;
}
strcpy(s, t) /* copy t to s; pointer version 1 */
char *s, *t;
{
while ((*s = *t) != '\0') {
s++;
t++;
}
}
strcpy(s, t) /* copy t to s; pointer version 2 */
char *s, *t;
{
while ((*s++ = *t++) != '\0')
;
}
strcpy(s, t) /* copy t to s; pointer version 3 */
char *s, *t;
{
while (*s++ = *t++)
;
}
хотя с первого взгляда эта запись может показаться загадочной, она дает значительное удобство. Этой идиомой следует овладеть уже хотя бы потому, что вы с ней будете часто встречаться в C-программах.
strcmp(s, t) /* return <0 if s<t, 0 if s==t, >0 if s>t */
char s[], t[];
{
int i;
i = 0;
while (s[i] == t[i])
if (s[i++] == '\0')
return(0);
return(s[i]-t[i]);
}
strcmp(s, t) /* return <0 if s<t, 0 if s==t, >0 if s>t */
char *s, *t;
{
for ( ; *s == *t; s++, t++)
if (*s == '\0')
return(0);
return(*s-*t);
}
*++p
увеличивает p до извлечения символа, на который указывает p, а
*--p
сначала уменьшает p.
5.6. Указатели - не целые.
char *strsave(s) /* save string s somewhere */
char *s;
{
char *p, *alloc();
if ((p = alloc(strlen(s)+1)) != NULL)
strcpy(p, s);
return(p);
}
на практике существует сильное стремление опускать описания:
*strsave(s) /* save string s somewhere */
{
char *p;
if ((p = alloc(strlen(s)+1)) != NULL)
strcpy(p, s);
return(p);
}
5.7. Многомерные массивы.
month_day(1977, 60, &m, &d)
Полагает m равным 3 и d равным 1 (1-ое марта).
static int day_tab[2][13] = {
(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31),
(0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
};
day_of_year(year, month, day) /* set day of year */
int year, month, day; /* from month & day */
{
int i, leap;
leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
for (i = 1; i < month; i++)
day += day_tab[leap][i];
return(day);
{
month_day(year, yearday, pmomth, pday) /*set month,day */
int year, yearday, *pmomth, *pday; /* from day of year */
{
leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
for (i = 1; yearday > day_tab[leap][i]; i++)
yearday -= day_tab[leap][i];
*pmomth = i;
*pday = yearday;
}
day_tab[i][j]
а не
day_tab [i, j]
как в большинстве языков. В остальном с двумерными массивами можно в основном обращаться таким же образом, как в других языках. Элементы хранятся по строкам, т.е. При обращении к элементам в порядке их размещения в памяти быстрее всего изменяется самый правый индекс.
f(day_tab)
int day_tab[2][13];
{
...
}
Так как количество строк является несущественным, то описание аргумента в f могло бы быть таким:
int day_tab[][13];
или таким
int (*day_tab)[13];
в котором говорится, что аргумент является указателем массива из 13 целых. Круглые скобки здесь необходимы, потому что квадратные скобки [] имеют более высокий уровень старшинства, чем *; как мы увидим в следующем разделе, без круглых скобок
int *day_tab[13];
является описанием массива из 13 указателей на целые.
5.8. Массивы указателей; указатели указателей
#define NULL 0
#define LINES 100 /* max lines to be sorted */
main() /* sort input lines */
{
char *lineptr[LINES]; /*pionters to text lines */
int nlines; /* number of input lines read */
if ((nlines = readlines(lineptr, LINES)) >= 0) {
sort(lineptr, nlines);
writelines(lineptr, nlines);
}
else
printf("input TOO BIG to sort\n");
}
#define MAXLEN 1000
readlines(lineptr, maxlines) /* read input lines */
char *lineptr[]; /* for sorting */
int maxlines;
{
int len, nlines;
char *p, *alloc(), line[MAXLEN];
nlines = 0;
while ((len = getline(line, MAXLEN)) > 0)
if (nlines >= maxlines)
return(-1);
else if ((p = alloc(len)) == NULL)
return (-1);
else {
line[len-1] = '\0'; /* zap newline */
strcpy(p,line);
lineptr[nlines++] = p;
}
return(nlines);
}
writelines(lineptr, nlines) /* write output lines */
char *lineptr[];
int nlines;
{
int i;
for (i = 0; i < nlines; i++)
printf("%s\n", lineptr[i]);
}
char *lineptr[LINES];
которое сообщает, что lineptr является массивом из LINES элементов, каждый из которых - указатель на переменные типа char. Это означает, что lineptr[i] - указатель на символы, а *lineptr[i] извлекает символ.
writelines(lineptr, nlines) /* write output lines */
char *lineptr[];
int nlines;
{
int i;
while (--nlines >= 0)
printf("%s\n", *lineptr++);
}
здесь *lineptr сначала указывает на первую строку; каждое увеличение передвигает указатель на следующую строку, в то время как nlines убывает до нуля.
sort(v, n) /* sort strings v[0] ... v[n-1] */
char *v[]; /* into increasing order */
int n;
{
int gap, i, j;
char *temp;
for (gap = n/2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0; j -= gap) {
if (strcmp(v[j], v[j+gap]) <= 0)
break;
temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
5.9. Инициализация массивов указателей.
char *month_name(n) /* return name of n-th month */
int n;
{
static char *name[] = {
"illegal month",
"JANUARY",
"FEBRUARY",
"MARCH",
"APRIL",
"MAY",
"JUN",
"JULY",
"AUGUST",
"SEPTEMBER",
"OCTOBER",
"NOVEMBER",
"DECEMBER"
};
return ((n < 1 || n > 12) ? name[0] : name[n]);
}
5.10. Указатели и многомерные массивы
int a[10][10];
int *b[10];
то a и b можно использовать сходным образом в том смысле, что как a[5][5], так и b[5][5] являются законными ссылками на отдельное число типа int. Но a - настоящий массив: под него отводится 100 ячеек памяти и для нахождения любого указанного элемента проводятся обычные вычисления с прямоугольными индексами. Для b, однако, описание выделяет только 10 указателей; каждый указатель должен быть установлен так, чтобы он указывал на массив целых. если предположить, что каждый из них указывает на массив из 10 элементов, то тогда где-то будет отведено 100 ячеек памяти плюс еще десять ячеек для указателей. Таким образом, массив указателей использует несколько больший объем памяти и может требовать наличие явного шага инициализации. Но при этом возникают два преимущества: доступ к элементу осуществляется косвенно через указатель, а не посредством умножения и сложения, и строки массива могут иметь различные длины. Это означает, что каждый элемент b не должен обязательно указывать на вектор из 10
элементов; некоторые могут указывать на вектор из двух элементов, другие - из двадцати, а третьи могут вообще ни на что не указывать.
5.11. Командная строка аргументов
echo hello, world
то выходом будет
hello, world
по соглашению argv[0] является именем, по которому вызывается программа, так что argc по меньшей мере равен 1. В приведенном выше примере argc равен 3, а argv[0], argv[1] и argv[2] равны соответственно "echo", "hello," и "world". Первым фактическим агументом является argv[1], а последним - argv[argc-1]. Если argc равен 1, то за именем программы не следует никакой командной строки аргументов. Все это показано в echo:
main(argc, argv) /* echo arguments; 1-st version */
int argc;
char *argv[];
{
int i;
for (i = 1; i < argc; i++)
printf("%s%c", argv[i], (i<argc-1) ? ' ' : '\n');
}
main(argc, argv) /* echo arguments; 2-nd version */
int argc;
char *argv[];
{
while (--argc > 0)
printf("%s%c",*++argv, (argc > 1) ? ' ' : '\n');
}
main(argc, argv) /* echo arguments; 3-rd version */
int argc;
char *argv[];
{
while (--argc > 0)
printf((argc > 1) ? "%s" : "%s\n", *++argv);
}
#define MAXLINE 1000
main(argc, argv) /* find pattern from 1-st argument */
int argc;
char *argv[];
{
char line[MAXLINE];
if (argc != 2)
printf ("usage: find pattern\n");
else
while (getline(line, MAXLINE) > 0)
if (index(line, argv[1] >= 0)
printf("%s", line);
}
find -x -n the
при входных данных
now is the time
for all good men
to come to the aid
of their party.
Должна выдать
2:for all good men
find -nx the
вот сама программа:
#define MAXLINE 1000
main(argc, argv) /* find pattern from 1-st argument */
int argc;
char *argv[];
{
char line[MAXLINE], *s;
long lineno = 0;
int except = 0, number = 0;
while (--argc > 0 && (*++argv)[0] == '-')
for (s = argv[0]+1; *s != '\0'; s++)
switch (*s) {
case 'x':
except = 1;
break;
case 'n':
number = 1;
break;
default:
printf("find: illegal option %c\n", *s);
argc = 0;
break;
}
if (argc != 1)
printf("usage: find -x -n pattern\n");
else
while (getlinе(line, MAXLINE) > 0) {
lineno++;
if ((index(line, *argv) >= 0) != except) \
if (number)
printf("%LD: ", lineno);
printf("%s", line);
}
}
}
add 2 3 4 + *
вычисляет 2*(3+4).
entab m +n
означающую табуляционные остановки через каждые n столбцов, начиная со столбца m. Выберите удобное (для пользователя) поведение функции по умолчанию.
tail -n
печатает последние n строк. программа должна действовать рационально, какими бы неразумными ни были бы ввод или значение n. Составьте программу так, чтобы она оптимальным образом использовала доступную память: строки должны храниться, как в функции sort, а не в двумерном массиве фиксированного
размера.
5.12. Указатели на функции
#define LINES 100 /* max number of lines
to be sorted */
main(argc, argv) /* sort input lines */
int argc;
char *argv[];
{
char *lineptr[LINES]; /* pionters to text lines */
int nlines; /* number of input lines read */
int strcmp(), numcmp(); /* comparsion functions */
int swap(); /* exchange function */
int numeric = 0; /* 1 if numeric sort */
if(argc>1 && argv[1][0] == '-' && argv[1][1]=='n')
numeric = 1;
if(nlines = readlines(lineptr, LINES)) >= 0) {
if (numeric)
sort(lineptr, nlines, numcmp, swap);
else
sort(lineptr, nlines, strcmp, swap);
writelines(lineptr, nlines);
} else
printf("input TOO BIG to sort\n");
}
sort(v, n, comp, exch) /* sort strings v[0] ... v[n-1] */
char *v[]; /* into increasing order */
int n;
int (*comp)(), (*exch)();
{
int gap, i, j;
for(gap = n/2; gap > 0; gap /= 2)
for(i = gap; i < n; i++)
for(j = i-gap; j >= 0; j -= gap) {
if((*comp)(v[j], v[j+gap]) <= 0)
break;
(*exch)(&v[j], &v[j+gap]);
}
}
int (*comp)()
говорит, что comp является указателем на функцию, которая возвращает значение типа int. Первые круглые скобки здесь необходимы; без них описание
int *comp()
говорило бы, что comp является функцией, возвращающей указатель на целые, что, конечно, совершенно другая вещь.
if (*comp)(v[j], v[j+gap]) <= 0)
полностью согласуется с описанием: comp - указатель на функцию, *comp - сама функция, а
(*comp)(v[j], v[j+gap])
- обращение к ней. Круглые скобки необходимы для правильного объединения компонентов.
numcmp(s1, s2) /* compare s1 and s2 numerically */
char *s1, *s2;
{
double atof(), v1, v2;
v1 = atof(s1);
v2 = atof(s2);
if(v1 < v2)
return(-1);
else if(v1 > v2)
return(1);
else
return (0);
}
swap(px, py) /* interchange *px and *py */
char *px[], *py[];
{
char *temp;
temp = *px;
*px = *py;
*py = temp;
}