netlib.narod.ru< Назад | Оглавление | Далее >

5.3. Указатели и массивы

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

Объявление

  int a[10];

определяет массив a, состоящий из десяти последовательных объектов с именами a[0], a[1], ..., a[9].


Рис. 4.

Запись a[i] отсылает нас к i-му элементу массива. Если pa есть указатель на int, т.е. объявлен как

  int *pa;

то в результате присваивания

  pa = &a[0];

pa будет указывать на нулевой элемент a, иначе говоря, pa будет содержать адрес элемента a[0].


Рис. 5.

Теперь присваивание

  x = *pa;

будет копировать содержимое a[0] в x.

Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i — на i-й элемент после pa, а pa-i — на i-й элемент перед pa. Таким образом, если pa указывает на a[0], то

  *(pa+1)

есть содержимое a[1], pa+i — адрес a[i], а *(pa+i) — содержимое a[i].


Рис. 6.

Сделанные замечания верны независимо от типа и размера элементов массива a. Смысл слов «добавить 1 к указателю», как и смысл любой арифметики с указателями, состоит в том, чтобы pa+1 указывал на следующий объект, а pa+i — на i-й после pa.

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

  pa = &a[0];

pa и a имеют одно и то же значение. Поскольку имя массива является синонимом адреса его начального элемента, присваивание pa = &a[0] можно также записать в следующем виде:

  pa = a;

Еще более удивительно (по крайней мере на первый взгляд) то, что a[i] можно записать как *(a+i). Вычисляя a[i], Си сразу преобразует его в *(a+i); указанные две формы записи эквивалентны. Из этого следует,что записи &a[i] и a+i также будут эквивалентными, т.е. и в том и в другом случае это адрес i-го элемента массива a. С другой стороны, если pa — указатель, то его можно использовать с индексом, т.е. запись pa[i] эквивалентна записи *(pa+i). Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом.

Между именем массива и указателем, выступающим в роли имени массива, существует одно различие. Указатель — это переменная, поэтому можно написать pa = a или pa++. Но имя массива не является переменной, и записи вроде a = pa или a++ не допускаются.

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

  /* strlen: возвращает длину строки */
  int strlen(char *s)
  {
      int n;

      for (n = 0; *s != '\0'; s++)
          n++;
      return n;
  }

Так как переменная s — указатель, к ней применима операция ++; s++ не оказывает никакого влияния на строку символов в функции, которая обратилась к strlen. Просто увеличивается на 1 некоторая копия указателя,находящаяся в личном пользовании функции strlen. Это значит,что все вызовы, такие как:

  strlen("Здравствуй, мир");  /* строковая константа */
  strlen(array);              /* char array[100]; */
  strlen(ptr);                /* char *ptr; */

правомерны.

Формальные параметры

  char s[];

и

  char *s;

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

Функции можно передать часть массива, для этого аргумент должен указывать на начало подмассива. Например, если a — массив, то в записях

  f(&a[2])

или

    f(a+2)

функции f передается адрес подмассива, начинающегося с элемента a[2]. Внутри функции f описание параметров может выглядеть как

    f(int arr[]) { ... }

или

    f(int *arr) { ... }

Следовательно, для f тот факт, что параметр указывает на часть массива, а не на весь массив, не имеет значения.

Если есть уверенность, что элементы массива существуют, то возможно индексирование и в «обратную» сторону по отношению к нулевому элементу; выражения p[-1], p[-2] и т.д. не противоречат синтаксису языка и обращаются к элементам, стоящим непосредственно перед p[0]. Разумеется, нельзя «выходить» за границы массива и тем самым обращаться к несуществующим объектам.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz