Радиоконструктор

Язык C, Часть 29

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

Понятия указателей и массивов тесно связаны. Рассмотрим следующий фрагмент программы:
char str[80], *p1;
p1 = str;
Здесь p1 указывает на первый элемент массива str. Обратиться к пятому элементу массива str можно с помощью любого из двух выражений:
str[4]
* (p1+4)
Массив начинается с нуля. Поэтому для пятого элемента массива str нужно использовать индекс 4. Можно также увеличить p1 на 4, тогда он будет указывать на пятый элемент. (Напомним, что имя массива без индекса возвращает адрес первого элемента массива.)
В языке С существуют два метода обращения к элементу массива: адресная арифметика и индексация массива. Стандартная запись массивов с индексами наглядна и удобна в использовании, однако с помощью адресной арифметики иногда удается сократить время доступа к элементам массива. Поэтому адресная арифметика часто используется в программах, где существенную роль играет быстродействие. 



В следующем фрагменте программы приведены две версии функции putstr(), выводящей строку на экран. В первой версии используется индексация массива, а во второй — адресная арифметика:
/* Индексация указателя s как массива. */
void putstr(char *s)
{
  register int t;

  for(t=0; s[t]; ++t) putchar(s[t]);
}

/* Использование адресной арифметики. */
void putstr(char *s)
{
  while(*s) putchar(*s++);
}
Большинство профессиональных программистов сочтут вторую версию более наглядной и удобной. Для большинства компиляторов она также более быстродействующая. Поэтому в процедурах такого типа приемы адресной арифметики используются довольно часто.

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

Как и объекты любых других типов, указатели могут быть собраны в массив. В следующем операторе объявлен массив из 10 указателей на объекты типа int:
int *x[10];
Для присвоения, например, адреса переменной var третьему элементу массива указателей, необходимо написать:
x[2] = &var;
В результате этой операции, следующее выражение принимает то же значение, что и var:
*x[2]
Для передачи массива указателей в функцию используется тот же метод, что и для любого другого массива: имя массива без индекса записывается как формальный параметр функции. Например, следующая функция может принять массив x в качестве аргумента:
void display_array(int *q[])
{
  int t;

  for(t=0; t<10; t++)
    printf("%d ", *q[t]);
}
Необходимо помнить, что q — это не указатель на целые, а указатель на массив указателей на целые. Поэтому параметр q нужно объявить как массив указателей на целые. Нельзя объявить q просто как указатель на целые, потому что он представляет собой указатель на указатель.
Массивы указателей часто используются при работе со строками. Например, можно написать функцию, выводящую нужную строку с сообщением об ошибке по индексу num:
void syntax_error(int num)
{
  static char *err[] = {
    "Нельзя открыть файл\n",
    "Ошибка при чтении\n",
    "Ошибка при записи\n",
    "Некачественный носитель\n"
  };

  printf("%s", err[num]);
}
Массив err содержит указатели на строки с сообщениями об ошибках. Здесь строковые константы в выражении инициализации создают указатели на строки. Аргументом функции printf() служит один из указателей массива err, который в соответствии с индексом num указывает на нужную строку с сообщением об ошибке. Например, если в функцию syntax_error() передается num со значением 2, то выводится сообщение Ошибка при записи.
Отметим, что аргумент командной строки argv (см. главу 6) также является массивом указателей на строковые константы. 

Многоуровневая адресация

Иногда указатель может ссылаться на указатель, который ссылается на число. Это называется многоуровневой адресацией. Иногда применение таких указателей существенно усложняет программу, делает ее плохо читаемой и подверженной ошибкам. Рис. 5.3 иллюстрирует концепцию многоуровневой адресации. На рисунке видно, что значением "нормального" указателя является адрес объекта, содержащего нужное значение. В случае двухуровневой адресации первый указатель содержит адрес второго указателя, который содержит адрес объекта с нужным значением.
Многоуровневая адресация может иметь сколько угодно уровней, однако уровни глубже второго, т.е. указатели более глубокие, чем "указатели на указатели" применяются крайне редко. Дело в том, что при использовании таких указателей часто встречаются концептуальные ошибки из-за того, что смысл таких указателей представить трудно.
На заметкуНе следует путать многоуровневую адресацию с многоуровневыми структурами данных, использующими указатели, такими, например, как связные списки. Это фундаментально различные концепции.
Переменная, являющаяся указателем на указатель, должна быть соответствующим образом объявлена. Это делается с помощью двух звездочек перед именем переменной. Например, в следующем операторе newbalance объявлена как указатель на указатель на переменную типа float:
float **newbalance;
Следует хорошо понимать, что newbalance — это не указатель на число типа float, а указатель на указатель на число типа float.
Рис. 5.3. Одноуровневая и многоуровневая адресация
       Указатель         Переменная
       +--------+        +--------+
       | Адрес  |------->|Значение|
       +--------+        +--------+
   
          Одноуровневая адресация

Указатель       Указатель       Переменная
+--------+      +--------+      +--------+
| Адрес  |----->| Адрес  |----->|Значение|
+--------+      +--------+      +--------+

          Многоуровневая адресация
