Краткое введение в то, что такое указатели, почему они существуют и как их использовать
Возможно, одним из наиболее сложных аспектов любого языка программирования низкого уровня является использование указателей. Если вам когда-либо приходилось сталкиваться с концепциями, лежащими в их основе, и с тем, как они работают, вы не одиноки!
Я впервые полностью осознал указатели, когда работал над встроенным проектом C с использованием микросхем Atmel. Там памяти было мало, и мне пришлось ее экономно использовать. Это было отличное введение в преимущества использования указателей.
Какую проблему они решают?
Когда компьютеры запускают код, они передают переменные, такие как целые числа, числа с плавающей запятой, строки и структуры, по значению во время выполнения. То есть они:
- сделать копию переменной
- передать его в функцию
- вернуть скопированную переменную, перезаписав исходную
Возьмите следующий фрагмент GO. Он устанавливает структуру User
и проверяет, достаточно ли возраст пользователя u
, чтобы употреблять алкоголь в Великобритании.
Здесь пользователь u
создается в строке 18 и копируется при передаче в isOldEnough
в строке 20. Это называется передачей по значению. К тому времени, как программа находится в строке 26, в куче уже есть 2 экземпляра переменной u
.
Почему это плохо?
Обычно это не так. Создание копий переменных во время выполнения обычно не является проблемой для большинства современных приложений. Например; Все больше и больше программного обеспечения пишется на Python. Целые отрасли переходят на этот язык, и его можно найти в финансовой, оборонной и медицинской отраслях, это серьезный язык и нет концепции указателей.
можно считать наилучшей практикой хранить данные в одном месте вместо того, чтобы копировать их повсюду, но опять же, это зависит от размера и применения кода. Указатели могут сделать ремонтопригодность проблемой, а арифметика указателей, как известно, сложна.
Скорость и пространство
Что, если бы объект User
был больше и включал другие поля, такие как username
, email
, profileImage
или даже resume
? Он может вырасти в большой объект, копирование которого может оказаться дорогостоящим. В этой ситуации было бы лучше передать исходный блок памяти в isOldEnough()
или, скорее, дать функции меньшую переменную, чтобы компилятор знал, где находится исходный блок памяти. Эта меньшая переменная является указателем, и это называется передачей по ссылке.
Теперь вместо того, чтобы u
передавалось как (u)
, адрес u
передается с использованием оператора &
, а isOldEnough()
было сказано ожидать указатель в строке 26.
Важно отметить, что указатели - это просто переменные адреса памяти. Это немного усложняется из-за количества перестановок вокруг типов указателей и присваиваний, которые мы рассмотрим ниже.
Но по сути, это все. Указатели существуют, поэтому мы можем передавать физическую память вместо переменных, которые являются абстракцией этой памяти.
Почему бы не использовать их постоянно?
- Накладные расходы на обслуживание и сложность: может быть довольно сложно следить за архитектурой, если все является указателем.
- Область действия: иногда в больших классах или функциях бывает сложно определить, имеете ли вы дело с переменной, имеющей область действия этой функции, или какие-либо изменения, которые вы вносите, будут иметь последствия в другом месте кода. Помните, что работа с указателями означает, что вы изменяете содержимое памяти напрямую, тогда как обычные переменные можно рассматривать как «попробуй и зафиксируй», когда они передаются в функцию и возвращаются из нее.
Создание указателей
Как мы видели выше, переменные - это просто абстракция распределения памяти, поэтому возьмите:
var x int32 = 5
это можно представить как:
и поэтому при следующей структуре и назначении:
type User struct { ID int Firstname string Lastname string Age int }john := User {ID: "5",
Firstname: "John",
Lastname: "Dory", Age: 40}
это можно представить как:
Затем мы можем создать указатель на структуру, содержащую значения:
johnsPointer:= &john println(johnsPointer) // would print something like 0x00070
Благодаря динамической проверке типов в GO нам не нужно явно называть его указателем. Используя :=
operator, он поможет нам. Помните, что указатели - это просто переменные, которые содержат адреса памяти, поэтому передача адреса чего-то вроде &john
GO знает, что это указатель. В противном случае нам пришлось бы объявить его как: var johnPointer *User = &john
, что является более подробным, но также действительным.
Почему объявление типа?
Фактический размер указателя на диске одинаков для всех типов указателей. Приведенный ниже код выводит размер 2 указателей, одного типа int32
и одного типа User
.
usr := User{1, "Foo", "Manchu", 17} var x int32 = 4 var intPointer *int32 = &x var johnPointer2 *User = &usr fmt.Println(unsafe.Sizeof(johnPointer2)) fmt.Println(unsafe.Sizeof(intPointer)) // Both output 8(bytes)
Итак, если указатели - это просто переменные адреса памяти, тогда зачем вам указывать тип, как в var johnPointer *User
?
Это сделано для того, чтобы компилятор знал, сколько памяти нужно прочитать при разыменовании указателя.
Использование указателей
Итак, теперь вы передаете johnPointer
и хотите получить доступ к его содержимому. Вы делаете это, разыменовывая указатель, то есть получая содержимое той ячейки памяти, на которую он указывает. В GO есть разные способы сделать это явно и неявно:
// both of these print the whole contents of the struct fmt.Println(johnPointer) // implicit fmt.Println(*johnPointer) // explicit // both of these print the Age variable of the struct fmt.Println(johnPointer2.Age) // implicit fmt.Println((*johnPointer2).Age) // explicit
ПРИМЕЧАНИЕ: будьте осторожны при использовании println
, это не так умно, как fmt.Println
.
Заключение
Хорошее общее практическое правило - использовать указатели только там, где вам нужно и где вы не можете использовать что-либо еще. Это будет иметь место, если вам нужно заботиться о производительности выполнения или использовании памяти.
Документация Golang здесь действительно полезна: