Указатель - это переменная, содержащая адрес другой
переменной. указатели очень широко используются в языке "C".
Это происходит отчасти потому, что иногда они дают
единственную возможность выразить нужное действие, а отчасти
потому, что они обычно ведут к более компактным и эффективным
программам, чем те, которые могут быть получены другими
способами.
    Так как указатель содержит адрес об'екта, это дает
возможность "косвенного" доступа к этому об'екту через
указатель. Предположим, что х - переменная, например, типа int, а
рх - указатель, созданный неким еще не указанным способом.
Унарная операция & выдает адрес об'екта, так что оператор
присваивает адрес х переменной рх; говорят, что рх
"указывает" на х. Операция & применима только к переменным и
элементам массива, конструкции вида &(х-1) и &3 являются
незаконными. Нельзя также получить адрес регистровой переменной.
Унарная операция * рассматривает свой операнд как адрес
конечной цели и обращается по этому адресу, чтобы извлечь
содержимое. Следовательно, если y тоже имеет тип int, то
присваивает y содержимое того, на что указывает рх. Так
последовательность
    рх = &х;
y = *рх;
присваивает y то же самое значение, что и оператор
переменные, участвующие во всем этом необходимо описать:
с описанием для x и y мы уже неодонократно встречались.
Описание указателя
является новым и должно рассматриваться как мнемоническое;
оно говорит, что комбинация *px имеет тип int. Это означает,
что если px появляется в контексте *px, то это эквивалентно
переменной типа int. Фактически синтаксис описания
переменной имитирует синтаксис выражений, в которых эта переменная
может появляться. Это замечание полезно во всех случаях,
связанных со сложными описаниями. Например,
говорит, что atof() и *dp имеют в выражениях значения типа
double.
присваивает y значение, на 1 большее значения x;
печатает текущее значение x;
получает в d квадратный корень из x, причем до передачи
функции sqrt значение x преобразуется к типу double. (Смотри
главу 2).
унарные операции * и & связаны со своим операндом более
крепко, чем арифметические операции, так что такое выражение
берет то значение, на которое указывает px, прибавляет 1 и
присваивает результат переменной y. Мы вскоре вернемся к
тому, что может означать выражение
    Ссылки на указатели могут появляться и в левой части
присваиваний. Если px указывает на x, то
полагает x равным нулю, а
увеличивает его на единицу, как и выражение
круглые скобки в последнем примере необходимы; если их
опустить, то поскольку унарные операции, подобные * и ++,
выполняются справа налево, это выражение увеличит px, а не ту
переменную, на которую он указывает.
копирует содержимое px в py, в результате чего py указывает
на то же, что и px.
    Так как в "C" передача аргументов функциям
осуществляется "по значению", вызванная процедура не имеет
непосредственной возможности изменить переменную из вызывающей
программы. Что же делать, если вам действительно надо изменить
аргумент? Например, программа сортировки захотела бы
поменять два нарушающих порядок элемента с помощью функции с
именем swap. Для этого недостаточно написать
определив функцию swap при этом следующим образом:
из-за вызова по значению swap не может воздействовать на
агументы a и b в вызывающей функции.
так как операция & выдает адрес переменной, то &a является
указателем на a. В самой swap аргументы описываются как
указатели и доступ к фактическим операндам осуществляется через
них.
    Указатели в качестве аргументов обычно используются в
функциях, которые должны возвращать более одного значения.
(Можно сказать, что swap вoзвращает два значения, новые
значения ее аргументов). В качестве примера рассмотрим функцию
getint, которая осуществляет преобразование поступающих в
свободном формате данных, разделяя поток символов на целые
значения, по одному целому за одно обращение. Функция getint
должна возвращать либо найденное значение, либо признак
конца файла, если входные данные полностью исчерпаны. Эти
значения должны возвращаться как отдельные об'екты, какое бы
значение ни использовалось для EOF, даже если это значение
вводимого целого.
в результате каждого обращения v становится равным
следующему целому значению, найденному во входных данных. Обратите
внимание, что в качестве аргумента getint необходимо указать
&v а не v. Использование просто v скорее всего приведет к
ошибке адресации, поскольку getint полагает, что она
работает именно с указателем.
    Сама getint является очевидной модификацией написанной
нами ранее функции atoi:
Выражение *pn используется всюду в getint как обычная
переменная типа int. Мы также использовали функции getch и
ungetch (описанные в главе 4) , так что один лишний символ,
кототрый приходится считывать, может быть помещен обратно во
ввод.
    Упражнение 5-1.
    В языке "C" существует сильная взаимосвязь между
указателями и массивами , настолько сильная, что указатели и
массивы действительно следует рассматривать одновременно. Любую
операцию, которую можно выполнить с помощью индексов
массива, можно сделать и с помощью указателей. Вариант с
указателями обычно оказывается более быстрым, но и несколько более
трудным для непосредственного понимания, по крайней мере для
начинающего. Описание
определяет массив размера 10, т.е. набор из 10
последовательных об'ектов, называемых a[0], a[1], ..., a[9]. Запись
a[i] соответствует элементу массива через i позиций от
начала. Если pa - указатель целого, описанный как
то присваивание
приводит к тому, что pa указывает на нулевой элемент массива
a; это означает, что pa содержит адрес элемента a[0]. Теперь
присваивание
будет копировать содержимое a[0] в x.
Если pa указывает на некоторый определенный элемент
массива a, то по определению pa+1 указывает на следующий
элемент, и вообще pa-i указывает на элемент, стоящий на i
позиций до элемента, указываемого pa, а pa+i на элемент,
стоящий на i позиций после. Таким образом, если pa указывает
на a[0], то
ссылается на содержимое a[1], pa+i - адрес a[i], а *(pa+i)
содержимое a[i].
    Еще более удивительным, по крайней мере на первый
взгляд, кажется тот факт, что ссылку на a[i] можно записать
в виде *(a+i). При анализировании выражения a[i] в языке "C"
оно немедленно преобразуется к виду *(a+i); эти две формы
совершенно эквивалентны. Если применить операцию & к обеим
частям такого соотношения эквивалентности, то мы получим,
что &a[i] и a+i тоже идентичны: a+i - адрес i-го элемента от
начала a. С другой стороны, если pa является указателем, то
в выражениях его можно использовать с индексом: pa[i]
идентично *(pa+i). Короче, любое выражение, включающее массивы и
индексы, может быть записано через указатели и смещения и
наоборот, причем даже в одном и том же утверждении.
Операция увеличения s совершенно законна, поскольку эта
переменная является указателем; s++ никак не влияет на
символьную строку в обратившейся к strlen функции, а только
увеличивает локальную для функции strlen копию адреса.
Описания формальных параметров в определении функции в
виде
совершенно эквивалентны; какой вид описания следует
предпочесть, определяется в значительной степени тем, какие
выражения будут использованы при написании функции. Если функции
передается имя массива, то в зависимости от того, что
удобнее, можно полагать, что функция оперирует либо с массивом,
либо с указателем, и действовать далее соответвующим
образом. Можно даже использовать оба вида операций, если это
кажется уместным и ясным.
так и
передают функции f адрес элемента a[2], потому что и &a[2],
и a+2 являются указательными выражениями, ссылающимися на
третий элемент a. внутри функции f описания аргументов могут
присутствовать в виде:
или
Что касается функции f, то тот факт, что ее аргумент в
действительности ссылается к части большего массива,не имеет для
нее никаких последствий.
    Если p является указателем, то каков бы ни был сорт
об'екта, на который он указывает, операция p++ увеличивает p
так, что он указывает на следующий элемент набора этих
об'ектов, а операция p +=i увеличивает p так, чтобы он
указывал на элемент, отстоящий на i элементов от текущего
элемента.эти и аналогичные конструкции представляют собой самые
простые и самые распространенные формы арифметики указателей
или адресной арифметики.
    Дадим некоторые пояснения. Вообще говоря, указатель
