Ошибки при работе с указателями 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Ошибки при работе с указателями



Указатели – это очень мощное, полезное, но и очень опасное средство. Ошибки, которые возникают при неправильном использовании указателей, кроме того, что могут приводить к серьезным и непредсказуемым ошибкам в работе программы, еще и очень трудно диагностировать (обнаруживать).

Основная и наиболее часто встречающаяся ошибка при работе с указателями связана с использованием неинициализированных указателей.

Рассмотрим следующий пример:

 

int *p1;

*p1 = 1001;

cout << *p1;

 

Хотя с точки зрения синтаксиса этот фрагмент программы корректен, попытка его выполнения закончится, скорее всего, плачевно. Когда мы определяем указатель (int *p1;), в некотором участке памяти создается обычная переменная – указатель, но поскольку значения этой переменной никакого не присвоено (она не инициализирована), то ее значение будет соответствовать тем случайным данным (“мусору”), которые содержались в этом участке памяти. Таким образом, неинициализированный указатель будет содержать некоторый случайный адрес. Дальнейшие попытки обратиться по этому адресу в память могут привести к одному из двух неприятным последствиям. Если это случайное значение адреса будет указывать на недопустимую область памяти (например, за пределами памяти, выделенной для нашей программы), то возникнет ошибка времени выполнения, и программа аварийно завершит свою работу. Но может быть и хуже. Если случайно значение указателя будет содержать адрес, принадлежащей области памяти нашей программы, то произойдет непредсказуемое изменение данных программы. Она (программа) может продолжить свою работу, а последствия такого несанкционированного изменения в программе могут сказаться значительно позднее и вызвать некорректное поведение программы. Обнаружить причину возникновения подобных ошибок чрезвычайно трудно.

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

 

int *p1 = 0;

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

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

 

If (p1) // Если указатель не равен 0, то все в порядке

{

*p1 = 1001;

cout << *p1;

}

Else

// Реакция на ошибочную ситуацию

;

Вторая группа ошибок может быть связана с некорректным использованием арифметики указателей. При некорректном выполнении наращивания или уменьшения указателей с помощью операций + или - можно выйти за пределы предполагаемого объекта (например, массива) и в результате получить неверные данные или модифицировать не те значения.

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

Третья группа ошибок относится к использованию операций сравнения > или <. Использование этих операций по отношению к указателям может служить для определения относительного расположения в памяти объектов, на которые ссылаются сравниваемые указатели. Однако такое сравнение имеет смысл, если идет сравнение адресов связанных между собой объектов (например, элементов массива, которые гарантированно упорядочены внутри массива). При сравнении с помощью этих операций указателей на несвязанные между собой переменные результат таких сравнений непредсказуем, так как фактическое расположение в памяти не связанных переменных может отличаться от порядка, в котором эти переменные определялись в программе (это зависит от компилятора).

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

 

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

В изучаемых нами языках программирования между массивами и указателями имеется очень тесная связь.

Кода мы определяем в программе некоторый массив, например,

 

int Arr[10]

переменная Arr без индексов представляет собой указатель на первый элемент массива в данном случае из 10 целых чисел (содержит адрес первого элемента массива). Если вывести на экран значение переменной Arr

cout << Arr:

мы увидим некоторое целое значение в шестнадцатеричном формате, соответствующее адресу первого элемента этого массива.

Замечание. Именно по этой причине в языке C++ отсутствует операция присвоения сразу всех значений одного массива другому (в некоторых других языках, например, в Pascal такая возможность имеется). Действительно, если имеются два массива

 

int A1[10], A2[10]

то попытка выполнить присвоение A1 = A2 привела бы к тому, что переменная A1 стала бы указывать на ту же область памяти, что и переменная A2 (мы скопировали адрес из A2 в A1, а не содержимое одного массива в другой). Адрес, который хранился ранее в переменной A1, был бы утерян, что привело бы к утечке памяти (для десяти элементов массива A1 в памяти было выделено место, но теперь мы “забыли”, где оно находится, то есть потеряли память). По этой причине подобные операции с массивами в языке C++ запрещены. Более того, запрещены любые изменения значения переменной массива.

Указателю, имеющему такой же базовый тип, как и элементы массива, можно присвоить массив следующим образом:

 

int Arr[10];

int *p;

p = Arr;

 

Но обратное присвоение выполнить невозможно:

 

Arr = p; // Ошибка

Такое присвоение невозможно, поскольку переменная массива – это константа, изменение которой запрещено.

Так как переменная массива является указателем на первый элемент массива, появляются дополнительные возможности по работе с массивами на основе использования арифметики указателей. Например, чтобы получить 5–й элемент массива Arr можно воспользоваться одним из следующих выражений:

 

Arr[4] или *(Arr + 4) или *(p + 4)

 

Первое выражение – это пример обычной индексации элементов массива. Во втором и третьем выражениях мы использовали арифметику указателей и с помощью операции + получили адрес пятого элемента массива. Затем с помощью операции * взяли значение по этому адресу и получили значение 5-го элемента массива. Обратите внимание на скобки в этих выражениях, если их не поставить и написать * Arr + 4 или *p + 4, то эти выражения будут равны значению первого элемента массива увеличенного на 4, так как операция * имеет больший приоритет, чем операция +.

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

 

int A[10];

for (int i = 0; i < 10; ++ i)

{

cin >> A[i];

A[i] = A[i] * A[i];

}

for (int i = 0; i < 10; ++ i)

cout << A[i] << “ “;

cout << endl;

…..

 

А вот тот же фрагмент, но с использованием арифметики указателей:

 

int A[10];

for (int *Next = A, *End = Next + 9; Next <= End; ++ Next)

{

cin >> *Next;

*Next = *Next * *Next; // *Next = (*Next) * (*Next);

}

for (int *Next = A, *End = Next + 9; Next <= End; ++ Next)

cout << *Next << “ “;

cout << endl;

…..

 

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

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

 

int A[10], *P = A;

for (int i = 0; i < 10; ++ i)

cout << P[i] << “ “;

 

Можно создавать и массивы указателей. Например:

 

int a = 1, b = 2, c = 3, *M[3];

M[0] = & a; // Элементам массива М присваиваются адреса переменных a, b и c

M[1] = & b;

M[2] = & c;

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

cout << *M[i] << “ ”;

cout << endl;

 

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

 

С помощью массивов указателей можно моделировать различные интересные конструкции данных. Например, пусть имеется квадратная матрица размерности 5 х 5 симметричная относительно главной диагонали. Для ее однозначного представления достаточно хранить в памяти не все 25 элементов этой матрицы, а только 15 (например, элементы под главной диагональю вместе с элементами главной диагонали). Для этого можно предложить следующую конструкцию:

 

int A1[1], A2[2], A3[3], A4[4], A5[5], *A[5] = { A1, A2, A3, A4, A5 };

// Вводим 15 целых значений - элементы под главной диагональю и диагональные

// элементы матрицы

for (int i = 0; i < 5; ++i)

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

cin >> A[i][j];

cout << endl;

// Выводим симметричную матрицу 5 на 5 на экран

for (int i = 0; i < 5; ++i)

{

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

cout << A[i][j] << " ";

for (int j = i + 1; j < 5; ++ j)

cout << A[j][i] << " ";

cout << endl;

}

cout << endl;

 

А это пятиэлементный массив указателей на символы, инициализированный некоторыми текстовыми строками:

 

char * Words[5] = { "Слово1", "Слово2", "Слово3", "Слово4", "Слово5" }

 

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

 



Поделиться:


Последнее изменение этой страницы: 2017-02-07; просмотров: 703; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 52.14.130.13 (0.031 с.)