1.7.1. Методы доступа к элементам массивов

В языке СИ между указателями и массивами существует тесная связь. Например, когда объявляется массив в виде int array[25], то этим определяется не только выделение памяти для двадцати пяти элементов массива, но и для указателя с именем array, значение которого равно адресу первого по счету (нулевого) элемента массива, т.е. сам массив остается безымянным, а доступ к элементам массива осуществляется через указатель с именем array. С точки зрения синтаксиса языка указатель arrey является константой, значение которой можно использовать в выражениях, но изменить это значение нельзя.

Поскольку имя массива является указателем допустимо, например, такое присваивание:


         int arrey[25];

         int *ptr;

         ptr=array;

Здесь указатель ptr устанавливается на адрес первого элемента масcива, причем присваивание ptr=arrey можно записать в эквивалентной форме ptr=&arrey[0].

Для доступа к элементам массива существует два различных способа. Первый способ связан с использованием обычных индексных выражений в квадратных скобках, например, array[16]=3 или array[i+2]=7. При таком способе доступа записываются два выражения, причем второе выражение заключается в квадратные скобки. Одно из этих выражений должно быть указателем, а второе - выражением целого типа. Последовательность записи этих выражений может быть любой, но в квадратных скобках записывается выражение следующее вторым. Поэтому записи array[16] и 16[array] будут эквивалентными и обозначают элемент массива с номером шестнадцать. Указатель используемый в индексном выражении не обязательно должен быть константой, указывающей на какой-либо массив, это может быть и переменная. В частности после выполнения присваивания ptr=array доступ к шестнадцатому элементу массива можно получить с помощью указателя ptr в форме ptr[16] или 16[ptr].

Второй способ доступа к элементам массива связан с использованием адресных выражений и операции разадресации в форме *(array+16)=3 или *(array+i+2)=7. При таком способе доступа адресное выражение равное адресу шестнадцатого элемента массива тоже может быть записано разными способами *(array+16) или *(16+array).

При реализации на компьютере первый способ приводится ко второму, т.е. индексное выражение преобразуется к адресному. Для приведенных примеров array[16] и 16[array] преобразуются в *(array+16).

Для доступа к начальному элементу массива (т.е. к элементу с нулевым индексом) можно использовать просто значение указателя array или ptr. Любое из присваиваний


       *array = 2;

       array[0] = 2;

       *(array+0) = 2;

       *ptr = 2;

       ptr[0] = 2;

       *(ptr+0) = 2;

присваивает начальному элементу массива значение 2, но быстрее всего выполнятся присваивания *array=2 и *ptr=2, так как в них не требуется выполнять операции сложения.

1.7.2. Указатели на многомерные массивы

Указатели на многомерные массивы в языке СИ - это массивы массивов, т.е. такие массивы, элементами которых являются массивы. При объявлении таких массивов в памяти компьютера создается несколько различных объектов. Например при выполнении объявления двумерного массива int arr2[4][3] в памяти выделяется участок для хранения значения переменной arr, которая является указателем на массив из четырех указателей. Для этого массива из четырех указателей тоже выделяется память. Каждый из этих четырех указателей содержит адрес массива из трех элементов типа int, и, следовательно, в памяти компьютера выделяется четыре участка для хранения четырех массивов чисел типа int, каждый из которых состоит из трех элементов. Такое выделение памяти показано на схеме на рис.3.

arr
в
arr[0]
а
arr[0][0]
arr[0][1]
arr[0][2]
arr[1]
а
arr[1][0]
arr[1][1]
arr[1][2]
arr[2]
а
arr[2][0]
arr[2][1]
arr[2][2]
arr[3]
а
arr[3][0]
arr[3][1]
arr[3][2]
Рис.3. Распределение памяти для двумерного массива.

Таким образом, объявление arr2[4][3] порождает в программе три разных объекта: указатель с идентификатором arr, безымянный массив из четырех указателей и безымянный массив из двенадцати чисел типа int. Для доступа к безымянным массивам используются адресные выражения с указателем arr. Доступ к элементам массива указателей осуществляется с указанием одного индексного выражения в форме arr2[2] или *(arr2+2). Для доступа к элементам двумерного массива чисел типа int должны быть использованы два индексных выражения в форме arr2[1][2] или эквивалентных ей *(*(arr2+1)+2) и (*(arr2+1))[2]. Следует учитывать, что с точки зрения синтаксиса языка СИ указатель arr и указатели arr[0], arr[1], arr[2], arr[3] являются константами и их значения нельзя изменять во время выполнения программы.

