Трудности при работе с указателями

Содержание

Ничто не может доставить больше неприятностей, чем «дикий» указатель! Указатели похожи на обоюдоострое оружие: их возможности огромны, однако обезвредить ошибки в них особенно трудно.

Ошибочный указатель трудно найти потому, что ошибка в самом указателе никак себя не проявляет. Проблемы возникают при попытке обратиться к объекту с помощью этого указателя. Если значение указателя неправильное, то программа с его помощью обращается к произвольной ячейке памяти. При чтении в программу попадают неправильные данные, а при записи искажаются другие данные, хранящиеся в памяти, или портится участок программы, не имеющий никакого отношения к ошибочному указателю. В обоих случаях ошибка может не проявиться вовсе или проявиться позже в форме, никак не указывающей на ее причину.

Поскольку ошибки, связанные с указателями, особенно трудно обезвредить, при работе с указателями следует соблюдать особую осторожность. Рассмотрим некоторые ошибки, наиболее часто возникающие при работе с указателями. Классический пример — неинициализированный указатель:

/* Это программа содержит ошибку. */
int main(void)
{
  int x, *p;

  x = 10;
  *p = x; /* ошибка, p не инициализирован */

  return 0;
}

Эта программа присваивает значение 10 некоторой неизвестной области памяти. Рассмотрим, почему это происходит. Хотя указателю р не было присвоено никакого значения, но в момент выполнения операции *р = х он имел некоторое (совершенно произвольное!) значение. Поэтому здесь имела место попытка выполнить операцию записи в область памяти, на которую указывал данный указатель. В небольших программах такая ошибка часто остается незамеченной, потому что если программа и данные занимают немного места, то «выстрел наугад» скорее всего будет «промахом». С увеличением размера программы вероятность «попасть» в нее возрастает.

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

Вторая распространенная ошибка заключается в простом недоразумении при использовании указателя:

/* Это программа содержит ошибку. */
#include <stdio.h>

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

  x = 10;
  p = x;

  printf("%d", *p);

  return 0;
}

Вызов printf() не выводит на экран значение х, равное 10. Выводится произвольная величина, потому что оператор

p = x;

записан неправильно. Он присваивает значение 10 указателю, однако указатель должен содержать адрес, а не значение. Правильный оператор выглядит так:

p = &x;

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

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

char s[80], y[80];
char *p1, *p2;

p1 = s;
p2 = y;
if(p1 < p2) . . .

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

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

int first[10], second[10];
int *p, t;

p = first;
for(t=0; t<20; ++t)  *p++ = t;

Так присваивать значения массивам first и second нельзя. Если компилятор разместит массивы рядом, это может и не привести к неправильному результату. Однако подобная ошибка особенно неприятна тем, что при проверке она может остаться незамеченной, а потом компилятор будет размещать массивы по-другому и программа выполнится неправильно.

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

/* Это программа с ошибкой. */
#include <string.h>
#include <stdio.h>

int main(void)
{
  char *p1;
  char s[80];

  p1 = s;
  do {
    gets(s);  /* чтение строки */

    /* печать десятичного эквивалента
       каждого символа */
    while(*p1) printf(" %d", *p1++);

  } while(strcmp(s, "выполнено"));

  return 0;
}

Программа печатает значения символов ASCII, находящихся в строке s. Печать осуществляется с помощью p1, указывающего на s. Ошибка состоит в том, что указателю p1 присвоено значение s только один раз, перед циклом. В первой итерации p1 правильно проходит по символам строки s, однако в следующей итерации он начинает не с первого символа, а с того, которым закончил в предыдущей итерации. Так что во второй итерации p1 может указывать на середину второй строки, если она длиннее первой, или же вообще на конец остатка первой строки. Исправленная версия программы записывается так:

/* Это правильная программа. */
#include <string.h>
#include <stdio.h> 

int main(void)
{
  char *p1;
  char s[80];

  do {
    p1 = s; /* установка p1 в начало строки s */
    gets(s);  /* чтение строки */

    /* печать десятичного эквивалента
       каждого символа */
    while(*p1) printf(" %d", *p1++);

  } while(strcmp(s, "выполнено"));

  return 0;
}

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

То, что неправильные указатели могут быть очень «коварными», не может служить причиной отказа от их использования. Следует лишь быть осторожным и внимательно проанализировать каждое применение указателя в программе.