Указатели, типы данных и память
 
Автор rdc

От переводчика: статья была написана для устаревшей версии freebasic. В итоге статью и пример пришлось изменить, поскольку арифметика указателей в старой версии freebasic была несколько иной.

Если вы читали статью введение в указатели вы знаете, что указатели содержат адреса расположения в памяти. Можно манипулировать данными в этих местах памяти, используя оператор *. Использование указателей с данными элемента не является проблемой, но что делать, если необходимо хранить несколько элементов данных вместе и манипулировать ими с помощью одного указателя? Это выглядит немного сложнее, если вы не понимаете, каким образом данные хранятся в памяти.

Место памяти в компьютере — 1 байт длиной. Этого достаточно, чтобы разместить один символ ANSI (в отличие от символов Юникода, которые имеют два байта. Мы не будем обсуждать символы Юникода в этой статье.) Однако все типы данных имеют разную длину в байтах. Вот простая программа, которая отображает длину каждого типа данных в байтах.

Dim a As Byte
Dim b As Short
Dim c As Integer
Dim d As LongInt
Dim au As UByte
Dim bu As UShort
Dim cu As UInteger
Dim du As ULongInt
Dim e As Single
Dim f As Double
Dim g As Integer Ptr
Dim h As Byte Ptr
Dim s1 As String * 10 'строка с фиксированной длиной
Dim s2 As String      'строка с переменной длиной
Dim s3 As ZString Ptr 'zstring


s1 = "Hello World!"
s2 = "Hello World from FreeBasic!"
s3 = Allocate( Len( s2 ) + 1 )
*s3 = s2

Print "Byte: ";Len(a)
Print "Short: ";Len(b)
Print "Integer: ";Len(c)
Print "Longint: ";Len(d)
Print "UByte: ";Len(au)
Print "UShort: ";Len(bu)
Print "UInteger: ";Len(cu)
Print "ULongint: ";Len(du)
Print "Single: ";Len(e)
Print "Double: ";Len(f)
Print "Integer Pointer: ";Len(g)
Print "Byte Pointer: ";Len(h)
Print "Fixed String: ";Len(s1)
Print "Variable String: ";Len(s2)
Print "ZString: ";Len(*s3)

Deallocate s3

Sleep



Вывод:

Byte:  1
Short:  2
Integer:  4
LongInt:  8
UByte:  1
UShort:  2
UInteger:  4
ULongInt:  8
Single:  4
Double:  8
Integer Pointer:  4
Byte Pointer:  4
Fixed String:  10
Variable String:  27
ZString:  27



Обратите внимание, что длина указателя всегда равна 4 байтам (так же, как у integer), независимо от данных, на которые этот указатель указывает. Это потому, что он содержит адрес памяти , а не сами данные.

Глядя на длину различных типов данных, вы можете увидеть, что если вам нужно выделить (Allocate) достаточно места для 10 Integer, вам потребуется 40 байт памяти. Каждый integer занимает 4 байта. Поэтому вопрос стоит в том: как сделать доступ к каждому integer из буфера памяти? Ответ прост: использовать арифметику указателей. Взгляните на следующую программу.

Dim a As Integer
Dim aptr As Integer Ptr

'Выделение достаточного места для 2 переменных integer
aptr = Allocate(Len(a) * 2)
'Загрузка нашего первого integer
*aptr = 1
Print "Int #1: ";*aptr
'Перемещение указателя на следующий integer
'aptr + 1
*(aptr + 1) = 2
Print "Int #2: ";*(aptr + 1)

Deallocate aptr
Sleep
End



В этой программе мы создаем две переменные, Integer и Integer Pointer. Aptr будет указывать на наш буфер памяти, который будет содержать два integer. Функция Allocate требует размер буфера, поэтому мы умножаем размер integer на 2, чтобы зарезервировать 8 байт памяти (каждый integer займет 4 байта).

После процесса распределения, Aptr содержит адрес первого байта нашего буфера памяти. Для хранения первого числа все просто: используем оператор * к Aptr и значение 1. Чтобы напечатать значение, мы снова просто используем оператор * к Aptr.

Теперь вопрос: каким образом компилятор знает, что значение 1 должно быть помещено в 4 байта и не в 1 или 2 байта? Это потому, что мы размер Aptr указывается как integer ptr. Компилятор знает, что Integer занимает 4 байта и поэтому загружает данные в четыре байта памяти. Вот почему когда мы выводим значение, мы получаем 1, а не какие либо странные числа.

Чтобы загрузить второе значение в наш буфер, мы используем:

*(aptr + 1) = 2



Это может выглядеть немного странно на первый взгляд. Aptr указывает на первый байт в нашем буфере памяти. Integer содержит 4 байта, поэтому чтобы попасть в следующую позицию байта Integer, мы должны добавить 1 к Aptr. Почему прибавляется 1 , а не 4 как в некоторых других языках? Потому что указатель показывает на integer (Integer ptr), то есть при прибавлении к адресу 1 , будет прибавляться integer (4 байта) . Если бы у нас был указатель Short ptr , то при Aptr+1 прибавлялось бы 2 байта, а если бы у нас был указатель Byte ptr , то при Aptr+1 прибавлялся бы 1 байт.  Нам нужны скобки вокруг операции добавления, потому что оператор * имеет более высокий приоритет, чем +. Скобки гарантируют, что сначала выполнится операция сложения, и только потом будет использоваться оператор *.

Обратите внимание, что мы отдельно не увеличиваем адрес, а берем косвенное смещение относительно начала буфера. Делается это для того , чтобы когда настанет время освободить память нашего буфера с помощью Deallocate , чтобы у нас не произошла ошибка доступа к памяти. Если возникает необходимость непосредственно увеличивать указатель, то создайте временную переменную-указатель, которой присвойте начальный адрес и эту переменную можете увеличивать. В итоге наш начальный адрес буфера будет сохранен и мы сможем его освободить когда потребуется или заново обратиться к начальному адресу.

Буферы памяти и указатели являются мощным способом для хранения и управления данными в памяти. Необходимо позаботиться о том, чтобы обращение к данным было правильным в зависимости от типа данных, хранящегося в буфере.