Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Code Ninja: Что такое указатели?

Оригинал: Code ninja: What are pointers?
Автор: Graham Morrison
Дата публикации: 9 апреля 2016 г.
Перевод: А.Панин
Дата перевода: 6 мая 2016 г.

Вы когда-нибудь задавались вопросом о том, что означают мистические символы "*" и "&" и каким образом они используются в коде? После чтения данной статьи у вас не будет возникать подобных вопросов.

Для чего это нужно?

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

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

Понимание принципа работы указателей позволит вам лучше понять принцип работы переменных, а также оценить масштаб работы, выполняемой вашими компилятором и компьютером в процессе превращения объявлений переменных в исполняемый код. Если вы следите за статьями серии "Школа ассемблера", указатели также окажутся мостом между описанной в этих статьях моделью адресации памяти и переменными в большинстве высокоуровневых языков программирования. Мы много раз упоминали переменные, так как указатель по своей сути очень похож на переменную (например, переменную 'i', значение которой присваивается следующим образом 'i = 1'), причем указатель может использоваться для выполнения очень похожих операций. При этом указатели являются даже более гибкими, чем переменные. Эта гибкость не объясняется повышенной сложностью их использования по сравнению с переменными - и это ключевой момент для понимания сути указателей - они являются более гибкими, так как с ними проще работать и их роли более четко определены.

В левой колонке приведены варианты осуществления доступа к адресу переменной в памяти (сверху) и ее значению (снизу)

В левой колонке приведены варианты осуществления доступа к адресу переменной в памяти (сверху) и ее значению (снизу)

Гибкость

Когда я говорил, что с указателями "проще работать", я имел в виду то, что они являются чем-то средним между ассемблерным представлением переменной и своеобразной высокоуровневой функцией, ведущей себя как обычная переменная. Это промежуточное звено является очень важным, ведь программист может использовать его в соответствии со своими потребностями. По этой причине указатели очень ценятся определенной частью программистов на языках C и C++, использующих указатели чаще всех остальных.

А это классический пример использования указателей при работе с языком программирования C++:

#include <iostream>

int main ()
{
	int variable = 123;
	int *pointer;
	
	std::cout << "Переменная: " << variable << "\n";
	pointer = &variable;
	std::cout << "Указатель: " << *pointer << "\n";
	
	return 0;
}

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

Единственными фрагментами приведенного выше кода, которые могут быть непонятны, являются объявления std::cout и операторы <<. В рамках упомянутого объявления cout является обычной функцией для отправки текста в стандартный поток вывода. Префикс std:: обозначает, что эта функция принадлежит пространству имен/классу с именем std, который был импортирован из библиотеки iostream в рамках первой строки кода. Оператор, состоящий из двух символов "меньше", <<, используется в данном случае для той же цели, что и в командной оболочке Bash в Linux, а именно, для перенаправления данных в поток стандартного вывода.

Следует сохранить этот код в файл с именем pointer.cpp. Если у вас установлено какое-либо сборочное окружение, например, то, которое устанавливается автоматически при сборке программного обеспечения из исходных кодов средствами менеджера пакетов программного обеспечения дистрибутива Arch Linux, вы сможете скомпилировать и осуществить связывание исполняемого файла с помощью следующей команды:

g++ -Wall pointer.cpp -o pointer

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

$ ./pointer
Переменная: 123
Указатель: 123

В первую очередь в нашем исходном коде создается целочисленная переменная с именем variable, которая используется для хранения значения 123. После этого создается еще одна целочисленная переменная с именем pointer. Также вы должны были заметить, что между описанием типа второй переменной (int) и ее именем находится символ звездочки (*), который используется как раз для объявления указателя. Символ звездочки является одним из двух символов, которые указывают на использование в коде указателей, причем вторым таким символом является ампрерсанд (&). Символ звездочки используется в первую очередь, так как он позволяет создать указатель вместо полноценной переменной. Указатель не хранит значение переменной, как в случае объявления 'int variable = 123'. Вместо этого он хранит ссылку (в данном случае под ссылкой понимается адрес данных указанного типа в памяти; не стоит путать его со специальным ссылочным типом языка C++ - прим. пер.) на данные переменной типа, указанного перед символом звездочки. Следовательно, мы создали объект, который будет хранить ссылку на целочисленную переменную. Эта ссылка обычно является адресом в памяти, по которому хранятся данные переменной, причем в конечном итоге он используется именно в качестве адреса данных определенного типа, а не произвольных данных в памяти.