Размещение трехмерного массива происходит аналогично и объявление float arr3[3][4][5] порождает в программе кроме самого трехмерного массива из шестидесяти чисел типа float массив из четырех указателей на тип float, массив из трех указателей на массив указателей на float, и указатель на массив массивов указателей на float.

При размещении элементов многомерных массивов они располагаются в памяти подряд по строкам, т.е. быстрее всего изменяется последний индекс, а медленнее - первый. Такой порядок дает возможность обращаться к любому элементу многомерного массива, используя адрес его начального элемента и только одно индексное выражение.

Например, обращение к элементу arr2[1][2] можно осуществить с помощью указателя ptr2, объявленного в форме int *ptr2=arr2[0] как обращение ptr2[1*4+2] (здесь 1 и 2 это индексы используемого элемента, а 4 это число элементов в строке) или как ptr2[6]. Заметим, что внешне похожее обращение arr2[6] выполнить невозможно так как указателя с индексом 6 не существует.

Для обращения к элементу arr3[2][3][4] из трехмерного массива тоже можнo использовать указатель, описанный как float *ptr3=arr3[0][0] с одним индексным выражением в форме ptr3[3*2+4*3+4] или ptr3[22].

Далее приведена функция, позволяющая найти минимальный элемент в трехмерном массиве. В функцию передается адрес начального элемента и размеры массива, возвращаемое значение - указатель на структуру, содержащую индексы минимального элемента.


    struct INDEX {  int i,

                    int j,

                    int k }  min_index ;



    struct INDEX  * find_min (int *ptr1, int l, int m int n)

    {     int  min, i, j, k, ind;

          min=*ptr1;

          min_index.i=min_index.j=min_index.k=0;

          for (i=0; i*(ptr1+ind)

               {  min=*(ptr1+ind);

                  min_index.i=i;

                  min_index.j=j;

                  min_index.k=k;

               }

          }

          return &min_index;

    }

1.7.3. Операции с указателями

Над указателями можно выполнять унарные операции: инкремент и декремент. При выполнении операций ++ и -- значение указателя увеличивается или уменьшается на длину типа, на который ссылается используемый указатель.

Пример:


        int *ptr, a[10];

           ptr=&a[5];

           ptr++;      /* равно адресу элемента a[6] */

           ptr--;      /* равно адресу элемента a[5] */

В бинарных операциях сложения и вычитания могут участвовать указатель и величина типа int. При этом результатом операции будет указатель на исходный тип, а его значение будет на указанное число элементов больше или меньше исходного.

Пример:


        int *ptr1, *ptr2, a[10];

        int i=2;

           ptr1=a+(i+4);   /*  равно адресу элемента a[6] */

           ptr2=ptr1-i;    /*  равно адресу элемента a[4] */

В операции вычитания могут участвовать два указателя на один и тот же тип. Результат такой операции имеет тип int и равен числу элементов исходного типа между уменьшаемым и вычитаемым, причем если первый адрес младше, то результат имеет отрицательное значение.

Пример:


        int *ptr1, *ptr2, a[10];

        int i;

           ptr1=a+4;

           ptr2=a+9;

           i=ptr1-ptr2;  /*   равно 5   */

           i=ptr2-ptr1;  /*   равно -5  */

Значения двух указателей на одинаковые типы можно сравнивать в операциях ==, !=, <, <=, >, >= при этом значения указателей рассматриваются просто как целые числа, а результат сравнения равен 0 (ложь) или 1 (истина).

Пример:


      int *ptr1, *ptr2, a[10];

         ptr1=a+5;

         ptr2=a+7;

         if (prt1>ptr2) a[3]=4;

В данном примере значение ptr1 меньше значения ptr2 и поэтому оператор a[3]=4 не будет выполнен.

1.7.4. Массивы указателей

В языке СИ элементы массивов могут иметь любой тип, и, в частности, могут быть указателями на любой тип. Рассмотрим несколько примеров с использованием указателей.

Следующие объявления переменных


     int a[]={10,11,12,13,14,};

     int *p[]={a, a+1, a+2, a+2, a+3, a+4};

     int **pp=p;

порождают программные объекты, представленные на схеме на рис.4.

  pp
в
  p
а . . . . .
в в в в в
  a
а
  11
  12
  13
  14
  15
Рис.4. Схема размещения переменных при объявлении.

При выполнении операции pp-p получим нулевое значение, так как ссылки pp и p равны и указывают на начальный элемент массива указателей, связанного с указателем p ( на элемент p[0]).

После выполнения операции pp+=2 схема изменится и примет вид, изображенный на рис.5.

  pp
в
  p
а . . . . .
в в в в в
  a
а
  10
  11
  12
  13
  14
Рис.5. Схема размещения переменных после выполнения операции pp+=2.

Результатом выполнения вычитания pp-p будет 2, так как значение pp есть адрес третьего элемента массива p. Ссылка *pp-a тоже дает значение 2, так как обращение *pp есть адрес третьего элемента массива a, а обращение a есть адрес начального элемента массива a. При обращении с помощью ссылки **pp получим 12 - это значение третьего элемента массива a. Ссылка *pp++ даст значение четвертого элемента массива p т.е. адрес четвертого элемента массива a.

Если считать, что pp=p, то обращение *++pp это значение первого элемента массива a (т.е. значение 11), операция ++*pp изменит содержимое указателя p[0], таким образом, что он станет равным значению адреса элемента a[1].

Сложные обращения раскрываются изнутри. Например обращение *(++(*pp)) можно разбить на следующие действия: *pp дает значение начального элемента массива p[0], далее это значение инкременируется ++(*p) в результате чего указатель p[0] станет равен значению адреса элемента a[1], и последнее действие это выборка значения по полученному адресу, т.е. значение 11.

В предыдущих примерах был использован одномерный массив, рассмотрим теперь пример с многомерным массивом и указателями. Следующие объявления переменных


     int a[3][3]={  { 11,12,13 },

                    { 21,22,23 },

                    { 31,32,33 }   };

     int *pa[3]={ a,a[1],a[2] };

     int *p=a[0];

порождают в программе объекты представленные на схеме на рис.6.


Рис.6. Схема размещения указателей на двумерный массив.

Согласно этой схеме доступ к элементу a[0][0] получить по указателям a, p, pa при помощи следующих ссылок: a[0][0], *a, **a[0], *p, **pa, *p[0].

Рассмотрим теперь пример с использованием строк символов. Объявления переменных


     char *c[]={ "abs", "dx", "yes", "no" };

     char **cp[]={ c+3, c+2 , c+1 , c };

     char ***cpp=cp;

можно изобразить схемой представленной на рис.7.


Рис.7. Схема размещения указателей на строки.

1.7.5. Динамическое размещение массивов

