указательОсновное значение состоит в том, чтобы хранить некоторое значение, гдеадрес памяти.
В Golang хоть и не все значения могут принимать адреса (хотя они тоже хранятся в памяти,например константа), но все переменные должны иметь возможность извлекать адреса.
Переменная — это значение, хранящееся в области памяти[1]. не только наши знакомыеvar x int
серединаx
является переменной, более сложное выражение также может представлять переменную, напримерsliceA[0]
,mapB["key"]
,а такжеstructC.FieldD
.也就是说,他们都可以有自己的指针。
Но вот вопрос, если изменится значение переменной, изменится ли его указатель?
Проанализируйте эту проблему, значение указателя меняется, оно будет связано только с адресом переменной, если адрес переменной не изменится, то и указатель не изменится. Итак, этот вопрос трансформируется в то, что изменение значения переменной изменит адрес памяти переменной? Ответ - без изменений[2].
Мы знаем, что если переменная имеет тип указателя, то он может хранить значение типа указателя, напримерvar ptr *int
Ptr может хранить значение типа указателя. Значение этой переменной может быть изменено, так что требуется только другое пространство памяти, но изменяется только значение этой переменной. Объем памяти самого ptr не изменился, т.е.&ptr
Всегда значение (если не происходит перемещение GC). Точно так же вопросы, которые мы упомянули выше, аналогичны заданиюvar a int
, если измененоa
значение ,&a
Изменится ли это? Ответ - нет.
Пример кода выглядит следующим образом:
b := 1
fmt.Printf("%p\n", &b) // 0x416028
b = 2
fmt.Printf("%p\n", &b) // 0x416028
c := &b
fmt.Printf("%p\n", c) // 0x416028
можно увидеть,b
Адрес памяти не изменился.
Но вот другой вопрос, если изменится значение переменной, изменится ли значение, на которое указывает его указатель (или значение, вынутое указателем)?
Ответ, очевидно, изменится. Потому что указатель переменной по-прежнему указывает на тот же адрес памяти, но значение по этому адресу изменилось. Пример:
type A struct {
Value int
}
a := A{Value: 1}
fmt.Printf("a-ptr: %p, value-ptr: %p, value: %d\n", &a, &a.Value, (&a).Value)
// a-ptr: 0x41602c, value-ptr: 0x41602c, value: 1
a = A{Value: 2}
fmt.Printf("a-ptr: %p, value-ptr: %p, value: %d\n", &a, &a.Value, (&a).Value)
// a-ptr: 0x41602c, value-ptr: 0x41602c, value: 2
Видно, что указатели не изменились (потому что поле Value — это первое поле структуры A, поэтому адрес памяти тот же), хотя мы даем переменнуюa
переназначен.
Различия между Golang и C
По сравнению с C указатели в Golang имеют 2 отличия (точнее, некоторые оптимизации):
1. Go может напрямую создать новый указатель структуры
В голанге мы можем пройтиptr := &A{Value: 1}
Также получается указатель на структурное значение A, но в C не будет получен отдельными операторами присваивания:
typedef struct {
int value;
} A;
A *ptr1; // 无法给 ptr 所指的值赋值
A *ptr2 = &A{1}; // 没有这样的语法
A a = {1}; // 再通过 &a 可以得到指针
Если эта разница это просто похоже на синтаксис, другая разница может быть разницей между ошибкой.
2. Безопасно возвращать указатель на локальную переменную в Go
В приведенном выше примере кода C мы действительно можем объявить некоторые переменные, но если эти объявления выполняются внутри метода, например:
A *init()
{
A *ptr;
return ptr;
}
или
A *init()
{
A a;
return &a;
}
Затем эта объявленная локальная переменная является автоматической переменной.[3]), исходный метод, который является методом init(), после окончания эти автоматические переменные "исчезают"[4].
Для версии с указателем прямого оператора мы проводим следующий эксперимент:
A *init(int value)
{
A *ptr;
printf("1. inside - ptr: %x, value: %d\n", ptr, ptr->value);
return ptr;
}
int main()
{
A *ptr = init(1);
printf("2. after return: ptr: %x, value: %d\n", ptr, ptr->value);
}
Результат может быть похож на:
1. inside - ptr: 1ad2f248, value: 25
2. after return - ptr: 1ad2f248, value: 25
Являются ли результаты неожиданными (на разных машинах результаты будут немного отличаться)? Мы объявили переменную типа pointer, но значение этой переменной, то есть фактический адрес памяти хранилища, не обязательно указывает на структуру A, и, скорее всего, это будет совершенно не относящийся к делу адрес. Это создает угрозу безопасности для программы, особенно если в адресе есть какие-то важные данные, к которым был получен случайный доступ.
Конечно, этот адрес также может быть недействительным, если вы хотите изменить значение в этом адресе, например:
ptr->value = 2;
Скорее всего, вы получите некоторые ошибки, например, в macOS вы получитеbus error
. То есть, когда программа хочет манипулировать значением по этому адресу памяти, она сталкивается с проблемой.
Аналогичным образом, для способа объявления значения структуры, а затем возврат указатель не будет иметь намерения вопросов. Мы делаем следующий эксперимент;
A *init(int value)
{
A a = {value};
printf("1. inside - ptr: %x, value; %d\n", &a, (&a)->value);
return &a;
}
int main() {
A *ptr = init(1);
printf("2. after return - ptr: %x, value: %d\n", ptr, ptr->value);
printf("3. after return - ptr: %x, value: %d\n", ptr, ptr->value);
A *ptr2 = init(2)
printf("4. after return - ptr: %x, value: %d\n", ptr, ptr->value); // Watch here!!!
}
Вы найдете результат, аналогичный этому (если это MacOS, результат будет ближе):
1. inside - ptr: e43de2d8, value: 1
2. after return - ptr: e43de2d8, value: 1
3. after return - ptr: e43de2d8, value: 0
1. inside - ptr: e43de2d8, value: 2
4. after return - ptr: e43de2d8, value: 2
Значение напечатанного указателя такое же (то есть адрес тот же), но значение структурного элемента очень странное. В частности, повторите значение по одному и тому же адресу, результат будет другим. Конкретные причины здесь связаны со структурой стека вызовов программы, но мы хотим объяснить здесь:
После возврата метода его локальные переменные исчезают.Хотя адрес памяти все еще там, лучше не использовать этот адрес памяти снова!Если вы обращаетесь к адресу исчезнувшей автоматической переменной, это может быть серьезной ошибкой, потому что значение по соответствующему адресу могло быть изменено другим кодом!- Этот тип проблемы часто упоминается какuse after free.
Если вы генерируете указатель на структуру внутри метода C, вы можете использоватьmalloc
:
A *ptr = (A *)malloc(sizeof(A));
Затем можно безопасно вернуть этот указатель.
Напротив, обработка в Golang намного проще, и эта часть памяти не будет восстановлена:
func init(value int) *A {
return &A{Value: 1}
}
Итак, этот код go безопасен.
арифметика указателя
Во многих программах golang хоть и используются указатели, но они не выполняют операции сложения и вычитания над указателями, что сильно отличается от программ на C. Официальный вводный обучающий инструмент Golang (go tour) даже сказать, что Go не поддерживает арифметику указателей. Хотя на самом деле это не так, я, кажется, не видел арифметики указателей в общих программах go (ну, я знаю, что вы хотите писать необычные программы).
Но на самом деле Go может пройтиunsafe.Pointer
преобразовать указатель вuintptr
Тип чисел для реализации арифметики указателя. Обратите внимание,uintptr
является целочисленным типом, а не типом указателя.
Например:
uintptr(unsafe.Pointer(&p)) + 1
только что получил&p
положение следующего байта. Однако, согласно подсказке «Go Programming Language», нам лучше напрямую преобразовать этот рассчитанный адрес памяти в тип указателя:
unsafe.Pointer(uintptr(unsafe.Pointer(&p) + 1))
Поскольку в go есть механизм сборки мусора, если определенный сборщик мусора перемещает адрес памяти целевого значения, значение указателя, хранящееся как целое число, становится недопустимым значением.
В то же время следует также отметить, что +1 к указателю в go действительно просто указывает на следующий байт, в то время как в C+ 1
или++
Учитывая длину типа данных, он будет автоматически указывать на следующий байт после конца текущего значения (или, возможно, на начало следующего значения). Если вы хотите добиться того же эффекта в go, вы можете использоватьunsafe.Sizeof
метод:
unsafe.Pointer(uintptr(unsafe.Pointer(&p) + unsafe.Sizeof(p)))
Наконец, еще одна распространенная операция с указателями — это преобразование типов указателей. Это также может быть достигнуто с помощью пакета unsafe:
var a int64 = 1
(*int8)(unsafe.Pointer(&a))
Если вы не сталкивались с необходимостью конвертировать типы указателей, вы можете посмотретьЭтот проект (Инструмент сканирования портов), в котором код построения заголовка IP-протокола использует преобразование типа указателя.
-
A variable is a piece of storage containing a value. -- Donovan, Alan A. A.. The Go Programming Language (Addison-Wesley Professional Computing Series) (p. 32). Pearson Education. Kindle Edition. ↩
-
Если не выполняется базовая программа, такая как перемещение GC (сборка мусора на основе перемещения адреса памяти, например, алгоритм копирования для восстановления памяти), адрес памяти переменной не изменяется.↩
-
Each local variable in a function comes into existence only when the function is called, and disappears when the function is exited. This is why such variables are usually known as automatic variables, following terminology in other languages. -- Kernighan, Brian W.. C Programming Language (p. 31). Pearson Education. Kindle Edition. ↩
-
Конкретный процесс и принцип исчезновения еще предстоит исследовать, возможно, это сбор мусора.↩