может быть инициализирован точно так же, как и любая другая
переменная, хотя обычно единственными осмысленными
значениями являются NULL (это обсуждается ниже) или выражение,
включающее адреса ранее определенных данных соответствующего
типа. Описание
определяет allocp как указатель на символы и инициализирует
его так, чтобы он указывал на allocbuf, т.е. на первую
свободную позицию при начале работы программы. Так как имя
массива является адресом его нулевого элемента, то это можно
было бы записать в виде
используйте ту запись, которая вам кажется более
естественной.
выясняется, осталось ли достаточно места, чтобы
удовлетворить запрос на n символов. Если достаточно, то новое
значение allocp не будет указывать дальше, чем на последнюю
позицию allocbuf. Если запрос может быть удовлетворен, то alloc
возвращает обычный указатель (обратите внимание на описание
самой функции). Если же нет, то alloc должна вернуть
некоторый признак, говорящий о том, что больше места не осталось.
В языке "C" гарантируется, что ни один правильный указатель
данных не может иметь значение нуль, так что возвращение
нуля может служить в качестве сигнала о ненормальном событии,
в данном случае об отсутствии места. Мы, однако, вместо нуля
пишем NULL, с тем чтобы более ясно показать, что это
специальное значение указателя. Вообще говоря, целые не могут
осмысленно присваиваться указателям, а нуль - это особый
случай.
демонстрируют несколько важных аспектов арифметики
указателей. Во-первых , при определенных условиях указатели можно
сравнивать. Если p и q указывают на элементы одного и того
же массива, то такие отношения, как <, >= и т.д., работают
надлежащим образом. Например,
истинно, если p указывает на более ранний элемент массива,
чем q. Отношения == и != тоже работают. Любой указатель
можно осмысленным образом сравнить на равенство или неравенство
с NULL. Но ни за что нельзя ручаться, если Вы используете
сравнения при работе с указателями, указывающими на разные
массивы. Если вам повезет, то на всех машинах Вы получите
очевидную бессмыслицу. Если же нет, то ваша программа будет
правильно работать на одной машине и давать непостижимые
результаты на другой.
подразумевает n-ый об'ект за тем, на который p указывает в
настоящий момент. Это справедливо независимо от того, на
какой вид об'ектов p должен указывать; компилятор сам
масштабирует n в соответствии с определяемым из описания p
размером об'ектов, указываемых с помощью p. например, на PDP-11
масштабирующий множитель равен 1 для char, 2 для int и
short, 4 для long и float и 8 для double.
    При описании указатель 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. Все действия с указателями автоматически
учитывают размер об'ектов, на которые они указывают, так что
больше ничего менять не надо.
    Строчная константа, как, например,
является массивом символов. Компилятор завершает внутреннее
представление такого массива символом \0, так что программы
могут находить его конец. Таким образом, длина массива в
памяти оказывается на единицу больше числа символов между
двойными кавычками.
когда символьная строка, подобная этой, появляется в
программе, то доступ к ней осуществляется с помощью указателя
символов; функция printf фактически получает указатель
символьного массива.
то в результате оператора
переменная message станет указателем на фактический массив
символов. Это не копирование строки; здесь участвуют только
указатели. в языке "C" не предусмотрены какие-либо операции
для обработки всей строки символов как целого.
Мы проиллюстрируем другие аспекты указателей и
массивов, разбирая две полезные функции из стандартной библиотеки
ввода-вывода, которая будет рассмотрена в главе 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 является излишним, так что функцию можно записать
в виде
хотя с первого взгляда эта запись может показаться
загадочной, она дает значительное удобство. Этой идиомой следует
овладеть уже хотя бы потому, что Вы с ней будете часто
встречаться в "C"-программах.
Вот версия strcmp с указателями:
    Так как ++ и -- могут быть как постфиксными, так и
префиксными операциями, встречаются другие комбинации * и ++ и
--, хотя и менее часто.
Например, *++p увеличивает p до извлечения символа, на
который указывает p, а *--p сначала уменьшает p.
    Упражнение 5-2.     Упражнение 5-3.     Упражнение 5-4.
    Вы, возможно, обратили внимание в предыдущих
"с"-программах на довольно непринужденное отношение к копированию
указателей. В общем это верно, что на большинстве машин
указатель можно присвоить целому и передать его обратно, не
изменив его; при этом не происходит никакого масштабирования
или преобразования и ни один бит не теряется. к сожалению,
это ведет к вольному обращению с функциями, возвращающими
указатели, которые затем просто передаются другим функциям,
- необходимые описания указателей часто опускаются.
на практике существует сильное стремление опускать описания:
эта программа будет правильно работать на многих машинах,
потому что по умолчанию функции и аргументы имеют тип int, а
указатель и целое обычно можно безопасно пересылать туда и
обратно. Однако такой стиль программирования в своем
существе является рискованным, поскольку зависит от деталей
реализации и архитектуры машины и может привести к неправильным
результатам на конкретном используемом вами компиляторе.
Разумнее всюду использовать полные описания. (Отладочная
программа lint предупредит о таких конструкциях, если они по
неосторожности все же появятся).
    В языке "C" предусмотрены прямоугольные многомерные
