Шаг 38.
Работа с массивами с помощью указателей

    Этот шаг посвящен еще одному способу работы с массивами.

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

    Значением объекта типа указатель является целое неотрицательное число, равное адресу того программного объекта (переменной, функции, массива и т.д.), на который ссылается указатель.

    Заметим, что в языке C++ гарантируется, что ни один "правильный" указатель не может иметь значение 0. Так что возвращение нуля в качестве значения указателя может служить сигналом о ненормальном завершении функции.

    Для того, чтобы начать работу с указателями, необходимо их предварительно описать. Например:

   int feet[20], /* Целочисленный массив (20 элементов). */
       *pfeet;   /* Указатель на целое.                  */

    Перед указателем pfeet на массив feet мы употребили символ операции *. Ясно, что *pfeet (содержимое области памяти, в которой расположен элемент массива) имеет тип int.

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

    Приведем еще один пример описания массива:

   int (*a)[15];

    Описан указатель на массив, содержащий 15 целых значений. Круглые скобки здесь необходимы, поскольку скобки [] имеют более высокий уровень старшинства, чем операция *.

    Теперь вспомним операцию нахождения адреса &. Например, запись:

   &pooh[3]

означает указатель на элемент с индексом 3 массива pooh. Скобки [] имеют более высокий уровень старшинства, чем операция &.

    Операция & применима только к переменным!

    Имя массива определяет также адрес его первого элемента, т.е. если nise[] является массивом, то nise==&nise[0] и обе части равенства определяют адрес первого элемента массива. Оба обозначения являются константами типа указатель, поскольку они не изменяются на протяжении всей программы. Однако их можно присваивать переменной типа указатель и изменять значение переменной, как показано в следующем примере.


    Пример 1.
   #include<iostream.h>
   int nise[4] = { 1,2,3,4 };
   main ()
   {
      int *pr;
      pr = nise; /* Вот необходимое нам присваивание */
                 /*   переменной pr значения nise.   */
      pr += 1; cout << "Адрес = " << pr << ", значение = " << *pr << endl;
      pr += 2; cout << "Адрес = " << pr << ", значение = " << *pr << endl;
   }
Текст этой программы можно взять здесь.

    А что произойдет со значением указателя, если к нему прибавить 1? Компилятор языка C++ добавит единицу памяти, т.е. если массив имеет тип int, то значение указателя увеличится на два (переменная типа int занимает два байта), если массив - типа float, то значение указателя увеличится на четыре (переменная типа float занимает четыре байта), и если массив - типа char, то значение указателя увеличится на единицу (переменная типа char занимает один байт). Вот почему необходимо специально оговаривать тип объекта, на который ссылается указатель; одного адреса здесь недостаточно, так как компилятор должен знать, сколько байтов потребуется для запоминания объекта. (Это справедливо также для указателя на скалярные переменные).

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

   pr = nise;
в примере 1 выражения pr+2 и &nise[2] будут представлять собой один и тот же адрес, а выражения *(pr+2) и nise[2] - одно и то же значение.

    Заметим, что имеется существенное различие между именем массива nise и указателем pr на этот массив: указатель является переменной, так что для него операция типа pr++ имеет смысл, а имя массива - это константа, так что конструкции типа:

   nise = pr;
   nise++;
   pr = &nise;
являются незаконными.


    Пример 2. Получение размера объекта.
   #include<iostream.h>
   main ()
   {
      int d[5];
      char f[3];
      int *g[2];
      char (*w)[4];
      /* ----------------------------------- */
      cout << "sizeof (d[5]) =" << sizeof (d[5]) << endl;
      cout << "sizeof (f[3]) =" << sizeof (f[3]) << endl;
      cout << "sizeof(int *[2])=" << sizeof(int *[2]) << endl;
      cout << "sizeof(char (*)[3])=" << sizeof(char (*)[3]) << endl;
   }
Текст этой программы можно взять здесь.

    Результат работы программы:

   sizeof d[5] =2
   sizeof f[3] =1
   sizeof(int *[2])=8
   sizeof(char (*)[3])=4

    Теперь покажем, как можно использовать указатель для представления i-го элемента массива. Например, рассмотрим следующий фрагмент программы:

   int *pa;
   pa = &a[0];
   x = *pa; y = *(pa+1);
   z = *(pa+i); //Разумеется, i уже инициализирована.
   pb = pa+n;   //Разумеется, n уже инициализирована.

    Первые два оператора означают, что переменная pa является указателем на начало (первый элемент) целочисленного массива a. Тогда в х заносится содержимое a[0], в y - a[1], в z - a[i], в pb копируется адрес элемента массива a[n].

    Еще раз напомним, что суть добавления 1 к указателю состоит в том, что приращение масштабируется размером памяти того объекта, на который ссылается указатель.

    Заметим еще, что компилятор превращает имя массива в указатель, поэтому присваивания:

   pa = &a[0];
   pa = a;
совершенно равноценны.

    Теперь ясно, что i-й элемент массива можно представить в виде a[i], или pa[i], или *(pa+i), или *(a+i), а адрес i-го элемента есть &a[i], либо &pa[i], либо pa+i, либо a+i.

    Теперь переделаем программу ввода массива в программу с использованием указателей.


    Пример 3.
   #include<iostream.h>
   main ()
   {
      int score[10], *pscore;
      //Ввод элементов массива.
      pscore = &score[0];
      for (int i=0; i<=9; i++)
         cin >> *(pscore+i);
      //Проверка правильности ввода.
      cout << "Введены следующие результаты:\n";
      for (i=0; i<=9; i++)
         cout << *(pscore+i) << " ";
      cout << endl;
   }
Текст этой программы можно взять здесь.

    Есть возможность инициализировать указатель в программе. Например, операторы:

   char a[50];
   char *pa = a;

описывают символьный массив a[] типа char и указатель pa на объект типа char, а так же инициализирует pa так, чтобы он указывал на начало массива a[].

    В конце шага отметим, что если p и q - указатели на элементы некоторого массива, то:



    На следующем шаге мы приведем несколько примеров использования указателей и массивов.


Предыдущий шаг Содержание Следующий шаг