Унарные операторы

Иногда точная позиция символа звездочки изменяется, тем не менее, этот символ всегда используется для обозначения факта использования указателя. Символ звездочки известен как унарный оператор, то есть, оператор, который работает с единственным операндом - следующим после него значением. Оба упомянутых символа, используемые для работы с указателями, являются унарными операторами. Вы можете, к примеру, забыть о неоднозначности int и * и считать, что тип int * используется для строки специального типа, которая, разумеется, отличается от указателя на переменную целочисленного типа, но ввиду того, что * является оператором, а не частью объявления типа, будет наблюдаться описанная неоднозначность. Вам придется использовать символ звездочки при любом использовании объекта посредством указателя, так как это не определенный тип данных, а способ передачи ссылки. Это очень важно.

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

pointer = &variable;

Здесь используется еще один символ, связанный с указателями, а именно, амперсанд (&). Это унарный оператор, который используется для получения адреса/расположения в памяти данных переменной. Да, реальное расположение данных переменной соответствует адресу данных переменной в памяти. Это приемы, которые используются скорее в ассемблере, а не в современных окружениях разработки, но их поддержка была сохранена так как они позволяют выполнять некоторые удобные трюки, которые крайне сложно так же эффективно реализовать с помощью какого-либо другого метода. Обладая адресом переменной, переданным с помощью указателя, а также информацией о типе указателя, который в данном случае используется в качестве ссылки на целочисленное значение, компилятор может получить значение переменной, что мы и делаем в последней строке кода.

Информация о размере значения переменной по переданному адресу необходима компилятору для того, чтобы знать, какой объем данных следует вернуть и как нужно их интерпретировать. Именно поэтому при использовании объявления *pointer возвращается значение, хранящееся в памяти по переданному с помощью указателя адресу, а не само значение адреса. Это, в свою очередь, объясняет, почему возвращаемое значение является значением переменной variable, а не каким-то другим значением. Если вы желаете увидеть само значение адреса в памяти, вы можете просто удалить символ * и пересобрать код. После этого вы увидите в выводе строку, аналогичную 'Указатель: 0x7ffcba364334', которая содержит реальный адрес значения переменной 'variable' в памяти, хранимый в рамках указателя.

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

int newvariable = *pointer;

Теперь значение, хранящееся в переменной newvariable, является копией значения, на которое ссылается указатель pointer, а не копией значения самого указателя, разумеется, в том случае, если указатель является корректным. Если значение указателя изменяется, значение переменной newvariable не будет изменяться, так как указатель был разыменован и значение было скопировано. Все операции в данном коде связаны с указателями - переменная используется для создания ссылки, а оператор & - для возврата адреса в памяти, по которому хранится значение этой переменной. Так как в данном случае рассматривается расположение значения переменной в памяти, при изменении этого значения будет меняться и значение, возвращаемое при разыменовании указателя. Указатели оказываются очень удобными тогда, когда вы не желаете копировать или дублировать наборы данных большого объема - вы можете использовать их, например, для передачи указателей на функции в другие функции, а также для создания связанных списков.

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

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

Существует один важный сторонний эффект: вы должны быть крайне осторожны при работе с указателями и не оставлять неиспользуемых зарезервированных фрагментов памяти, а также некорректных указателей. Во многих языках программирования для этих целей используются сборщики мусора, но языки программирования C и C++ никак не помогают программисту в данном плане. Вы должны самостоятельно заботиться об освобождении зарезервированных фрагментов памяти и неиспользуемых указателей. Если вы хотите поупражняться в работе с указателями, я могу порекомендовать использовать для этого современный язык программирования с поддержкой указателей и системы автоматической сборки мусора, такой, как Go. Реализация указателей в данном языке программирования очень похожа на их реализацию в таких языках программирования, как C и C++, что упростит эксперименты.

Указатели являются анахронизмом, который лучше всего использовать лишь при особых обстоятельствах. В языках программирования C и C++ они являются единственным механизмом для выполнения определенных операций со сложными типами данных, причем ввиду своего примитивизма, они являются чрезвычайно быстрыми. При этом всегда полезно знать о том, как они работают и что позволяют сделать. Вы наверняка встретите их в процессе чтения кода ядра и утилит Linux, причем в коде ядра они используются особенно часто, а сами по себе указатели являются полезным механизмом, который может использоваться тогда, когда язык программирования не предоставляет какой-либо альтернативы.