массивы, хотя на практике существует тенденция к их
значительно более редкому использованию по сравнению с массивами
указателей. В этом разделе мы рассмотрим некоторые их
свойства.
полагает m равным 3 и d равным 1 (1-ое марта).
Обе эти функции нуждаются в одной и той же
информационной таблице, указывающей число дней в каждом месяце. Так как
число дней в месяце в високосном и в невисокосном году
отличается, то проще представить их в виде двух строк двумерного
массива, чем пытаться прослеживать во время вычислений, что
именно происходит в феврале. Вот этот массив и выполняющие
эти преобразования функции:
Массив day_tab должен быть внешним как для day_of_year, так
и для month_day, поскольку он используется обеими этими
функциями.
как в большинстве языков. В остальном с двумерными массивами
можно в основном обращаться таким же образом, как в других
языках. Элементы хранятся по строкам, т.е. при обращении к
элементам в порядке их размещения в памяти быстрее всего
изменяется самый правый индекс.
Так как количество строк является несущественным, то
описание аргумента в f могло бы быть таким:
или таким
в котором говорится, что аргумент является указателем
массива из 13 целых. Круглые скобки здесь необходимы, потому что
квадратные скобки [] имеют более высокий уровень
старшинства, чем *; как мы увидим в следующем разделе, без круглых
скобок
является описанием массива из 13 указателей на целые.
    Так как указатели сами являются переменными, то Вы
вполне могли бы ожидать использования массива указателей.
Это действительно так. Мы проиллюстрируем это написанием
программы сортировки в алфавитном порядке набора текстовых
строк, предельно упрощенного варианта утилиты sort
операционной систем UNIX.     Процесс сортировки включает три шага:
    чтение всех строк ввода
их сортировка
вывод их в правильном порядке
Как обычно, лучше разделить программу на несколько функций в
соответствии с естественным делением задачи и выделить
ведущую функцию, управляющую работой всей программы.
Давайте отложим на некоторое время рассмотрение шага
сортировки и сосредоточимся на структуре данных и
вводе-выводе. Функция, осуществляющая ввод, должна извлечь символы
каждой строки, запомнить их и построить массив указателей
строк. Она должна также подсчитать число строк во вводе, так
как эта информация необходима при сортировке и выводе. так
как функция ввода в состоянии справиться только с конечным
числом вводимых строк, в случае слишком большого их числа
она может возвращать некоторое число, отличное от возможного
числа строк, например -1. Функция осуществляющая вывод,
должна печатать строки в том порядке, в каком они появляются в
массиве указателей.
Символ новой строки в конце каждой строки удаляется, так что
он никак не будет влиять на порядок, в котором сортируются
строки.
    Существенно новым в этой программе является описание
которое сообщает, что lineptr является массивом из lines
элементов, каждый из которых - указатель на переменные типа
char. Это означает, что lineptr[i] - указатель на символы, а
*lineptr[i] извлекает символ.
здесь *lineptr сначала указывает на первую строку; каждое
увеличение передвигает указатель на следующую строку, в то
время как nlines убывает до нуля.
Так как каждый отдельный элемент массива v (имя формального
параметра, соответствующего lineptr) является указателем на
символы, то и temp должен быть указателем на символы, чтобы
их было можно копировать друг в друга.     Упражнение 5-5.
    Рассмотрим задачу написания функции month_name(n),
