9. Указатели, изменение значений и время жизни
Изучите адрес, разыменование, изменение значения с помощью указателя и висячие указатели.
Wave Foundation
Различайте ценность и место хранения.
Целое число 42 и адрес, по которому оно хранится, представляют собой разные значения. Указатель указывает на место хранения. Передача адреса позволяет функции читать или изменять память вызывающего объекта.
В этой главе рассматривается получение адресов, разыменование, изменение исходного значения, арифметика указателей и время жизни. Динамическое размещение рассматривается в следующей главе. Начните с адресов локальных переменных и элементов массива.
Получение адресов и разыменование
fun main() {
var value: i32 = 42;
var address: ptr<i32> = &value;
println("value={}", value);
println("through pointer={}", deref address);
deref address = 99;
println("changed={}", value);
}
Результат выполнения:
value=42
through pointer=42
changed=99
&value получает адрес, deref address считывает или записывает значение этого адреса и так далее. Вместо сохранения 99 в address, 99 было записано в целое число, на которое указывает address. Сама переменная address продолжает указывать на value.
ptr<i32> — это тип указателя для доступа к хранилищу i32. Тип не записывает длину и не обеспечивает автоматическое освобождение.
Изменение самого указателя
fun main() {
var first: i32 = 10;
var second: i32 = 20;
var selected: ptr<i32> = &first;
selected = &second;
deref selected = 25;
println("{} {}", first, second);
}
Результат выполнения:
10 25
selected = &second сохраняет другой адрес в переменной-указателе. Значение first не меняется. Затем, если вы запишете значение как deref, second изменится. Разделение слов «изменение адреса» и «изменение стоимости через адрес» на отдельные предложения уменьшит путаницу.
Заставить функцию изменить оригинал
fun increment(value: ptr<i32>) {
deref value = deref value + 1;
}
fun main() {
var count: i32 = 4;
increment(&count);
increment(&count);
println("{}", count);
}
Результат выполнения:
6
Функция получает адрес счетчика, а не его значение 4. Изменение этого хранилища также меняет счетчик в вызывающей стороне. Для этой функции требуется действительный, доступный для записи адрес i32. Передача null нарушает это требование.
Каждая функция определяет, принимает ли она null. В противном случае вызывающий абонент должен предоставить действительный адрес. Если это так, функция должна включать путь, обрабатывающий null.
Функция для обработки null
fun try_increment(value: ptr<i32>) -> bool {
if (value == null) {
return false;
}
deref value = deref value + 1;
return true;
}
fun main() {
var count: i32 = 7;
if (!try_increment(null)) {
println("no value");
}
if (try_increment(&count)) {
println("count={}", count);
}
}
Результат выполнения:
no value
count=8
Проверка null обрабатывает только отсутствие адреса. Преобразование произвольного числа, отличного от null, в указатель не создает действительную память. Чтение и запись также требуют действительного срока службы, достаточного размера, правильного выравнивания и соответствующих разрешений на доступ.
Адреса массивов и поэлементная арифметика указателей
fun main() {
var values: array<i32, 3> = [10, 20, 30];
var first: ptr<i32> = &values[0];
var second: ptr<i32> = first + 1;
println("{}", deref first);
println("{}", deref second);
println("{}", deref first[2]);
}
Результат выполнения:
10
20
30
Добавление 1 к указателю перемещает его на один элемент целевого типа. Следующий элемент в i32 и следующий элемент в u8 имеют разное количество байтов сдвига. Если вы еще раз умножите first + 1 на размер шрифта и прибавите его, он переместится в нежелательное положение.
Индексирование указателя также должно выполняться в пределах допустимого диапазона. first сам не запоминает длину массива 3, поэтому при передаче диапазона функции он использует форму, которая получает и указатель, и длину.
Передача диапазона чтения в функцию
fun sum(values: ptr<i32>, count: i32) -> i32 {
var total: i32 = 0;
for (var index: i32 = 0; index < count; index += 1) {
total += deref values[index];
}
return total;
}
fun main() {
var values: array<i32, 4> = [2, 4, 6, 8];
println("first two={}", sum(&values[0], 2));
println("all={}", sum(&values[0], 4));
}
Результат выполнения:
first two=6
all=20
Единица измерения count — количество элементов. Эта функция требует, чтобы вызывающая сторона имела читаемый count i32. Передача длины, большей, чем фактический массив, нарушает контракт. Даже если вы используете ту же комбинацию ptr·i64, API, вам следует проверить в документации, указана ли длина в байтах или в элементах.
Продолжительность жизни: Как долго действителен адрес?
Локальные переменные используются в течение всего времени существования вызова и блока. Если вы возвращаете адрес локальной переменной внутри функции, чтобы вызывающая сторона могла прочитать ее позже, возможно, срок действия этого пространства памяти достиг конца.
Вот пример плохого дизайна, который не следует реализовывать:
fun invalid_address() -> ptr<i32> {
var local: i32 = 42;
return &local;
}
Если требуется одно значение, возвращается i32. Если ему необходимо выполнить запись в пространство памяти, предоставленное вызывающим объектом, в качестве входных данных он принимает указатель. Если вам нужно отдельное хранилище для хранения после вызова, выделите его явно и передайте ответственность за его освобождение.
Два указателя, указывающие на одно и то же место хранения.
Копирование указателя создает другое имя, указывающее на тот же адрес. Не дублирует память.
fun main() {
var value: i32 = 1;
var first: ptr<i32> = &value;
var second: ptr<i32> = first;
deref second = 9;
println("{} {}", value, deref first);
}
Результат выполнения:
9 9
Результаты, измененные с помощью second, также можно увидеть с помощью first. Как только исходная память освобождается, оба указателя становятся непригодными для использования. Присвоение null одной переменной-указателю не приводит к автоматическому изменению других копий.
Упражнение: Поменяйте местами два целых числа.
Напишите функцию, которая принимает два адреса i32 и обменивает их значения. Первое значение необходимо сохранить во временной переменной перед его перезаписью. Проверьте, сохраняется ли значение, даже если вы дважды передаете один и тот же адрес.
Полное решение
fun swap(left: ptr<i32>, right: ptr<i32>) {
var saved: i32 = deref left;
deref left = deref right;
deref right = saved;
}
fun main() {
var first: i32 = 3;
var second: i32 = 8;
swap(&first, &second);
println("{} {}", first, second);
swap(&first, &first);
println("{}", first);
}
Результат выполнения:
8 3
8
Эта функция также требует, чтобы оба адреса указывали на допустимое, доступное для записи целочисленное хранилище. Чтобы обработать null, добавьте результат, указывающий успех или неудачу, как в try_increment.
Wave Explicit Memory Type Model
Конструкция указателя Wave основана на Wave Explicit Memory Type Model. Эта модель определяет указатели и массивы как явные типы памяти на уровне языка, а не синтаксические трюки или библиотечные абстракции.
ptr<T> — это тип, указывающий на адрес памяти, в котором хранится значение T, а array<T, N> — это тип памяти фиксированной длины, в котором последовательно хранятся значения N из T. Таким образом, структура указателей и массивов раскрывается так же, как и в аргументах функций, возвращаемых значениях, полях структуры и других типах.
null
var buffer: ptr<u8> = null;
if (buffer == null) {
println("no buffer");
}
null — это значение указателя, которое не указывает на действительный адрес памяти. null может быть присвоен только типу ptr<T> и не может использоваться как целочисленное, логическое значение или значение массива.
Функция распределения или поиска может возвращать null, если она не дает результата. Проверьте наличие null перед разыменованием такого результата. Разыменование указателя null не приводит к доступу к допустимому хранилищу.
преобразование указателя
Если вам нужно изменить адрес или другое представление указателя, используйте as.
var raw: i64 = 0;
var p: ptr<u8> = raw as ptr<u8>;
Используйте преобразования между целыми числами и указателями только на границах низкого уровня и учитывайте ширину адреса целевой платформы и ABI.