Чем больше я C, тем меньше я вижу. - Неизвестно

Программирование на C / C ++ обычно считается очень гибким и мощным из-за наличия указателей (среди прочего, мы не собираемся крышка здесь!).

Указатели великолепны - они предоставляют вам доступ к памяти, помогают сократить время выполнения вашей программы, используются для создания сложных структур данных (таких как деревья, связанные списки и т. Д.) И так далее. Тем не менее, не все, что блестит, - золото - указатели на самом деле представляют собой опасные «разновидности», которые могут привести к таким проблемам, как сбои программы, повреждение данных, ненужная сложность кода ( просто взгляните на картинку выше!) и сложные процессы отладки. Из-за всех этих проблем некоторые языки программирования (например, Java) вообще не имеют типов переменных-указателей (у них есть что-то довольно близкое, но не совсем то же самое - мы рассмотрим все это в конце этой части).

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

Что такое указатель и откуда он

Трудно отследить, кто первым изобрел указатель, но, согласно Википедии, заслуга Гарольда Лоусона, который ввел его в язык PL / I. Изобретение указателей приблизило программирование высокого уровня к языку ассемблера с косвенной адресацией.

указатель - это переменная, значением которой может быть действительный (или кажущийся действительным) адрес памяти или ноль (т. Е. Недопустимый адрес). Указатели помогают получить доступ к динамическому хранилищу - например, к переменным, выделенным из кучи (т.е. свободному хранилищу) - и, следовательно, могут буквально указывать на любую область памяти, даже ограниченную.

Назначение и разыменование

Когда указатель назначен, его можно интерпретировать двумя способами:

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

Разыменование может быть явным или неявным. Явное разыменование указывается с помощью этого оператора в C / C ++: * (называемого оператором типа). Например:

int value = 20;
int *p = &value; // stores the address of variable 'value' in p
cout << "Address is stored here " << p << endl;
cout << "Value stored here is " << *p << endl;

Поскольку указатели могут эффективно использоваться для управления бесплатным хранилищем (т.е. они могут указывать куда угодно!), C / C ++ включает явные операторы выделения и освобождения. Например, в C ++ пример операции выделения / освобождения будет выглядеть так:

int *p = new int(25); // declare and assign value, i.e. allocate
delete p; // deallocate

Обратите внимание, что указатели также могут использоваться для указания на функции (опять же - см. Рисунок выше!).

Проблемы с указателями

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

int *p;
int *q = new int(30);
p = q;
delete q;

Указателю q сначала было присвоено 30. После этого мы устанавливаем указатель p так, чтобы он указывал на тот же адрес, что и q. Однако впоследствии q был освобожден. Итак, теперь оба p и q болтаются, поскольку они содержат адрес освобожденной переменной.

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

int *p = new int(40);
int *q = new int(30);
p = q; // 40 is lost

Указатель арифметики

Да, верно - вы можете выполнять вычисления с указателями (хотя и в ограниченной форме). Например, возможно что-то вроде этого:

int myArray [20];
int *p;
p = myArray;

Семантика приведенного выше фрагмента кода такова, что p теперь назначается адрес myArray[0].

Следующее также является законным:

*(p + 2) == myArray[2];
*(p + idx) == myArray[idx];
p[idx] == myArray[idx];

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

p++;
p--;

Указатели можно использовать для сравнения с помощью традиционных операторов отношения, как в этом примере:

int *p;
int value = 20;
p = value;
if (p == &value) {cout << "Equal" << endl;}

Примечание. Амперсанд (&) в коде возвращает адрес переменной.

Более безопасная альтернатива - справочные типы

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

  • В то время как указатели относятся к адресам памяти, ссылки относятся к фактическому значению, хранящемуся в памяти. Это возможно, потому что ссылки всегда неявно разыменовываются. В этом отношении они более безопасны, чем указатели, использующие явное разыменование. Более того, как только ссылкам присваивается какое-то значение, они никогда не могут быть повторно установлены для ссылки на другую переменную. Например:
int a = 1;
int b = 2;
int &r = a; // references must be initialized as soon as created
int &r = b; // this is illegal

Примечание. Ссылки указываются с использованием адреса оператора (&).

  • Хотя указатели могут быть нулевыми, ссылки не могут - все ссылки должны относиться к некоторым существующим объектам / значениям в памяти, независимо от того, действительны они или нет.
  • Ссылки имеют тот же адрес, что и исходная переменная, на которую они ссылаются. Поскольку указатели могут быть переназначены и указывать на любое место в памяти, они имеют свои собственные адреса и хранят их в стеке.

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

Почему некоторые программисты избегают указателей?

Если в программе используются указатели, программисты должны помнить все из них и все о каждом указателе, что действительно сложно сделать. Очень сложно понять и отследить, на что указывает указатель на разных этапах выполнения программы. Изменилось ли значение, на которое он указывает? Указатель был назначен на другой адрес? Указывает ли он на действительную переменную? Более того, чем больше косвенных указаний, тем сложнее становится код.

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

Ссылки

Себеста, Р. В. (2016). Концепции языков программирования, 11-е издание. Глава 6