Язык Си - Уэйт, Прата, Мартин. M. уэит с. Прата д. Мартин
Скачать 4.69 Mb.
|
УКАЗАТЕЛИ МАССИВОВ Далее Содержание Как было сказано в гл. 9, указатели позволяют нам работать с символическими адресами. Поскольку в реализуемых аппаратно командах вычислительной машины интенсивно используются адреса, указатели предоставляют возможность применять адреса примерно так, как это делается в самой машине, и тем самым повышать эффективность программ. В частности, указатели позволяют эффективно организовать работу с массивами. Действительно, как мы могли убедиться, наше обозначение массива представляет собой просто скрытую форму использования указателей. Например, имя массива определяет также его первый элемент, т. е. если flizny[] - массив, то flizny == &flizny[0] и обе части равенства определяют адрес первого элемента массива. (Вспомним, что операция & выдает адрес.) Оба обозначения являются константами типа указатель, поскольку они не изменяются на протяжении всей программы. Однако их можно присваивать (как значения) переменной типа указатель и изменять значение переменной, как показано в ниже следующем 237 примере. Посмотрите, что происходит со значением указателя, если к нему прибавить число. /* прибавление к указателю */ main( ) { int dates[4], *pti, index; float bills [4], *ptf; pti = dates; /* присваивает адрес указателю массива */ ptf = bills; for(index = 0; index < 4; index++) printf(" указатели + %d: %10 u %10u \n", index, pti + index, ptf + index); } Вот результат: указатели + 0 56014 56026 указатели + 1 56016 56030 указатели + 2 56018 56034 указатели + 3 56020 56038 Первая напечатанная строка содержит начальные адреса двух массивов, а следующая строка - результат прибавления единицы к адресу и т. д. Почему так получается? 56014 + 1 = 56016? 56026 + 1 = 56030? Не знаете, что сказать? В нашей системе единицей адресации является байт, но тип int использует два байта, а тип float - четыре. Что произойдет, если вы скажете: "прибавить единицу к указателю?" Компилятор языка Си добавит единицу памяти. Для массивов это означает, что мы перейдем к адресу следующего элемента, а не следующего байта. Вот почему мы должны специально оговаривать тип объекта, на который ссылается указатель; одного адреса здесь недостаточно, так как машина должна знать, сколько байтов потребуется для запоминания объекта. (Это справедливо также для указателей на скалярные переменные; иными словами, при помощи операции *pt нельзя получить значение.) РИС. 12.1. Увеличение указателя массива. Благодаря тому что компилятор языка Си умеет это делать, мы имеем следующие равенства: dates + 2 == &dates[2] /* один и тот же адрес */ *(dates + 2) == dates[2] /* одно и то же значение */ Эти соотношения суммируют тесную связь между массивами и указателями. Они показывают, что можно использовать указатель для определения отдельного элемента массива, а также для 238 получения его значения. По существу мы имеем два различных обозначения для одного и того же. Действительно, компилятор превращает обозначение массива в указатели, поэтому метод указателей более предпочтителен. Между прочим, постарайтесь различать выражения *(dates + 2), и *dates + 2. Операция (*) имеет более высокий приоритет, чeм +, поэтому последнее выражение означает (*dates) + 2: *(dates + 2) /* значение 3-го элемента массива dates */ *dates +2 /* 2 добавляется к значению 1-го элемента массива */ Связь между массивами и указателями часто позволяет нам применять оба подхода при создании программ. Одним из примеров этого является функция с массивом в качестве аргумента. ФУНКЦИИ, МАССИВЫ И УКАЗАТЕЛИ Далее Содержание Массивы можно использовать в программе двояко. Во-первых, их можно описать в теле функции. Во-вторых, они могут быть аргументами функции. Вес, что было сказано в этой главе о массивах, относится к первому их применению; теперь рассмотрим массивы в качестве аргументов. Об этом уже говорилось в гл. 10. Сейчас, когда мы познакомились с указателями, можно заняться более глубоким изучением массивов-аргументов. Давайте проанализируем скелет программы, обращая внимание на описания: /* массив-аргумент */ main( ) { int ages[50]; /* массив из 50 элементов */ convert(ages); } convert (years); int years [ ]; /* каков размер массива? */ { } Очевидно, что массив ages состоит из 50 элементов. А что можно сказать о массиве years? Оказывается, в программе нет такого массива. Описатель int years[ ]; создает не массив, а указатель на него. Посмотрим, почему это так. Вот вызов нашей функции: convert(ages); ages - аргумент функции convert. Вы помните, что имя ages является указателем на первый элемент массива, состоящего из 50 элементов. Таким образом, оператор вызова функции передает ей указатель, т. е. адрес функции convert(). Это значит, что аргумент функции является указателем, и мы можем написать функцию convert() следующим образом: convert (years); int *years; { } Действительно, операторы int years[ ]; 239 int *years; синонимы. Оба они объявляют переменную years указателем массива целых чисел. Однако главное их отличие состоит в том, что первый из них напоминает нам, что указатель years ссылается на массив. Как теперь связать его с массивом ages? Вспомним, что при использовании указателя в качестве аргумента, функция взаимодействует с соответствующей переменной в вызывающей программе, т. е. операторы, использующие указатель years в функции соnvert(), фактически работают с массивом ages, находящимся в теле функции main(). Посмотрим, как работает этот механизм. Во-первых, вызов функции инициализирует указатель years, ссылаясь на ages[0]. Теперь предположим, что где-то внутри функции convert( ) есть выражение years[3]. Как вы видели в предыдущем разделе, оно аналогично *(years + 3). Однако если years указывает на ages[0], то years+3 ссылается на ages[3]. Это приводит к тому, что *(years+3) означает ages[3]. Если внимательно проследить данную цепочку, то мы увидим, что years[3] аналогично *(years + 3), которое в свою очередь совпадает с ages[3]. Что и требовалось доказать, т. е. операции над указателем years приводят к тем же результатам, что и операции над массивом ages. Короче говоря, когда имя массива применяется в качестве аргумента, функции передается указатель. Затем функция использует этот указатель для выполнения изменений в исходном массиве, принадлежащем программе, вызвавшей функцию. Рассмотрим пример. ИСПОЛЬЗОВАНИЕ УКАЗАТЕЛЕЙ ПРИ РАБОТЕ С МАССИВАМИ Далее Содержание Попробуем написать функцию, использующую массивы, а затем перепишем ее, применяя указатели. Рассмотрим простую функцию, которая находит (или пытается найти) среднее значение массива целых чисел. На входе функции мы имеем имя массива и количество элементов. На выходе получаем среднее значение, которое передастся при помощи оператора return. Оператор вызова функции может выглядеть следующим образом: printf("Среднее из заданных значений %d.\n", mean(numbs,size)); /* находит среднее значение массива из n целых чисел */ int mean(array, n); int array[ ], n; { int index; long sum; /* Если целых слишком много, их можно суммировать в формате long int */ if(n > 0) { for(index = 0, sum = 0; index < n; index++) sum + = array[index]; return((int)(sum/n)); /* возвращает int * / } else { printf("Нет массива. \n"); return(0); } } Эту программу легко переделать, применяя указатели. Объявим ра указателем на тип int. Затем заменим элемент массива array[index] на соответствующее значение: *(ра + index). /* Использование указателей для нахождения среднего значения массива n целых чисел */ 240 int mean(pa, n) int oра, n; { int index; long sum; /*Если целых слишком много, их можно суммировать в формате long int */ if(n > 0) { for(index=0, sum=0; index < n; index++) sum + = *(pa + index); return((int)(sum/n)); /* Возвращает целое */ } else { printf("Нет массива.\n"); return(0); } } Это оказалось несложным, но возникает вопрос: должны ли мы изменить при этом вызов функции, в частности numbs, который был именем массива в операторе mean(numbs, size)? Ничего не нужно менять, поскольку имя массива является указателем. Как мы уже говорили в предыдущем разделе, операторы описания: int ра[ ]; и int *ра; идентичны по действию: оба объявляют ра указателем. В программе можно применять любой из них, хотя до сих пор мы использовали второй в виде *(ра + index). Понятно ли вам, как работать с указателями? Указатель устанавливается на первый элемент массива, и значение, находящееся там, добавляется в sum. Затем указатель передвигается на следующий элемент (к указателю прибавляется единица), и значение, находящееся в нем, также прибавляется к sum и т. д. Это похоже на механизм работы с массивом, где индекс действует как стрелка часов, показывающая по очереди на каждый элемент массива. Теперь у нас есть два подхода; какой же из них выбрать? Во-первых, хотя массивы и указатели тесно связаны, у них есть отличия. Указатели являются более общим и широко применяемым средством, однако многие пользователи (по крайней мере начинающие) считают, что массивы более привычны к понятны. Во-вторых, при использовании указателей у нас нет простого эквивалента для задания размера массива. Самую типичную ситуацию, в которой можно применять указатель, мы уже показали: это функция, работающая с массивом, который находится где-то в другой части программы. Мы предлагаем использовав любой из подходов по вашему желанию. Однако несомненное преимущество использования указателей в приведенном выше примере должно научить вас легко применять их, когда в этом возникает необходимость. ОПЕРАЦИИ С УКАЗАТЕЛЯМИ Далее Содержание Что же мы теперь умеем делать с указателями? Язык Си предлагает пять основных операций, которые можно применять к указателям, а нижеследующая программа демонстрирует эти возможности. Чтобы показать результаты каждой операции, мы будем печатать значение указателя (являющегося адресом, на который ссылается указатель), значение, находящееся по этому адресу, и адрес самого указателя. /* операции с указателями */ #define PR(X) printf("X = %u,*X = %d, &X = %u\n",X, *X,&X); /* печатает значение указателя (адрес), значение, находящееся по */ /* этому адресу, и адрес самого указателя */ 241 main( ) static int urn[ ] = [100, 200, 300]; int *ptrl, *ptr2; { ptrl = urn; /* присваивает адрес указателю */ ptr2 = &urn [2]; /* то же самое */ PR(ptrl); /* см. макроопределение, указанное выше */ ptrl++; /* увеличение указателя */ PR(ptrl); PR(ptr2); ++рtr2; /* выходит за конец массива */ PR(ptr2); printf("ptr2 - ptrl = %u\n", ptr2 - ptrl); } В результате работы программы получены следующие результаты: ptrl = 18, *ptrl = 100, &ptrl = 55990 ptrl = 20, *ptrl = 200, &ptrl = 55990 ptr2 =22, *ptr2 = 300, &ptr2 = 55992 ptr2 =24, *ptr2 = 29808, &ptr2 = 55992 ptr2 - ptrl = 2 Программа демонстрирует пять основных операций, которые можно выполнять над переменными типа указатель. 1. ПРИСВАИВАНИЕ. Указателю можно присвоить адрес. Обычно мы выполняем это действие, используя имя массива или операцию получения адреса (&). Программа присваивает переменной ptrl адрес начала массива urn; этот адрес принадлежит ячейке памяти с номером 18. (В нашей системе статические переменные запоминаются в ячейках оперативной памяти.) Переменная ptr2 получает адрес третьего и последнего элемента массива, т. е. urn[2]. 2. ОПРЕДЕЛЕНИЕ ЗНАЧЕНИЯ. Операция выдает значение, хранящееся в указанной ячейке. Поэтому результатом операции *ptrl в самом начале работы программы является число 100, находящееся в ячейке с номером 18. 3. ПОЛУЧЕНИЕ АДРЕСА УКАЗАТЕЛЯ. Подобно любым переменным переменная типа указатель имеет адрес и значение. Операция & сообщает нам, где находится сам указатель. В нашем примере указатель ptrl находится в ячейке с номером 55990. Эта ячейка содержит число 18, 242 являющееся адресом начала массива urn. 4. УВЕЛИЧЕНИЕ УКАЗАТЕЛЯ. Мы можем выполнять это действие с помощью обычной операции сложения либо с помощью операции увеличения. Увеличивая указатель, мы перемещаем его на следующий элемент массива. Поэтому операция ptr1++ увеличивает числовое значение переменной ptrl на 2 (два байта на каждый элемент массива целых чисел), после чего указатель ptrl ссылается уже на urn[l] (рис. 12.2). Теперь ptrl имеет значение 20 (адрес следующего элемента массива), а операция *ptrl выдает число 200, являющееся значением элемента urn[1]. Заметим, что адрес самой ячейки ptrl остается неизменным, т.е. 55990. После выполнения операции сама переменная не переместилась, потому что она только изменила значение! РИС. 12.2. Увеличение указателя типа int. Аналогичным образом можно и уменьшить указатель. Однако при этом следует соблюдать осторожность. Машина не следит, ссылается ли еще указатель на массив или уже нет. Операция ++ptr2 перемещает указатель ptr2 на следующие два байта, и теперь он ссылается на некоторую информацию, расположенную в памяти за массивом. Кроме того, оператор увеличения можно использовать для переменной типа указатель, но не для констант этого типа подобно тому, как вы не можете применять оператор увеличения для обычных констант. Для переменных и констант типа указатель можно использовать простое сложение: Правильно Неправильно ptr1++; urn ++; х ++; 3++; ptr2 = ptr1 + 2; ptr2 = urn++; ptr2 = urn + 1; х = у + 3++; 5. РАЗНОСТЬ. Можно находить разность двух указателей. Обычно это делается для указателей, ссылающихся на элементы одного и того же массива, чтобы определить, на каком расстоянии друг от друга находятся элементы. Помните, что результат имеет тот же тип, что и переменная, содержащая размер массива. Перечисленные выше операции открывают большие возможности. Программисты на языке Си создают массивы указателей, указатели на функции, массивы указателей на указатели, массивы 243 указателей на функции и т. д. Мы будем придерживаться основных применений, которые уже упоминались. Первое из них - передача информации в функцию и из нее. Мы использовали указатели, когда хотели, чтобы функция изменила переменные, находящиеся в вызывающей программе. Второе - использование указателей в функциях, работающих с многомерными массивами. МНОГОМЕРНЫЕ МАССИВЫ Далее Содержание Темпест Клауд, метеоролог, занимающаяся явлением перисто-сти облаков, хочет проанализировать данные о ежемесячном количестве осадков на протяжении пяти лет. В самом начале она должна решить, как представлять данные. Можно использовать 60 переменных, по одной на каждый месяц. (Мы уже упоминали о таком подходе ранее, но в данном случае он также неудачен.) Лучше было бы взять массив, состоящий из 60 элементов, но это устроило бы нас только до тех пор, пока можно хранить раздельно данные за каждый год. Мы могли бы также использовать 5 массивов по 12 элементов каждый, но это очень примитивно и может создать действительно большие неудобства, если Темнеет решит изучать данные о количестве осадков за 50 лет вместо пяти. Нужно придумать что-нибудь получше. Хорошо было бы использовать массив массивов. Основной массив состоял бы тогда из 5 элементов, каждый из которых в свою очередь был бы массивом из 12 элементов. Вот как это записывается: static float rain[5][12]; Можно также представить массив rain в виде двумерного массива, состоящего из 5 строк и 12 столбцов. При изменении второго индекса на единицу мы передвигаемся вдоль строки, а при изменении первого индекса на единицу, передвигаемся вертикально вдоль столбца. В нашем примере второй индекс дает нам месяцы, а первый - годы. РИС. 12.3. Двумерный массив. Используем этот двумерный массив в метеорологической программе. Цель нашей программы - 244 найти общее количество осадков для каждого года, среднегодовое количество осадков и среднее количество осадков за каждый месяц. Для получения общего количества осадков за год следует сложить все данные, находящиеся в нужной строке. Чтобы найти среднее количество осадков за данный месяц, мы сначала складываем все данные в указанном столбце. Двумерный массив позволяет легко представить и выполнить эти действия. Рис. 12.4 содержит программу. /* найти общее количество осадков для каждого года, среднего */ /* довое, среднемесячное количество осадков, за несколько лет */ #define TWLV 12 /* число месяцев в году */ #define YRS 5 /* число лет */ main( ) { static float rain [YRS][TWLV] = { {10.2, 8.1, 6.8, 4.2, 2.1, 1.8, 0.2, 0.3, 1.1, 2.3, 6.1, 7.4}, {9.2, 9.8, 4.4, 3.3, 2.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 5.2}, {6.6, 5.5, 3.8, 2.8, 1.6, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 4.2}, {4.3, 4.3, 4.3, 3.0, 2.0, 1.0, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6}, {8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.2} }; /* инициализация данных по количеству осадков за 1970-1974 */ int year, month; float subtot, total; printf("ГОД КОЛИЧЕСТВО ОСАДКОВ (дюймы)\n\n"); for(year = 0, total = 0; year < YRS; year++) { /* для каждого года, суммируем количество осадков для каждого месяца */ for(month = 0, subtot = 0; month < TWLV; month++) subtot + = rain [year][month]; printf("%5d %15.lf\n", 1970 + year, subtot); total + = subtot; /* общее для всех лет */ } printf(" \n среднегодовое количество осадков составляет %.lf дюймов. \n \n ", total/YRS ); printf(" Янв. Фев. Map. Апр.Май Июн.Июл. Авг.Сент."); printf(" Окт. Нояб. Дек.\n" ); for(month = 0; month < TWLV; month++) { /* для каждого месяца, суммируем количество осадков за все годы */ for(year = 0, subtot = 0; year < YRS; year++) subtot += rain[year][month]; printf(" %4.lf ", subtot/YRS); } printf(" \n"); } РИС. 12.4. Метеорологическая программа. ГОД КОЛИЧЕСТВО ОСАДКОВ (дюймы) 1970 50.6 1971 41.9 1972 28.6 1973 32.3 1974 37.8 Среднегодовое количество осадков составляет 38.2 дюйма. ЕЖЕМЕСЯЧНОЕ КОЛИЧЕСТВО: Янв. Фев. Mар. Апр. Mай. Июн. Июл. Авг. Ceнт. OКТ. Нояб. Дек. 7.8 7.2 4.1 3.0 2.1 0.8 1.2 0.3 0.5 1.7 3.6 6.l В этой программе следует отметить два основных момента: инициализацию и вычисления. Инициализация сложнее, поэтому мы сначала рассмотрим вычисления. Чтобы найти общее количество осадков за год, мы не изменяем |