A pointer is a variable that holds the 32-bit (4 byte) address of code or data located elsewhere in memory. It is called a pointer because it literally points to that location. The location pointed to is known as the target of the pointer.
Pointers represent a powerful addition to the BASIC programmer's arsenal. The address is defined at run-time, so your program can reference any memory location as if it were a standard variable. When a pointer is used to access a memory location, it is called "indirect addressing".
Pointers are declared using the DIM statement, and the type of the target must be specified. The keywords PTR and POINTER are synonymous.
DIM i AS INTEGER PTR 'declares i as a pointer to an Integer
or:
DIM i AS INTEGER POINTER
The above example declares i
as an Integer pointer. Before it
can be used, i
must be initialized with an actual address of a variable
(easily done with the VARPTR function;
or STRPTR for
DIM Ptr1 AS BYTE PTR ' declares Ptr1 as a byte pointer
DIM Ptr2 AS BYTE PTR ' declares Ptr2 as a byte pointer
DIM Byte1 AS BYTE ' Declares Byte1 as a byte variable
DIM Byte2 AS BYTE ' Declares Byte2 as a byte variable
Ptr1 = VARPTR(Byte1) ' Ptr1 points to Byte1
@Ptr1 = 36 ' Sets Byte1 to the value 36
Ptr2 = VARPTR(Byte2) ' Ptr2 points to Byte2
@Ptr2 = @Ptr1 + 4 ' Sets Byte2 to 40 (36 + 4)
In summary, when you reference a pointer variable without an at-sign, you are referencing the 32-bit address contained in it. When you precede the name with an at-sign, you are referencing the target data located at the address "pointed to" by the pointer.
By assigning the address of another pointer to a pointer, we can set up another level of indirection. Pointers to pointers are useful when setting up linked lists in memory. You can then access the target by adding a second at-sign in front of the pointer's name:
DIM y AS STRING POINTER
DIM z AS STRING POINTER
DIM TmpStr AS STRING
y = VARPTR(TmpStr) ' y points to TmpStr
z = VARPTR(y) ' z points to y
@y = "A" ' put an "A" in TmpStr
@@z = "B" ' overwrite it with a "B"
Display @y ' display the target value of y
PowerBASIC supports up to 200 levels of indirection. For each level, you add another preceding at-sign to the pointer name. You can only use the (@) prefix with pointer variables.
A pointer with a value of zero (0) is considered a null-pointer by PowerBASIC. Windows will generate a General Protection Fault (GPF) if you attempt to access data at an invalid pointer address. See the section on assembler programming for more information.
The true power of pointers resides in their speed and flexibility. Traditionally, to access memory, a BASIC programmer had to use combinations of PEEK and POKE. This allowed the programmer to address memory as bytes. If the target data took any other form, conversion was necessary. Pointers allow you to address the target data in any fashion you desire, even as a user-defined structure. Moreover, because the setup of calling PEEK and POKE is no longer necessary, access is much faster.
Let's say that we want to scan all the characters in a buffer, replacing all upper case "A"s with lower case "a"s. The code might look something like this:
SUB Lower(zStr AS STRING)
DIM s AS BYTE PTR, ix AS INTEGER
s = STRPTR(zStr) ' Access the dynamic string directly
FOR ix = 1 TO LEN(zStr)
IF @s = 65 THEN @s = 97 ' "A" -> "a"
INCR s
NEXT
END SUB
When using a pointer to a structure, the prefix is placed before the structure name when you wish to access an element of the structure. The structure name by itself refers to its address. This distinction is extremely important when treating structures as a whole. The following example shows two ways of doing a simple bubble sort of an array of User-Defined Types. The first uses conventional BASIC methods, the second uses pointers to illustrate their speed and efficiency.
'-- Example 1 --------------
#COMPILE EXE
#DIM ALL
TYPE NameRec
Last AS STRING * 20 ' Last name
First AS STRING * 20 ' First name
END TYPE
FUNCTION PBMAIN () AS LONG
DIM Rec(1 TO 10) AS NameRec
DIM RP AS NameRec POINTER
DIM ix AS LONG, ij AS LONG
DIM hFile AS DWORD
'-- Put some data in the records --
FOR ix = 1 TO 10
Rec(ix).First = CHOOSE$(ix,"Jacob","Michael","Joshua","Matthew","Ethan", _
"Emily","Emma","Madison","Abigail","Olivia")
Rec(ix).Last = CHOOSE$(ix,"SMITH","JOHNSON","WILLIAMS","JONES","BROWN", _
"DAVIS","MILLER","WILSON","MOORE","TAYLOR")
NEXT ix
'-- Sort UDT array in ascending order using a bubble sort
'-- ARRAY SORT Rec(),FROM 1 TO 20,ASCEND will do this as well
FOR ix = 9 TO 1 STEP -1
FOR ij = 1 TO ix
IF Rec(ij-1).Last > Rec(ij).Last THEN
SWAP Rec(ij-1), rec(ij)
END IF
NEXT ij
NEXT ix
#IF %DEF(%PB_CC32)
FOR ix = 1 TO 10
PRINT TRIM$(Rec(ix).Last)+ ", " +TRIM$(Rec(ix).First)
NEXT ix
PRINT "Press any key to quit ... "
WAITKEY$
#ELSE
DIM msg AS STRING
FOR ix = 1 TO 10
msg = msg + TRIM$(Rec(ix).Last)+ ", " +TRIM$(Rec(ix).First) + $CRLF
MSGBOX msg
NEXT ix
#ENDIF
END FUNCTION
'-- Example 2 --------------
' The difference between example 1 and this example is
' that we're manipulating pointers (4 bytes) instead
' of whole records (40 bytes).
#COMPILE EXE
#DIM ALL
TYPE NameRec
Last AS STRING * 20 ' Last name
First AS STRING * 20 ' First name
END TYPE
FUNCTION PBMAIN () AS LONG
DIM Rec(1 TO 10) AS NameRec
DIM RP AS NameRec POINTER
DIM ix AS LONG, ij AS LONG
DIM hFile AS DWORD
'-- Put some data in the records --
FOR ix = 1 TO 10
Rec(ix).First = CHOOSE$(ix,"Jacob","Michael","Joshua","Matthew","Ethan", _
"Emily","Emma","Madison","Abigail","Olivia")
Rec(ix).Last = CHOOSE$(ix,"SMITH","JOHNSON","WILLIAMS","JONES","BROWN",
_
"DAVIS","MILLER","WILSON","MOORE","TAYLOR")
NEXT ix
'-- Sort UDT array in ascending order using a bubble sort with pointers
'-- note a bubble sort is not recommended for large collections
'-- and note ARRAY SORT Rec(),FROM 1 TO 20,ASCEND will do this as well
'-- so this is only to show pointers to UDT arrays in action!
RP = VARPTR(Rec(1))
FOR ix = 9 TO 1 STEP -1
FOR ij = 1 TO ix
'note pointers to array elements use zero based subscripts in brackets!
IF @RP[ij-1].Last > @RP[ij].Last THEN
SWAP @RP[ij-1], @RP[ij]
END IF
NEXT ij
NEXT ix
#IF %DEF(%PB_CC32)
FOR ix = 1 TO 10
PRINT TRIM$(Rec(ix).Last)+ ", " +TRIM$(Rec(ix).First)
NEXT ix
PRINT "Press any key to quit ... "
WAITKEY$
#ELSE
DIM msg AS STRING
FOR ix = 1 TO 10
msg = msg + TRIM$(Rec(ix).Last)+ ", " +TRIM$(Rec(ix).First) + $CRLF
MSGBOX msg
NEXT ix
#ENDIF
END FUNCTION
If you declare a member of a structure as a pointer, the @ prefix is used with the member name, not the structure name. The previous example could be improved by adding a couple of pointers to the structure to point to the previous and next record, respectively. This lets you allocate memory for a record only when needed, instead of pre-allocating a fixed-size array of records. The modified structure would look something like this:
TYPE NameRec
Last AS STRING * 20 ' Last name
First AS STRING * 20 ' First name
Nxt AS NameRec PTR ' Pointer to next record
Prv AS NameRec PTR ' Pointer to previous record
END TYPE
DIM Rec AS NameRec
The pointer members are then accessed like this:
Rec.@Nxt ' next record
Rec.@Prv ' previous record
Putting the @ prefix in front of the structure name (i.e., @Rec) would cause a compile-time error, as Rec itself is not a pointer.
When calculating the length of the Type, all pointers are internally stored as Double-word (DWORD) variables, so NameRec is 48 bytes long (20 + 20 + 4 + 4). If you need to know the length of a Type, it is easier to let PowerBASIC calculate it for you using the LEN function than doing it yourself:
length = LEN(structure)
See Also
Pointers to nul-terminated and fixed-length strings
Pointers to arrays with dual indexes