При двухуровневой адресации для доступа к значению объекта нужно поставить перед идентификатором две звездочки:
#include <stdio.h>

int main(void)
{
  int x, *p, **q;

  x = 10;
  p = &x;
  q = &p;

  printf("%d", **q); /* печать значения x */

  return 0;
}
Здесь p объявлена как указатель на целое, a q — как указатель на указатель на целое. Функция printf() выводит на экран число 10

Инициализация указателей

После объявления нестатического локального указателя до первого присвоения он содержит неопределенное значение. (Глобальные и статические локальные указатели при объявлении неявно инициализируются нулем.) Если попытаться использовать указатель перед присвоением ему нужного значения, то скорее всего он мгновенно разрушит программу или всю операционную систему. Это очень досадная ошибка.
При работе с указателями большинство программистов придерживаются следующего важного соглашения: указатель, не ссылающийся в текущий момент времени должным образом на конкретный объект, должен содержать нулевое значение. Нуль используется потому, что С гарантирует отсутствие чего-либо по нулевому адресу. Следовательно, если указатель равен нулю, то это значит, во-первых, что он ни на что не ссылается, а во-вторых — что его сейчас нельзя использовать.
Указателю можно задать нулевое значение, присвоив ему 0. Например, следующий оператор инициализирует р нулем:
char *p = 0;
Дополнительно к этому во многих заголовочных файлах языка С, например, в <stdio.h> определен макрос NULL, являющийся нулевой указательной константой. Поэтому в программах на С часто можно увидеть следующее присваивание:
p = NULL;
Однако равенство указателя нулю не делает его абсолютно "безопасным". Использование нуля в качестве признака неподготовленности указателя — это только соглашение программистов, но не правило языка С. В следующем примере компиляция пройдет без ошибки, а результат, тем не менее, будет неправильным:
int *p = 0;
*p = 10; /* ошибка! */
В этом случае присваивание посредством p будет присваиванием по нулевому адресу, что обычно вызывает разрушение программы.
Во многих процедурах для повышения эффективности программы можно использовать то, что нулевой указатель заведомо считается неподготовленным для использования. Например, можно использовать нулевой указатель как признак конца массива указателей (по аналогии с нулевым терминатором строки). Процедура, использующая массив указателей, таким образом узнает о конце массива. Такой подход иллюстрируется в таком примере. Просматривая список имен, функция search() определяет, есть ли в этом списке заданное имя.
#include <stdio.h>
#include <string.h>

int search(char *p[], char *name);

char *names[] = {
  "Сергей",
  "Юрий",
  "Ольга",
  "Игорь",
  NULL}; /* Нулевая константа кончает список */

int main(void)
{
  if(search(names, "Ольга") != -1)
    printf("Ольга есть в списке.\n");

  if(search(names, "Павел") == -1)
    printf("Павел в списке не найден.\n");

  return 0;
}

/* Просмотр имен. */
int search(char *p[], char *name)
{
  register int t;

  for(t=0; p[t]; ++t)
    if(!strcmp(p[t], name)) return t;

    return -1; /* имя не найдено */
}
В функцию search() передаются два параметра. Первый из них, p — массив указателей на строки, представляющие собой имена из списка. Второй параметр name является указателем на строку с заданным именем. Функция search() просматривает массив указателей, пока не найдет строку, совпадающую со строкой, на которую указывает name. Итерации цикла for повторяются до тех пор, пока не произойдет совпадение имен, или не встретится нулевой указатель. Конец массива отмечен нулевым указателем, поэтому при достижении конца массива управляющее условие цикла примет значение ЛОЖЬ. Иными словами, p[t] имеет значение ЛОЖЬ, когда p[t] является нулевым указателем. В рассмотренном примере именно это и происходит, когда идет поиск имени "Павел", которого в списке нет.
В программах на С указатель типа char * часто инициализируют строковой константой (как в предыдущем примере). Рассмотрим следующий пример:
char *p = "тестовая строка";
Переменная р является указателем, а не массивом. Поэтому возникает логичный вопрос: где хранится строковая константа "тестовая строка"? Так как p не является массивом, она не может храниться в p, тем не менее, она где-то записана. Чтобы ответить на этот вопрос, нужно знать, что происходит, когда компилятор встречает строковую константу. Компилятор создает так называемую таблицу строк, в ней он сохраняет строковые константы, которые встречаются ему по ходу чтения текста программы. Следовательно, когда встречается объявление с инициализацией, компилятор сохраняет строку "тестовая строка" в таблице строк, а в указатель p записывает ее адрес. Дальше в программе указатель p может быть использован как любая другая строка. Это иллюстрируется следующим примером:
#include <stdio.h>
#include <string.h>

char *p = "тестовая строка";

int main(void)
{
  register int t;

  /* печать строки слева направо и справа налево */
  printf(p);
  for(t=strlen(p)-1; t>-1; t--) printf("%c", p[t]);

  return 0;
}



Комментарии