которая возвращает указатель на символьную строку, содержащую
имя n-го месяца. Это идеальная задача для применения
внутреннего статического массива. Функция month_name содержит
локальный массив символьных строк и при обращении к ней
возвращает указатель нужной строки. Тема настоящего раздела
как инициализировать этот массив имен.
Описание массива указателей на символы name точно такое же,
как аналогичное описание lineptr в примере с сортировкой.
Инициализатором является просто список символьных строк;
каждая строка присваивается соответствующей позиции в
массиве. Более точно, символы i-ой строки помещаются в какое-то
иное место, а ее указатель хранится в name[i]. Поскольку
размер массива name не указан, компилятор сам подсчитывает
количество инициализаторов и соответственно устанавливает
правильное число.
    Начинающие изучать язык "с" иногда становятся в тупик
перед вопросом о различии между двумерным массивом и
массивом указателей, таким как name в приведенном выше примере.
то а и в можно использовать сходным образом в том смысле,
что как a[5][5], так и b[5][5] являются законными ссылками
на отдельное число типа int. Но а - настоящий массив: под
него отводится 100 ячеек памяти и для нахождения любого
указанного элемента проводятся обычные вычисления с
прямоугольными индексами. Для b, однако, описание выделяет только 10
указателей; каждый указатель должен быть установлен так,
чтобы он указывал на массив целых. если предположить, что
каждый из них указывает на массив из 10 элементов, то тогда
где-то будет отведено 100 ячеек памяти плюс еще десять ячеек
для указателей. Таким образом, массив указателей использует
несколько больший об'ем памяти и может требовать наличие
явного шага инициализации. Но при этом возникают два
преимущества: доступ к элементу осуществляется косвенно через
указатель, а не посредством умножения и сложения, и строки
массива могут иметь различные длины. Это означает, что каждый
элемент в не должен обязательно указывать на вектор из 10
элементов; некоторые могут указывать на вектор из двух
элементов, другие - из двадцати, а третьи могут вообще ни на
что не указывать.
Указатели обычно смешивают в одну кучу с операторами
goto, характеризуя их как чудесный способ написания
программ, которые невозможно понять. Это безусловно справедливо,
если указатели используются беззаботно; очень просто ввести
указатели, которые указывают на что-то совершенно
неожиданное. Однако, при определенной дисциплине, использование
указателей помогает достичь ясности и простоты. Именно этот
аспект мы попытаемся здесь проиллюстрировать.
5.1. Указатели и адреса
рх = &х;
y = *рх;
y = x;
int x, y;
int *px;
int *px;
double atof(), *dp;
Вы должны также заметить, что из этого описания
следует, что указатель может указывать только на определенный вид
об'ектов.
Указатели могут входить в выражения. Например, если px
указывает на целое x, то *px может появляться в любом
контексте, где может встретиться x. Так оператор
y = *px + 1
printf("%d\n", *px)
d = sqrt((double) *px)
В выражениях вида
y = *px + 1
y = *(px + 1)
*px = 0
*px += 1
(*px)++
И наконец, так как указатели являются переменными, то с
ними можно обращаться, как и с остальными переменными. Если
py - другой указатель на переменную типа int, то
py = px
5.2. Указатели и аргументы функций
swap(a, b);
swap(x, y) /* wrong */
int x, y;
{
int temp;
temp = x;
x = y;
y = temp;
}
К счастью, все же имеется возможность получить желаемый
эффект. Вызывающая программа передает указатели подлежащих
изменению значений:
swap(&a, &b);
swap(px, py) /* interchange *px and *py */
int *px, *py;
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}
Одно из решений, основывающееся на описываемой в главе
7 функции ввода scanf, состоит в том, чтобы при выходе на
конец файла getint возвращала EOF в качестве значения
функции; любое другое возвращенное значение говорит о нахождении
нормального целого. Численное же значение найденного целого
возвращается через аргумент, который должен быть указателем
целого. Эта организация разделяет статус конца файла и
численные значения.
Следующий цикл заполняет массив целыми с помощью
обращений к функции getint:
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;
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);
}
Напишите функцию getfloat, аналог getint для чисел с
плавающей точкой. Какой тип должна возвращать getfloat в качестве
значения функции?
5.3. Указатели и массивы
int a[10]
int *pa
pa = &a[0]
x = *pa
*(pa+1)
Эти замечания справедливы независимо от типа переменных
в массиве a. Суть определения "добавления 1 к указателю", а
также его распространения на всю арифметику указателей,
состоит в том, что приращение масштабируется размером памяти,
занимаемой об'ектом, на который указывает указатель. Таким
образом, i в pa+i перед прибавлением умножается на размер
об'ектов, на которые указывает pa.
Очевидно, существует очень тесное соответствие между
индексацией и арифметикой указателей. В действительности
компилятор преобразует ссылку на массив в указатель на
начало массива. В результате этого имя массива является
указательным выражением. Отсюда вытекает несколько весьма
полезных следствий. Так как имя массива является синонимом
местоположения его нулевого элемента, то присваивание pa=&a[0]
можно записать как
pa = a
Имеется одно различие между именем массива и
указателем, которое необходимо иметь в виду. указатель является
переменной, так что операции 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);
}
char s[];
char *s;
Можно передать функции часть массива, если задать в
качестве аргумента указатель начала подмассива. Например, если
a - массив, то как
f(&a[2])
f(a+2)
f(arr)
int arr[];
{
...
}
f(arr)
int *arr;
{
...
}
5.4. Адресная арифметика
Язык "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.
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 pointed by p */
char *p;
{
if (p >= allocbuf && p < allocbuf + allocsize)
allocp = p;
}
static char *allocp = allocbuf;
static char *allocp = &allocbuf[0];
С помощью проверки
if (allocp + n <= allocbuf + allocsize)
Проверки вида
if (allocp + n <= allocbuf + aloocsize)
и
if (p >= allocbuf && p < allocbuf + allocsize)
p < q
Во-вторых, как мы уже видели, указатель и целое можно
складывать и вычитать. Конструкция
p + n
Вычитание указателей тоже возможно: если 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);
}
while (*p)
p++;
За исключением упомянутых выше операций (сложение и
вычитание указателя и целого, вычитание и сравнение двух
указателей), вся остальная арифметика указателей является
незаконной. Запрещено складывать два указателя, умножать,
делить, сдвигать или маскировать их, а также прибавлять к ним
переменные типа float или double.
5.5. Указатели символов и функции
"i am a string"
По-видимому чаще всего строчные константы появляются в
качестве аргументов функций, как, например, в
printf ("hello, world\n");
Конечно, символьные массивы не обязаны быть только
аргументами функций. Если описать message как
char *message;
message = "now is the time";
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++)
;
}
Вторая функция - strcmp(s, t), которая сравнивает
символьные строки s и т, возвращая отрицательное, нулевое или
положительное значение в соответствии с тем, меньше, равно
или больше лексикографически s, чем t. Возвращаемое значение
получается в результате вычитания символов из первой
позиции, в которой s и t не совпадают.
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);
}
Напишите вариант с указателями функции strcat из главы 2:
strcat(s, t) копирует строку t в конец s.
Напишите макрос для strcpy.
Перепишите подходящие программы из предыдущих глав и
упражнений, используя указатели вместо индексации массивов.
Хорошие возможности для этого предоставляют функции getline
/главы 1 и 4/, atoi, itoa и их варианты /главы 2, 3 и 4/,
reverse /глава 3/, index и getop /глава 4/.
5.6. Указатели - не целые
Рассмотрим, например, функцию strsave(s), которая копирует строку s
в некоторое место для хранения, выделяемое посредством
обращения к функции alloc, и возвращает указатель на это место.
Правильно она должна быть записана так:
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. Многомерные массивы
Рассмотрим задачу преобразования дня месяца в день года
и наоборот. Например, 1-ое марта является 60-м днем
невисокосного года и 61-м днем високосного года. Давайте введем
две функции для выполнения этих преобразований: day_of_year
преобразует месяц и день в день года, а month_day
преобразует день года в месяц и день. Так как эта последняя функция
возвращает два значения, то аргументы месяца и дня должны
быть указателями:
month_day(1977, 60, &m, &d)
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, pmonth, pday) /*set month,day */
int year, yearday, *pmonth, *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];
*pmonth = i;
*pday = yearday;
}
Массив day_tab является первым двумерным массивом, с
которым мы имеем дело. По определению в "C" двумерный массив
по существу является одномерным массивом, каждый элемент
которого является массивом. Поэтому индексы записываются как
day_tab[i][j]
а не
day_tab [i, j]
Массив инициализируется с помощью списка начальных
значений, заключенных в фигурные скобки; каждая строка
двумерного массива инициализируется соответствующим подсписком. Мы
поместили в начало массива day_tab столбец из нулей для
того, чтобы номера месяцев изменялись естественным образом от
1 до 12, а не от 0 до 11. Так как за экономию памяти у нас
пока не награждают, такой способ проще, чем подгонка
индексов.
Если двумерный массив передается функции, то описание
соответствующего аргумента функции должно содержать
количество столбцов; количество строк несущественно, поскольку, как
и прежде, фактически передается указатель. В нашем
конкретном случае это указатель об'ектов, являющихся массивами из
13 чисел типа int. Таким образом, если бы требовалось
передать массив day_tab функции f, то описание в f имело бы вид:
f(day_tab)
int day_tab[2][13];
{
...
}
int day_tab[][13];
int (*day_tab)[13];
int *day_tab[13];
5.8. Массивы указателей; указатели указателей
В главе 3 мы привели функцию сортировки по Шеллу,
которая упорядочивала массив целых. Этот же алгоритм будет
работать и здесь, хотя теперь мы будем иметь дело со строчками
текста различной длины, которые, в отличие от целых, нельзя
сравнивать или перемещать с помощью одной операции. Мы
нуждаемся в таком представлении данных, которое бы позволяло
удобно и эффективно обрабатывать строки текста переменной
длины.
Здесь и возникают массивы указателей. Если подлежащие
сортировке сроки хранятся одна за другой в длинном
символьном массиве (управляемом, например, функцией alloc), то к
каждой строке можно обратиться с помощью указателя на ее
первый символ. Сами указатели можно хранить в массиве. Две
строки можно сравнить, передав их указатели функции strcmp.
Если две расположенные в неправильном порядке строки должны
быть переставлены, то фактически переставляются указатели в
массиве указателей, а не сами тексты строк. Этим исключаются
сразу две связанные проблемы: сложного управления памятью и
больших дополнительных затрат на фактическую перестановку
строк.
#define NULL 0
#define LINES 100 /* max lines to be sorted */
main() /* sort input lines */
{
char *lineptr[lines]; /*pointers 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 является массивом, который
передается функции writelines, с ним можно обращаться как с
указателем точно таким же образом, как в наших более ранних
примерах. Тогда последнюю функцию можно переписать в виде:
writelines(lineptr, nlines) /* write output lines */
char *lineptr[];
int nlines;
{
int i;
while (--nlines >= 0)
printf("%s\n", *lineptr++);
}
Справившись с вводом и выводом, мы можем перейти к
сортировке. программа сортировки по Шеллу из главы 3 требует
очень небольших изменений: должны быть модифицированы
описания, а операция сравнения выделена в отдельную функцию.
Основной алгоритм остается тем же самым, и это дает нам
определенную уверенность, что он по-прежнему будет работать.
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;
}
}
Мы написали эту программу по возможности более просто с
тем, чтобы побыстрее получить работающую программу. Она
могла бы работать быстрее, если, например, вводить строки
непосредственно в массив, управляемый функцией readlines, а не
копировать их в line, а затем в скрытое место с помощью
функции alloc. но мы считаем, что будет разумнее первоначальный
вариант сделать более простым для понимания, а об
"эффективности" позаботиться позднее. Все же, по-видимому, способ,
позволяющий добиться заметного ускорения работы программы
состоит не в исключении лишнего копирования вводимых строк.
Более вероятно, что существенной разницы можно достичь за
счет замены сортировки по Шеллу на нечто лучшее, например,
на метод быстрой сортировки.
В главе 1 мы отмечали, что поскольку в циклах while и
for проверка осуществляется до того, как тело цикла
выполнится хотя бы один раз, эти циклы оказываются удобными для
обеспечения правильной работы программы при граничных
значениях, в частности, когда ввода вообще нет. Очень полезно
просмотреть все функции программы сортировки, разбираясь,
что происходит, если вводимый текст отсутствует.
Перепишите функцию readlines таким образом, чтобы она
помещала строки в массив, предоставляемый функцией main, а не в
память, управляемую обращениями к функции alloc. Насколько
быстрее стала программа?
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];
Хотя мы вели это обсуждение в терминах целых,
несомненно, чаще всего массивы указателей используются так, как мы
продемонстрировали на функции month_name, - для хранения
символьных строк различной длины.