Краткое введение в то, что такое указатели, почему они существуют и как их использовать

Возможно, одним из наиболее сложных аспектов любого языка программирования низкого уровня является использование указателей. Если вам когда-либо приходилось сталкиваться с концепциями, лежащими в их основе, и с тем, как они работают, вы не одиноки!

Я впервые полностью осознал указатели, когда работал над встроенным проектом 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 здесь действительно полезна: