dates + 2 == &dates[2] /* один и тот же адрес */
*(dates + 2) == dates[2] /* одно и то же значение */
Эти соотношения суммируют тесную связь между массивами и указателями. Они показывают, что можно использовать указатель для определения отдельного элемента массива, а также для получения его значения. По существу мы имеем два различных обозначения для одного и того же. Действительно, компилятор превращает обозначение массива в указатели, поэтому метод указателей более предпочтителен.
Между прочим, постарайтесь различать выражения * (dates + 2), и *dates + 2. Операция (*) имеет более высокий приоритет, чем +, поэтому последнее выражение означает
(* 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 (уears)
int *years;
{
}
Действительно, операторы
int years [];
int *years;
синонимы. Оба они объявляют переменную years указателем массива целых чисел. Однако главное их отличие состоит в том, что первый из них напоминает нам, что указатель years ссылается на массив.
Как теперь связать его с массивом ages? Вспомним, что при использовании указателя в качестве аргумента, функция взаимодействует с соответствующей переменной в вызывающей программе, т.е. операторы, использующие указатель years в функции convert (), фактически работают с массивом 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. Оператор вызова функции может выглядеть следующим образом:
/* X54.C */
#include <stdio.h>
void main(void)
{
int n = 9,i;
int array[] = {1,3,5,9,0,-9,-5,-3,-1};
int mean(int array[],int n);
i = mean(array,n);
printf("Среднее из заданных значений %d\n",i);
}
int mean(int array[],int n)
{
int index;
long sum;
if(n > 0)
{
for(index = 0, sum = 0; index < n; index++)
sum += array[index];
return((int) (sum/n));
}
else
{
printf("Нет массива\n");
return(0);
}
}
Эту программу легко переделать, применяя указатели. Объявим ра указателем на тип int. Затем заменим элемент массива array[index] на соответствующее значение: *(ра + index).
/* Использование указателей для нахождения среднего значения
массива n целых чисел */
int mean(pa, n)
int *pa, 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 pa[];
и
int *pa;
идентичны по действию: оба объявляют ра указателем. В программе можно применять любой из них, хотя до сих пор мы использовали второй в виде *(ра + index).
Понятно ли вам, как работать с указателями? Указатель устанавливается на первый элемент массива, и значение, находящееся там, добавляется в sum. Затем указатель передвигается на следующий элемент (к указателю прибавляется единица), и значение, находящееся в нем, также прибавляется к sum и т. д. Это похоже на механизм работы с массивом, где индекс действует как стрелка часов, показывающая по очереди на каждый элемент массива.
Теперь у нас есть два подхода: какой же из них выбрать? Во-первых, хотя массивы и указатели тесно связаны, у них есть отличия. Указатели являются более общим и широко применяемым средством, однако многие пользователи (по крайней мере, начинающие) считают, что массивы более привычны и понятны. Во-вторых, при использовании указателей у нас нет простого эквивалента для задания размера массива. Самую типичную ситуацию, в которой можно применять указатель, мы уже показали: это функция работающая с массивом, который находится где-то в другой части программы. Мы предлагаем использовать любой из подходов по вашему желанию. Однако несомненное преимущество использования указателей в приведенном выше примере должно научить вас легко применять их, когда в этом возникает необходимость.
Что же мы теперь умеем делать с указателями? Язык Си предлагает пять основных операций, которые можно применять к указателям, а нижеследующая программа демонстрирует эти возможности. Чтобы показать результаты каждой операции, мы будем печатать значение указателя (являющегося адресом, на который ссылается указатель), значение, находящееся по этому адресу, и адрес самого указателя.
/* операции с указателями */
#define PR(X) printf(" X = %u, *X = %d, &X = %u\n", X, *X, &X)
/* печатает значение указателя (адрес), значение, находящееся по */
/* этому адресу, и адрес самого указателя */
main()
static int urn[] = {100, 200, 300};
int *ptr1, *ptr2;
{
ptr1 = urn; /* присваивает адрес указателю */
ptr2 = &urn[2]; /* то же самое */
PR(ptr1); /* см. макроопределение, указанное выше */
ptr1++; /* увеличение указателя */
PR(ptr1);
PR(ptr2);
++ptr2; /* выходит за конец массива */
PR(ptr2);
printf(“ptr2 – ptr1 = %u\n", ptr2 – ptr1);
}
В результате работы программы получены следующие результаты:
ptr1 = 18, *ptr1 = 100, &ptr1 = 55990
ptr1 = 20, *ptr1 = 200, &ptr1 = 55990
ptr2 = 22, *ptr2 = 300, &ptr2 = 55992
ptr2 = 24, *ptr2 = 29808, &ptr2 -= 55992
ptr2 – ptr1 = 2
Программа демонстрирует пять основных операций, которые можно выполнять над переменными типа указатель.
1. ПРИСВАИВАНИЕ. Указателю можно присвоить адрес. Обычно мы выполняем это действие, используя имя массива или операцию получения адреса (&). Программа присваивает переменной ptr1 адрес начала массива urn; этот адрес принадлежит ячейке памяти с номером 18. (В нашей системе статические переменные запоминаются в ячейках оперативной памяти.) Переменная ptr2 получает адрес третьего и последнего элемента массива, т. е. urn [2].
2. ОПРЕДЕЛЕНИЕ ЗНАЧЕНИЯ. Операция * выдает значение, хранящееся в указанной ячейке. Поэтому результатом операции *ptr1 в самом начале работы программы является число 100, находящееся в ячейке с номером 18.
3. ПОЛУЧЕНИЕ АДРЕСА УКАЗАТЕЛЯ. Подобно любым переменным переменная типа указатель имеет адрес и значение. Операция & сообщает нам, где находится сам указатель. В нашем примере указатель ptr1 находится в ячейке с номером 55990. Эта ячейка содержит число 18, являющееся адресом начала массива urn.
4. УВЕЛИЧЕНИЕ УКАЗАТЕЛЯ. Мы можем выполнять это действие с помощью обычной операции сложения либо с помощью операции увеличения. Увеличивая указатель, мы перемещаем его на следующий элемент массива. Поэтому операция ptr1++ увеличивает числовое значение переменной ptr1 на 2 (два байта на каждый элемент массива целых чисел), после чего указатель ptr1 ссылается уже на urn[1] (смотри рисунок). Теперь ptr1 имеет значение 20 (адрес следующего элемента массива), а операция *ptr1 выдает число 200, являющееся значением элемента urn[1]. Заметим, что адрес самой ячейки ptr1 остается неизменным, т. е. 55990. После выполнения операции сама переменная не переместилась, потому что она только изменила значение!
Аналогичным образом можно и уменьшить указатель.
Однако при этом следует соблюдать осторожность. Машина не следит, ссылается ли еще указатель на массив или уже нет. Операция ++ptr2 перемещает указатель ptr2 на следующие два байта, и теперь он ссылается на некоторую информацию, расположенную в памяти за массивом.