Указатель (тип данных)

Эта статья находится на начальном уровне проработки, в одной из её версий выборочно используется текст из источника, распространяемого под свободной лицензией
Материал из энциклопедии Руниверсалис
Переменная num объявляется и инициализируется. После чего объявляется переменная-указатель pNum. Затем указателю pNum присваивается адрес переменной num. Таким образом обе переменные можно использовать для доступа к одному и тому же месту в памяти.

Указатель (англ. pointer) — переменная, диапазон значений которой состоит из адресов ячеек памяти или специального значения — нулевого адреса. Последнее используется для указания того, что в данный момент указатель не ссылается ни на одну из допустимых ячеек. Указатели были изобретены Ющенко Екатериной Логвиновной в Адресном языке программирования (1955 г.), а не Harold Lawson в 1964 г., как долгое время считали за рубежом[1]. В 1955 г. в Адресном языке программирования были введены понятия косвенной адресации и адресации высших рангов, что покрывает понятие указателя и области его применения в современных языках программирования.

Область применения

Указатели применяются в двух сферах:

  • Работа в системе косвенной адресации (как в языках ассемблера). Одним из её преимуществ можно назвать экономию памяти. Делая указатель на файл, мы читаем его с диска, а не загружаем в ОЗУ. Передавая указатель на переменную в функцию мы не делаем копию этой переменной, а редактируем её напрямую[2]. Указатели используют для хранения адресов точек входа для подпрограмм в процедурном программировании и для подключения динамических подключаемых библиотек.
  • Динамическое управление памятью. В таком случае выделяется место в так называемой куче (динамической памяти), а переменные, для которых память выделена таким образом, называются динамическими[3]. В языке Си нет понятия строковой переменной, так что для строк часто используют указатель на массив символов.

Действия над указателями

Языки программирования, в которых предусмотрен тип указателей, содержат, как правило, две основные операции над ними: присваивание и разыменование.

В 1955 г. Адресном языке программирования (СССР) была введена «штрих-операция» (разыменование указателя), которая была аппаратно реализована Ф-операцией процессора в компьютере «Киев» (1955 г.), а позднее в компьютерах М-20, «Днепр», компьютерах семейства БЭСМ (БЭСМ-2, БЭСМ-3, БЭСМ-3М и БЭСМ-4), семейств «Минск» и «Урал», также и некоторых других компьютерах советского производства. Многократное применение разыменования указателя также было аппаратно реализовано в указанных компьютерах групповыми операциями модернизации адресов для ускорения работы с деревобразными форматами (списки и другие абстрактные типы данных являются частным случаем деревобразных форматов).

Первая присваивает указателю некоторый адрес. Вторая служит для обращения к значению в памяти, на которое указывает указатель. Разыменование может быть явным и неявным; в большинстве современных языков программирования разыменование происходит только при явном указании[чего?][источник не указан 2778 дней].

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

int  n  = 6;    // Объявление переменной n типа int и присваивание ей значения 6
int *pn = malloc( sizeof ( int ) ); // Объявление указателя pn и выделение под него памяти
    *pn = 5;    // Разыменование указателя и присваивание значения 5
     n = *pn;   // Присваивание n того значения (5), на которое указывает pn
     free(pn);  // Освобождение занятой памяти
     pn = &n;   // Присваивание указателю pn адреса переменной n (указатель будет ссылаться на n)
     n = 7;     // *pn тоже стало равно 7

Унарный оператор & возвращает адрес переменной, а оператор * используется для разыменования:

int sourceNum1 = 100;
int sourceNum2 = 200;
int* pNum1 = &sourceNum1;
int* pNum2 = &sourceNum2;
printf("Pointer value of 1-%d, 2-%d\n", *pNum1, *pNum2);  
pNum1 = pNum2;
printf("Pointer value of 1-%d, 2-%d\n", *pNum1, *pNum2);

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

Языки, предусматривающие использование указателей для динамического распределения памяти, должны содержать оператор явного размещения переменных в памяти. В некоторых языках помимо этого оператора предусмотрен ещё и оператор явного удаления переменных из памяти. Обе эти операции часто принимают форму встроенных подпрограмм (функции malloc и free в Си, операторы new и delete в C++ и т. п.). При использовании простого, а не умного указателя следует всегда своевременно удалять переменную из памяти, дабы избежать утечки памяти.

Указатель на void

Указатель на тип void позволяет сослаться на любой тип данных, в том числе класс. Эта технология лежит в основе типа any библиотеки Boost.

class A {
    int field;
};

A clA;
void* pA = (void*)&clA; // указатель pA ссылается на объект класса А

Указатель на указатель (адресация высших рангов)

В программировании встречаются также указатели на указатели. Они хранят в себе адреса памяти, где находятся указатели на память, где расположен объект данных, или другой указатель. Цепочка указатель на указатель, который снова указывет на указатель позволяют ввести понятие многократного разыменования указателя (в Адресном языке программирования: «адресация высших рангов») и соответствующее действие над указателями: Multiple indirection.

int x, *p, **q;
x = 10;
p = &x;
q = &p; // указатель на указатель
printf ("%d", **q);