При динамическом распределении памяти для массивов следует описать соответствующий указатель и присваивать ему значение при помощи функции calloc. Одномерный массив a[10] из элементов типа float можно создать следующим образом


      float *a;

      a=(float*)(calloc(10,sizeof(float));

Для создания двумерного массива вначале нужно распределить память для массива указателей на одномерные массивы, а затем распределять память для одномерных массивов. Пусть, например, требуется создать массив a[n][m], это можно сделать при помощи следующего фрагмента программы:


   #include 

   main ()

       {  double **a;

          int n,m,i;

          scanf("%d %d",&n,&m);

          a=(double **)calloc(m,sizeof(double *));

          for (i=0; i<=m; i++)

              a[i]=(double *)calloc(n,sizeof(double));

         . . . . . . . . . . . .

         }

Аналогичным образом можно распределить память и для трехмерного массива размером n,m,l. Следует только помнить, что ненужную для дальнейшего выполнения программы память следует освобождать при помощи функции free.


   #include 

   main ()

       {  long ***a;

          int n,m,l,i,j;

          scanf("%d %d %d",&n,&m,&l);

          /* --------  распределение памяти -------- */

          a=(long ***)calloc(m,sizeof(long **));

          for (i=0; i<=m; i++)

             {    a[i]=(long **)calloc(n,sizeof(long *));

                  for (j=0; i<=l; j++)

                  a[i][j]=(long *)calloc(l,sizeof(long));

              }

         . . . . . . . . . . . .

          /* --------- освобождение памяти ----------*/

          for (i=0; i<=m; i++)

              {   for (j=0; j<=l; j++)

                  free (a[i][j]);

                  free (a[i]);

               }

           free (a);

         }

Рассмотрим еще один интересный пример, в котором память для массивов распределяется в вызываемой функции, а используется в вызывающей. В таком случае в вызываемую функцию требуется передавать указатели, которым будут присвоены адреса выделяемой для массивов памяти.


     Пример:

   #include 

   main()

   {   int vvod(double ***, long **);

       double **a;     /*  указатель для массива  a[n][m]   */

       long *b;        /*  указатель для массива  b[n]      */

       vvod (&a,&b);

        ..   /* в функцию vvod передаются адреса указателей, */

        ..   /* а не их значения                             */

        ..

    }

    int vvod(double ***a, long **b)

    {   int n,m,i,j;

        scanf (" %d %d ",&n,&m);

        *a=(double **)calloc(n,sizeof(double *));

        *b=(long *)calloc(n,sizeof(long));

        for (i=0; i<=n; i++)

            *a[i]=(double *)calloc(m,sizeof(double));

        .....

     }

Отметим также то обстоятельство, что указатель на массив не обязательно должен показывать на начальный элемент некоторого массива. Он может быть сдвинут так, что начальный элемент будет иметь индекс отличный от нуля, причем он может быть как положительным так и отрицательным.

Пример:


    #include 

    int main()

    {  float *q, **b;

       int i, j, k, n, m;

       scanf("%d %d",&n,&m);

       q=(float *)calloc(m,sizeof(float));

    /* сейчас указатель q показывает на начало массива    */

       q[0]=22.3;

       q-=5;

    /* теперь начальный элемент массива имеет индекс 5,   */

    /* а конечный элемент индекс n-5                      */

       q[5]=1.5;

    /* сдвиг индекса не приводит к перераспределению      */

    /* массива в памяти и изменится начальный элемент     */

       q[6]=2.5;   /*  -  это второй элемент              */

       q[7]=3.5;   /*  -  это третий элемент              */

       q+=5;

    /* теперь начальный элемент вновь имеет индекс 0,     */

    /* а значения элементов q[0], q[1], q[2] равны        */

    /* соответственно 1.5, 2.5, 3.5                       */

       q+=2;

    /* теперь начальный элемент имеет индекс -2,          */

    /* следующий -1, затем 0 и т.д. по порядку            */

       q[-2]=8.2;

       q[-1]=4.5;

       q-=2;

    /* возвращаем начальную индексацию, три первых        */

    /* элемента массива q[0], q[1], q[2], имеют           */

    /* значения 8.2, 4.5, 3.5                             */

       q--;

    /* вновь изменим индексацию .                         */

    /* Для освобождения области памяти в которой размещен */

    /* массив q используется функция free(q), но поскольку */

    /* значение указателя q смещено, то выполнение     */

    /* функции free(q) приведет к непредсказуемым последствиям. */

    /* Для правильного выполнения этой функции    */

    /* указатель q должен быть возвращен в первоначальное */

    /* положение                                          */

       free(++q);

    /* Рассмотрим возможность изменения индексации и      */

    /* освобождения памяти для двумерного массива         */

       b=(float **)calloc(m,sizeof(float *));

       for (i=0; i < m; i++)

           b[i]=(float *)calloc(n,sizeof(float));

    /* После распределения памяти начальным элементом      */

    /* массива будет элемент b[0][0]                       */

    /* Выполним сдвиг индексов так, чтобы начальным        */

    /* элементом стал элемент b[1][1]                      */

       for (i=0; i < m ; i++) --b[i];

       b--;

    /* Теперь присвоим каждому элементу массива сумму его  */

    /* индексов                                            */

       for (i=1; i<=m; i++)

           for (j=1; j<=n; j++)

               b[i][j]=(float)(i+j);

    /* Обратите внимание на начальные значения счетчиков   */

    /* циклов i и j, он начинаются с 1 а не с 0            */

    /* Возвратимся к прежней индексации                    */

       for (i=1; i<=m; i++) ++b[i];

       b++;

    /* Выполним освобождение памяти                        */

       for (i=0; i < m; i++)  free(b[i]);

       free(b);

       ...

       ...

       return 0;

    }

В качестве последнего примера рассмотрим динамическое распределение памяти для массива указателей на функции, имеющие один входной параметр типа double и возвращающие значение типа double.

Пример:


   #include 

   #include 

    double cos(double);

    double sin(double);

    double tan(double);

   int main()

   { double (*(*masfun))(double);

     double x=0.5, y;

     int i;

     masfun=(double(*(*))(double))

              calloc(3,sizeof(double(*(*))(double)));

     masfun[0]=cos;

     masfun[1]=sin;

     masfun[2]=tan;

     for (i=0; i<3; i++);

     {  y=masfun[i](x);

        printf("\n x=%g  y=%g",x,y);

     }

     return 0;

   }

[ Назад | Оглавление | Вперед ]