Нулевой указатель

Нулевой указатель — это указатель, хранящий специальное значение, показывающее, что данная переменная-указатель не ссылается (не указывает) ни на какой объект. В языках программирования он представлен особой константой[4]:

Основные проблемы применения

Указателями сложно управлять. Достаточно легко записать в указатель неправильное значение, что может вызвать трудновоспроизводимую ошибку. Например, вы случайно поменяли адрес указателя в памяти, или неправильно выделили под информацию память и тут вас может ожидать сюрприз: другая очень важная переменная, которая используется только внутри программы будет перезаписана. Понять, где именно находится ошибка и воспроизвести её будет нелегко, а устранение таких ошибок — не всегда тривиальная задача, иногда приходится переписывать существенную часть программы[6].

Для решения части проблем есть методы предохранения и страховки:

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

Пример ошибки с неинициализированным указателем:

/* программа неверна. */
int main (void)
{
    int x, *p; // Выделилась память под x, но не под *p
    x = 10;    // В память записано 10
    *p = x;    // 10 записывается в неопределённое место в памяти, что может привести к аварийному завершению программы.
    return 0;
}

В такой маленькой программе проблема может остаться незамеченной. Но, когда программа разрастется, то внезапно может выясниться, что переменная записана между других блоков данных, важных для программы. Чтобы избежать такой ситуации, просто инициализируйте указатель[6].

Используйте указатели правильно

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

#include <stdio.h>
/* программа неверна */
int main(void)
{
    int x, *p;
    x = 10;
    p = x;
    printf ("%d", *p);
    return 0;
}

Вызов printf() не выводит значения х, которое равно 10, на экран. Вместо этого выводится некоторое неизвестное значение — это результат неправильного использования оператора присваивания (р = х;). Этот оператор присваивает значение 10 указателю р, который должен содержать адрес, а не значение. К счастью, ошибка в данной программе обнаруживается компилятором — он выдаёт предупреждение о необычном преобразовании указателя. Для устранения ошибки следует написать p = &х;[6].

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

  • старайтесь инициализировать переменные сразу при объявлении (int x = 10;);
  • не смешивайте указатели с обычными переменными (например, int x, *p, y, *y_ptr;);
#include <stdio.h>
int main(void)
{
    int x = 10;
    int *p = &x;
    printf ("%d", *p);
    return 0;
}

Утечка памяти

Утечка памяти — процесс неконтролируемого уменьшения объёма свободной оперативной памяти (RAM) компьютера, связанный с ошибками в работающих программах, вовремя не освобождающих ненужные уже участки памяти, или с ошибками системных служб контроля памяти.

char *pointer = NULL;
int i = 0;
for( i = 0; i < 10; i++ )
{
    pointer = (char *)malloc(100); // Память выделяет 10 раз
}
free(pointer); // А освобождается только в последнем случае

Сравнение указателей

Адреса в памяти, присвоенные указателям, можно сравнивать. Сравнения вида pNum1 < pNum2 и pNum1 > pNum2 часто используются для последовательного перебора элементов массива в цикле: pNum1 соответствует текущему положению в памяти, а pNum2 — концу массива. pNum1 == pNum2 вернёт истину в том случае, если оба указателя указывают на одну ячейку памяти.

Адресная арифметика

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

Типичные операции адресной арифметики:

int* p; // Допустим, p указывает на адрес 200
p++;    // После инкрементации она указывает на 200 + sizeof(int) = 204
p--;    // Сейчас она вновь указывает на 200.

Умный указатель

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

Указатель в биологии человека

Мозг использует группы клеток, похожие на указатели, для решения некоторых задач, связанных с запоминанием новой информации[7].

Примечания

  1. Videla, Alvaro Kateryna L. Yushchenko — Inventor of Pointers (англ.). https://medium.com/. A Computer of One’s Own Pioneers of the Computing Age (Dec 8, 2018). Дата обращения: 30 жовтня 2020. Архивировано 23 сентября 2020 года.
  2. Для чего используются указатели?. Дата обращения: 20 февраля 2013. Архивировано 26 февраля 2013 года.
  3. 14.1. Распределение памяти (недоступная ссылка). — «Адрес начала выделенной памяти возвращается в точку вызова функции и записывается в переменную-указатель. Созданная таким образом переменная называется динамической переменной.». Дата обращения: 22 февраля 2013. Архивировано 25 июня 2013 года.
  4. Question 5.1. comp.lang.c Frequently Asked Questions. Дата обращения: 20 февраля 2013. Архивировано 26 февраля 2013 года.
  5. A name for the null pointer: nullptr (англ.). JTC1.22.32. JTC1/SC22/WG21 — The C++ Standards Committee (2 октября 2007). Дата обращения: 4 октября 2010. Архивировано 11 февраля 2012 года.
  6. 6,0 6,1 6,2 Проблемы, связанные с указателями. Дата обращения: 22 февраля 2013. Архивировано 26 февраля 2013 года.
  7. Мозг использует приемы из программирования для решения новых задач. РИА Новости (23 сентября 2013). Дата обращения: 13 сентября 2016. Архивировано 20 сентября 2016 года.