Brandon
07-26-2013, 08:50 AM
Welcome to my first Lape-Tutorial to help everyone since most of the new Simba scripts will be using it anyway..
I don't know that much so far so I'll just help out with what I know and hope that everyone understands it and can help others out.
Default Parameters and Function overloading
There isn't much to say here but here goes. Imagine you wanted to write a function that can have a default parameter. In other words, a parameter that the user does not HAVE to specify but they could if they wanted. You have two options.
The first option is to overload the function as follows so that it has two separate signatures:
Overloads:
Procedure Meh(a, b: Integer);
begin
writeln('Function Meh with 2 parameters a, b. C does not exist so assume 0.');
end;
Procedure Meh(a, b, c: Integer); overload;
begin
writeln('Function Meh with 3 parameters: a, b, c. User must specify C.');
end;
Now if you notice, these two functions have the same name and both return nothing (void). However, they have a different signature. By that, I mean they have different parameters. Thus they are different functions. In order for the Lape interpreter to accept two functions or procedures with the same name, you must add the "overload" keyword at the end of the declaration of the function doing the overloading. It is safe to add overload for both functions but it's a waste of time. The first function is considered the base function and any function after it with the same name but different signature is called an overload function.
To use the above functions, the user can simply do:
begin
Meh(1, 2); //Calls the first function.
Meh(1, 2, 3); //Calls the second function.
end.
Overloading can also be done with different types and parameters:
Procedure Meh(a, b: Integer);
begin
writeln('Function Meh with 2 parameters a, b. C does not exist so assume 0.');
end;
Procedure Meh(a, b, c: Integer); overload;
begin
writeln('Function Meh with 3 parameters: a, b, c. User must specify C.');
end;
Procedure Meh(Area: TBox); overload;
begin
writeln('Function With TBox as parameter instead');
end;
Procedure Meh(a, b: Integer; c: TBox); overload;
begin
writeln('Two int paramters and 1 tbox.');
end;
Default Parameters:
There are times when function overloading is more than enough, but there's no need to have two functions just to emit the last parameter. In such times, we can use something called "Default Parameters". Default parameters are different from function overloading in that you get to specify the parameter being used as default. In the previous example, we saw that the user has to enter a value for parameter C or else it'd be emitted by calling the first function which doesn't have C.
With default parameters, you can have the best of both worlds and still specify what value C should be if the user enters nothing.
Take the following for example:
procedure Foo(a, b: Integer; C: Integer = 3);
begin
writeln(C);
end;
We see that Foo is a procedure that takes 3 parameters. The first two aka "a" and "b" must be entered by the user no matter what. Parameter C is special in that it is a default parameter. It is "optional". The user can either enter a value or just only enter a and b. If the user enters a value, that value is used instead of the value we put for C. If the user enters nothing, it uses the value we entered.
Example:
procedure Foo(a, b: Integer; C: Integer = 3);
begin
writeln(C);
end;
begin
Foo(1, 2); //This will print 3 because 3 is the default value and we only entered a value for a and b.
Foo(1, 2, 9); //This will print 9 because we entered a third value and it "overrides" the default value.
end.
You can also have multiple default parameters:
procedure Foo(a, b: Integer; C: Integer = 2; D: Integer = 3);
begin
writeln(C);
writeln(D);
end;
begin
Foo(1, 2); //Prints 2 and 3.
Foo(1, 2, 4); //Prints 4 and 3.
Foo(1, 2, 4, 9); //Prints 4 and 9. Notice that you cannot enter A, B, D. It always goes in order of A, B, C, D.
end.
Or all parameters optional:
procedure Foo(a, b, c: Integer = 2);
begin
writeln(c);
end;
begin
Foo();
Foo(1);
Foo(1, 2);
Foo(1, 2, 3);
end.
As you can see above. If the user enters nothing, it uses the value the developer/scripter set as default. However, if the user enters a value, it will use the user's value instead. As you can see for yourselves, only one function is created and that the last parameter has priority of being optional over all other parameters before it. In other words, parameters before it has to be entered before you can enter the last one. You cannot skip inputting parameters as shown previously.
Downsides and Abuse:
There comes a time when users abuse and misuse default parameters in combination with function overloads. Take the following for example:
procedure Foo(a, b: Integer; c: Integer = 2);
begin
writeln(c);
end;
procedure Foo(a, b, c: Integer); overload;
begin
writeln(c);
end;
begin
Foo(1, 2); //Calls first Foo.
Foo(1, 2, 3); //ERROR! Which one should it call?
end.
As shown above, the first function always has priority over all others because it is known as the "base" function. Thus the first call to foo calls the first foo function and prints 2. However, the second call to Foo produces an error. But WHY?! Here's the thing. The reason the first call succeeded is because it matched the signature the user specified in the function call.
Thus it never had to look for an overload. However, if it finds two functions with the same name and signature, it will get confused on which one the user is talking about. For example, the First function has 3 parameters with the last being optional. The second function has 3 parameters with none being optional.
However, the first function already covers the second one as well. Both have three of the same type of parameters and both have the same name, thus both are the same function. The error you now get is:
Exception in Script: Don't know which overloaded method to call with params (Int32, Int32, Int32) at line 12, column 3
Be careful when using optional parameters and overloading of a particular function. Other than that, use it to your heart's content.
Type Extension:
Remember in Pascal script when you had to write a function to do a certain task for you. For example, to Insert a string into another string, you'd have to call a function to do that and clog up your scripts? Well type-extension allows you to use a type as though it were a class.
Take for example this pascal script example:
procedure InsertIntoString(var StrToModify: String; StringToInsert: String);
begin
StrToModify := StrToModify + StringToInsert;
end;
var
B: String;
begin
InsertIntoString(B, 'Hello World');
end.
And this Lape Example:
Procedure String.Insert(StrToInsert: String);
Begin
Self := Self + StrToInsert;
End;
var
B: String;
begin
B.Insert('Hello World');
writeln(B);
end.
Notice anything cool? Pretty neat huh? Well you can do this for quite a lot of different types. You can do it for records, existing simba types like string, integer, arrays, etc.. Imagine doing Array.erase(0); and it'll erase the first index in the array.. OR
Array.find(1); and it searches the whole array to find a value and return the index.
Such a thing can be done as follows:
function TIntegerArray.find(ValueToFind: Integer): Integer;
var
I: Integer;
begin
For I := 0 To High(Self) do
if (Self[I] = ValueToFind) then
begin
Result := I;
Exit;
end;
Result := -1;
end;
var
TIA: TIntegerArray;
begin
TIA := [1, 2, 7, 4, 5];
writeln(TIA.Find(7));
end.
As you can see, the above will search the whole TIntegerArray to find the value 7 and then prints the index at which it was found (2). Type extensions introduces object oriented programming into scripting! :D
Variable Initialization:
In most languages, we can initialize variables upon declaration. This way, we never forget what value they start with. Also, we avoid having to depend on the compiler initializing them to some default value.
For example, most users would say that the default value for a pointer is null or nil or 0. This is NOT ALWAYS true. Sometimes, they default value of an integer or pointer is some random value or random location in memory. Thus users always give them a value before use so as to prevent problems later down the line.
In Pascal-Script, we do the following:
var
I: Integer; //Sets default value of 0 in Simba.
Str: String; //Sets default value of '' in Simba.
begin
I := 2; //Assignment within the body of the function. Overwrites the default value. Thus a value has been set twice.
Str := 'Hello World'; //Assignment to Str within the body of the function.
writeln(I);
writeln(Str);
end.
Lape introduces variable initialization as shown in the following example:
var
I: Integer = 2; //Integer.
Str: String = 'Hello World'; //String.
TIA: TIntegerArray = [1, 2, 3, 4]; //Dynamic Array.
TIA2: array[0..4] of integer = [1, 2, 3, 4, 5]; //Constant Array.
begin
writeln(I); //Prints 2.
writeln(Str); //Prints Hello World.
writeln(TIA); //Prints 1, 2, 3, 4.
writeln(TIA2); //Prints 1, 2, 3, 4, 5.
end.
As you can see above, the pascal version is longer and does assignment twice. However, the Lape version does an assignment to I only once (at least I think so due to this being the nature of C++ class initialization). I also have a firm belief that it only assigns once since it assigns at compile time. However, there is no need to do an assignment within the body of the function and the variable can be used immediately.
There is one thing to note about variable initialization. Only constant expressions are allowed. For example, you cannot do:
var
I: Integer = BitmapFromString(512, 512, '');
begin
writeln(I);
end.
It simply will not compile because BitmapFromString is a function call and variable initialization is done at compile time whereas BitmapFromString is a run-time function. Lape also does not evaluate or have constexpr functions as C++ does and thus this is not possible.
Records and Inheritance:
Imagine you have an object in the real world such as a "Vehicle". Now we all know there can be many types of vehicles. Examples: Cars, Trucks, Motorcycles, Bikes, etc.. They are all a form of transportation and they all share some characteristics in common.
For example, all of the above listed vehicles may shared "wheels" because they all have wheels and they all have a "name". They may or may not have "doors".
These things listed in quotes are all known as properties. Wheels, Names, Doors are all properties of a vehicle.
To represent such a hierarchy in any programming language, we introduce something known as inheritance. All sub-classes known better as "children" share the same properties as their parent category "vehicle".
Such a hierarchy may be represented as follows:
type Vehicle = record
Name: String;
Wheels: Integer;
Doors: Integer;
Seats: Integer;
end;
type Car = record(Vehicle)
Automatic: Boolean;
MaxSpeed: Integer;
end;
type Truck = record(Vehicle)
OffRoad: Boolean;
FourWheelDrive: Boolean;
MaxSpeed: Integer;
end;
var
V: Vehicle = ['Vehicle', 4, 2, 4];
C: Car = ['Car', 4, 2, 4, true, 700];
T: Truck = ['Truck', 4, 2, 4, true, true, 400];
begin
writeln(V);
writeln(C);
writeln(T);
end;
When you print the above, it prints:
{NAME = Vehicle, WHEELS = 4, DOORS = 2, SEATS = 4}
{NAME = Car, WHEELS = 4, DOORS = 2, SEATS = 4, AUTOMATIC = True, MAXSPEED = 700}
{NAME = Truck, WHEELS = 4, DOORS = 2, SEATS = 4, OFFROAD = True, FOURWHEELDRIVE = True, MAXSPEED = 400}
As you can see, Vehicle is the parent class. It is at the top of the hierarchy. The Car and Truck as "Types Of" vehicles.
Relationships:
To be more clear, we call these relationships. Vehicle is the parent and Car and Truck are the children. Brother and Sister where the Vehicle is Mother.
In programming terms, we call this relationship the "Is-A" relationship.
Take the following for example:
type Vehicle = record
Name: String; //Vehicle "Has-A" name.
Wheels: Integer; //Vehicle "Has" wheels.
Doors: Integer; //Vehicle "Has" doors.
Seats: Integer; //Vehicle "Has" Seats.
end;
type Car = record(Vehicle) //Car "Is-A" vehicle. Thus a car has everything vehicle has + more.
Automatic: Boolean; //Car "Has" automatic or manual mode.
MaxSpeed: Integer; //Car "Has" max speed.
end;
type Truck = record(Vehicle) //Truck "Is-A" vehicle. Thus a truck has everything a vehicle has + more.
OffRoad: Boolean; //Truck "Has" offroad type.
FourWheelDrive: Boolean; //Truck "Has" four-wheel drive.
MaxSpeed: Integer; //Truck "Has" Max Speed.
end;
var
V: Vehicle = ['Vehicle', 4, 2, 4]; //Initialize Vehicle if we wanted to.
C: Car = ['Car', 4, 2, 4, true, 700]; //When initializing Car, we must initialize all its parent properties too.
T: Truck = ['Truck', 4, 2, 4, true, true, 400]; //Initializing truck is the same as initializing car. All parent properties too.
begin
writeln(V);
writeln(C);
writeln(T);
end;
As you can see, a truck and a car "Is-A" vehicle but they both "Has/Has-A" their properties.
A truck and a car "Is" both vehicle but a car is NOT a truck. They only share their parent properties!
Those are the only two types of relationships when it comes to inheritance. As explained in the comments, since a car has everything a vehicle has, you must thus initialize all the parent properties of the car structure when declaring it.
It can also be written like:
var
V: Vehicle;
C: Car;
T: Truck;
begin
//Assign to each 'member' of the vehicle structure. Vehicle structure is know as the 'Base' class.
V.Name := 'Vehicle';
V.Doors := 4;
V.Seats := 2;
V.Wheels := 4;
//Assign to each 'member' of the car structure. Car structure is known as the 'Derived' class.
C.Name := 'Car'; //member is inherited from the vehicle parent structure.
C.Doors := 2; //member is inherited from the vehicle parent structure.
C.Seats := 4; //member is inherited from the vehicle parent structure.
C.Wheels := 4; //member is inherited from the vehicle parent structure.
C.Automatic := true;
C.MaxSpeed := 700;
//Assign to each 'member' of the truck structure using short-hand assignment. Truck structure is known as the 'Derived' class.
T := ['Truck', 4, 2, 4, true, true, 400];
writeln(V);
writeln(C);
writeln(T);
end;
Pointers, Addresses, References, Bytes:
References:
How many of you remember using the "var" keyword in Pascal-Script to modify a variable passed to a function?
Under the hood, the var keyword really says "Pass-By-Reference" this variable such that the function can modify it.
Example:
Procedure ModifyInt(var I: Integer); //Pass I by reference..
begin
I := 10; //Modifies the value of I so that I is now 10. Anything that happens to I, affects the parameter I.
end;
Procedure FailModifyInt(I: Integer); //Pass I by value..
Begin
I := 15; //Only modifies a copy of I. Thus changes to the parameter I never happen outside this function.
End;
var
I: Integer = 5;
begin
writeln(I); //prints 5.
ModifyInt(I); //Changes value of I.
writeln(I); //Prints 10.
FailModifyInt(I); //Failure..
writeln(I); //Still Prints 10.
end.
As you can see, I was passed by reference to the first function and thus it was modified. However, the second function passes it by value and only modifies a copy of I. So how does this var keyword really operate.
Var is a keyword that says, pass the address of this variable to the function such that any modifications the function does, affects the data at that address. Since variable I is at that address, any changes at that address, changes the value of I.
Here's the thing though, a REFERENCE can NEVER be NULL. In other words, a reference must be a variable!
Thus the following is invalid code:
Procedure ModifyInt(var I: Integer); //Pass I by reference..
begin
I := 10; //Modifies the value of I so that I is now 10. Anything that happens to I, affects the parameter I.
end;
var
I: Integer = 5;
begin
ModifyInt(0); //ERROR! 0 is not a variable! The address of "0" does not exist.
ModifyInt(I); //Give the function the address of variable I. Aka the location of variable I in memory.
end.
Now we know that var is used to declare variables and to pass variables by reference to a function, let us check out the next operator.
Address-Of:
The Address-Of operator aka the @ sign, gets the address of a variable or member of a structure or even the address of a function. So far, in pascal-script, we've used it to only get the address of functions so that we can a function to another function such as shown below:
Procedure Caller(Callee: Procedure(Number: Integer));
begin
Callee(416); //Call the Callee function.
end;
Procedure CallMeMaybe(Number: Integer);
Begin
writeln('Calling Carlie Rae-Jepsen: ' + ToStr(Number));
End;
begin
Caller(@CallMeMaybe); //Pass the address of "CallMeMaybe" to "Caller"
end.
However, this is not the only use of the address-of operator (@). You can also grab the address of a variable as shown below:
var
I: Integer;
begin
I := 1502525235;
writeln(@I); //Prints: "I"::0x7D58700 (1502525235)
end.
As you can see above, "I" was located at the memory address represented in hexadecimal-->(0x7D58700). The value at that address is the value of I-->(1502525235). Thus, if we modify what is at that address, we will change the value of I. To do this, read on to the next section.
Pointers to Types and Dereferencing:
To create a pointer to a variable, we first need to get the address of the variable. We then assign the address of that variable to some pointer. What is a pointer you ask? A pointer is a variable which holds the address/memory location of another variable, structure or type. Do you see the irony there? Since a pointer is a variable as well, we can have pointers to pointers and pointers to pointers to pointers and so on..
However, we'll leave that for another time and move on. To declare a pointer to a type, we use the following syntax:
type PInt = ^Integer; //we declare a type call PInt. It is a pointer to an integer.
var
I, J: Integer;
Ptr: PInt; //A variable called Ptr which is a pointer to an int.
Ptr2: ^Integer; //A variable called Ptr2 which is a pointer to an int as well.
begin
Ptr := @I; //Ptr now holds the address of I.
Ptr2 := @J; //Ptr2 now holds the address of J.
writeln(Ptr); //prints: "I"::0x7F601C0 (0).
writeln(@I); //prints the same as above :D
writeln(Ptr2); //prints: "J"::0x7F601E0 (0).
writeln(@J); //prints the same as above.
end.
As you can see above, the pointers can only hold the address of some variable, function, or structure. We use the address-of operator to get such information. Now you may notice that we have used the ^ operator followed by some data-type to declare a pointer of such type.
Example:
^Byte; //Pointer to a byte.
^Integer; //Pointer to an int.
^Single; //Pointer to a floating point type. Aka same as ^Extended.
^Vehicle; //Pointer to a vehicle structure.
^Car; //Pointer to a car structure.
So on and so forth.
Now how do we get the value of what is being pointed at? In other words, how can we get the value stored at the address the pointer is holding? Well, the weird thing is.. It's the same operator. Take a look at the following:
var
I: Integer = 150;
J: Integer = 200;
Ptr: ^Integer; //^ followed by Type. Thus pointer to integer.
begin
Ptr := @I; //Ptr now holds the address of I.
J := Ptr^; //Pointer followed by ^.
writeln(J); //Prints the value of I. Prints 150 instead of 250.
end.
Notices that to get the value the pointer points at, we do "Pointer^" and to declare a pointer we do "^Pointer". Yes. I know it looks similar but keep this in mind all the time! "Pointer^" means to dereference and get the value of it. Whereas "^Pointer" means to declare a pointer of some type.
Here is a little exercise. Try and figure out how the following works without running it. Then run it and see if it you got it right.
Test:
type
PInt = ^Integer;
PPInt = ^PInt;
var
I, J: Integer;
Ptr: PInt;
Ptr2: PPint;
begin
I := 700;
J := 5000;
Ptr := @I;
Ptr^ := 300;
Ptr := @J;
Ptr2 := @Ptr;
Ptr2^^ := 100;
writeln(I); //What is the value of I?
writeln(J); //What is the value of J?
end;
Pointers, Arrays, Bytes:
Now if you are comfortable with pointers and modifying variables using them, it is time to learn how a pointer and an array work together to achieve specific tasks such as modifying an array or swapping the values of an array, etc..
The first task is to understand how pointers operate and their different variable sizes. A pointer on 32-bit operating systems are always 4 bytes in size and on 64-bit systems, they are 8-bytes in size. A byte is the smallest usable amount of memory that a computer has.
4 bits = 1 byte.
1 byte = 1 char.
2 bytes = 1 word/short-int.
4 bytes = 1 integer/double-word/dword.
8 bytes = 1 int64/long-int/qword.
Yes that's right. Every integer is 4-bytes long and every character is 1-byte.
Now why should we know this? Well, when doing pointer arithmetic such as iterating a pointer (moving it to the next memory address), we need to know how much to increase it to get to the next desirable cell.
For example, see the following:
var
Ptr: ^Char;
I: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(Ptr^); //What do you think it prints?
Inc(Ptr);
end;
end;
See we just assigned a pointer of "char" type to an array of integers. An integer is 4 bytes in size but a char is 1. Thus we are only printing the first byte in each integer. The above will print some random garbage to your screen. It isn't random at all because I know how the assembly registers works but for now, lets just leave it as "random garbage". In order to print all the values in the array, we must use a 4-byte pointer or increase the pointer by 4 every loop.
So lets try creating a pointer to an int then as follows:
type PInt = ^Integer;
var
Ptr: PInt;
I, J: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(Ptr^); //What do you think it prints?
Inc(Ptr);
end;
end;
Now what does it print? It of course, prints the values of the array. But why? Well the reason it now prints the value of the array is because Inc(Ptr) increases the Pointer by 1. Since the pointer is an int pointer, it has really increased by 4 bytes.
Thus the above is the same as using a char pointer like so:
type PInt = ^Integer;
var
Ptr: PChar;
I, J: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(PInt(Ptr)^); //Cast our Char Pointer to an Int Pointer and dereference it. Aka get the value it points at.
Ptr := Ptr + 4; //Increase the char pointer by 4 bytes.
end;
end;
Size-Of with Pointers; RawPointers:
Now let us say that you aren't sure what architecture the user is using.. In other words, you're not sure whether they are using x32 or x64 bit operating systems. Then how do you know whether to increase the pointer by 4 bytes or by 8? Well, introducing the sizeof operator/macro:
type PInt = ^Integer;
var
Ptr: PChar;
I, J: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA; //Can point to an integer-array.
For I := 0 To High(TIA) do
begin
writeln(PInt(Ptr)^);
Ptr := Ptr + sizeof(Integer); //Increase the pointer by the sizeof(integer) in bytes.. An Integer is 4 bytes so sizeof(Integer) is 4.
end;
end;
As you can see above, using sizeof(DataType) is much safer to figure out how much to increase the pointer by. The above will still print all the values in the array and at the same time introduces pointer-safety. If you ever forget what the size of any data-type is, you can do:
writeln(sizeof(DataTypeHere)); //prints the size of a data-type in bytes.
Now what is a raw-pointer? A raw pointer is a pointer that has no size or type. Wtf? How? Well, in C++, this is known as a void pointer. It is not 4 byte, not 2 bytes, not 8, nothing.. To get the value of a raw pointer, you need to cast it to a pointer with a specific type. To declare a raw-pointer, we do the following:
var
I: Integer;
Ptr: Pointer; //Declares a raw pointer ;)
TIA: array[0..5] of integer;
TCA: array[0..5] of char;
begin
TIA := [1, 2, 3, 4, 5, 6];
TCA := ['a', 'b', 'c', 'd', 'e', 'f'];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(Integer(Ptr^)); //Cast our raw-pointer to an integer pointer and get the value at that address and print it.
Ptr := Ptr + sizeof(Integer); //we are iterating an integer array. So we increase our raw pointer by sizeof(Integer);
end;
Ptr := @TCA; //Notice that the raw-pointer can point to any data-type.
For I := 0 To High(TCA) do
begin
writeln(char(Ptr^)); //Cast our raw-pointer to an char pointer and get the value at that address and print it.
Ptr := Ptr + sizeof(char); //we are iterating a char array. So we increase our raw pointer by sizeof(char);
end;
end;
Pointers to structures and arithmetic:
Pointers to structures utilize the sizeof operator/macro and pointer-data size quite a lot. See the following on how to print the entire struct below:
type Struct = record
X: Integer;
Y: Word;
Z: Integer;
end;
var
Ptr: Pointer;
S: Struct = [100, 500, 255];
begin
Ptr := @S;
writeln(Integer(Ptr^)); //Print S.X;
Ptr := Ptr + sizeof(Integer); //Move onto the next data member.
writeln(Word(Ptr^)); //Print S.Y;
Ptr := Ptr + sizeof(Word); //Move onto the next data member.
writeln(Integer(Ptr^)); //Print S.Z;
Ptr := Ptr + sizeof(Integer); //Not needed because we already printed the last member.
end;
Notice that we have declared a pointer to the Struct. The pointer holds the address of S. To print the values of S, we move the pointer to the memory location of each data member and then print it. But this is quite tedious isn't it?
Well here is another pointer arithmetic trick. Not only can we add pointers and sizes, we can also subtract, multiply, and divide them. We can also use member offsets. For example:
Let us assume that S is located at 0. If that is true, then S.X is located at 0 bytes from S's address. S.Y is located at 4 from S's address and S.Z is located at 6 bytes from S's address.
type Struct = record
X: Integer; //0 bytes away from Struct.
Y: Word; //4 bytes away from Struct; aka sizeof(X).
Z: Integer; //6 bytes away from Struct; aka sizeof(X) + sizeof(Y).
end;
var
Ptr: Pointer;
S: Struct = [100, 500, 255];
begin
Ptr := @S;
writeln(Integer(Ptr^)); //Prints S.X
writeln(Word((Ptr + 4)^)); //Prints S.Y
writeln(Integer((Ptr + 6)^)); //Prints S.Z
end;
Another example of pointer arithmetic is the following:
type Struct = record
X: Integer;
Y: Word;
Z: Integer;
end;
var
Ptr: Pointer;
SA: array[0..3] of Struct;
I: Integer;
begin
SA[0] := [100, 200, 300];
SA[1] := [400, 500, 600];
SA[2] := [700, 800, 900];
SA[3] := [1000, 1100, 1200];
Ptr := @SA;
For I := 0 To High(SA) do
begin
writeln('SA[' + ToStr(I) + ']: ');
writeln(Integer(Ptr^));
writeln(Word((Ptr + 4)^));
writeln(Integer((Ptr + 6)^));
writeln('');
Ptr := Ptr + sizeof(Struct); //increment by the sizeof(Struct) aka.. sizeof(Integer) + sizeof(Word) + sizeof(Integer).. aka 10.
end;
end;
Now lets say you wanted to print only every 3rd value in the following, you'd do:
var
TIA: Array[0..23] of integer;
I: Integer;
Ptr: Pointer;
begin
TIA := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
Ptr := @TIA;
while (Ptr < @TIA[23]) do
begin
writeln(Integer(Ptr^));
Ptr := Ptr + sizeof(Integer) * 3;
end;
end;
Simulating Polymorphism:
Polymorphism is the concept of having values of different types use a single data-type/interface for handling. Confused? Well remember the vehicle, car, truck example previously shown? If we wanted to create an array of all types of vehicles, how could we do such a thing?
The following code will give you errors:
type Vehicle = record
Name: String;
Wheels: Integer;
Doors: Integer;
Seats: Integer;
end;
type Car = record(Vehicle)
Automatic: Boolean;
MaxSpeed: Integer;
end;
type Truck = record(Vehicle)
OffRoad: Boolean;
FourWheelDrive: Boolean;
MaxSpeed: Integer;
end;
type
PVehicle = ^Vehicle;
var
V: Vehicle = ['Vehicle', 4, 2, 4];
C: Car = ['Car', 4, 2, 4, true, 700];
T: Truck = ['Truck', 4, 2, 4, true, true, 400];
VA: Array[0..2] of Vehicle;
begin
VA[0] := C; //Even though a car is a vehicle, we cannot assign it to a vehicle array. This would introduce a problem known as "slicing". Lape stops this by not allowing you to do it.
VA[1] := T; //Same thing here.
end;
So how can we create an array that can hold ALL types of vehicles? The answer is polymorphism through pointers. Since Car and Truck are both "Vehicles" and are children of the vehicle structure, we can create a pointer to both a car and a truck and store it in an array of vehicle:
The following code is 100% valid and polymorphic (closest to at least):
type Vehicle = record
Name: String;
Wheels: Integer;
Doors: Integer;
Seats: Integer;
end;
type Car = record(Vehicle)
Automatic: Boolean;
MaxSpeed: Integer;
end;
type Truck = record(Vehicle)
OffRoad: Boolean;
FourWheelDrive: Boolean;
MaxSpeed: Integer;
end;
type
PVehicle = ^Vehicle;
var
V: Vehicle = ['Vehicle', 4, 2, 4];
C: Car = ['Car', 4, 2, 4, true, 700];
T: Truck = ['Truck', 4, 2, 4, true, true, 400];
VA: Array[0..2] of PVehicle;
PV, PV2: PVehicle;
begin
VA[0] := @C;
VA[1] := @T;
PV := @C;
PV2 := @T;
writeln(PV^);
writeln(PV2^);
writeln(VA[0]^);
writeln(VA[1]^);
end;
As you can see, we just created an array of Pointers to Vehicle type. Since all pointers are the same size (4 bytes on x32 and 8 bytes on x64), we can simply store the address of each vehicle type into any pointer. This is known as a polymorphic array. An array of parent holding pointers to children. You can also see that a pointer to the vehicle type can hold the address of a child type. Thus we can now print out that array and it'll print all the children. Useful for holding an array of "anything" or array of children basically. This can be done with all data-types. Polymorphism has many uses and is a very powerful tool! Unfortunately Lape does not have "typeof" to tell which type a pointer is.. But that is ok we can still use polymorphism simulation and many pointer features.
Cool huh?
I don't know that much so far so I'll just help out with what I know and hope that everyone understands it and can help others out.
Default Parameters and Function overloading
There isn't much to say here but here goes. Imagine you wanted to write a function that can have a default parameter. In other words, a parameter that the user does not HAVE to specify but they could if they wanted. You have two options.
The first option is to overload the function as follows so that it has two separate signatures:
Overloads:
Procedure Meh(a, b: Integer);
begin
writeln('Function Meh with 2 parameters a, b. C does not exist so assume 0.');
end;
Procedure Meh(a, b, c: Integer); overload;
begin
writeln('Function Meh with 3 parameters: a, b, c. User must specify C.');
end;
Now if you notice, these two functions have the same name and both return nothing (void). However, they have a different signature. By that, I mean they have different parameters. Thus they are different functions. In order for the Lape interpreter to accept two functions or procedures with the same name, you must add the "overload" keyword at the end of the declaration of the function doing the overloading. It is safe to add overload for both functions but it's a waste of time. The first function is considered the base function and any function after it with the same name but different signature is called an overload function.
To use the above functions, the user can simply do:
begin
Meh(1, 2); //Calls the first function.
Meh(1, 2, 3); //Calls the second function.
end.
Overloading can also be done with different types and parameters:
Procedure Meh(a, b: Integer);
begin
writeln('Function Meh with 2 parameters a, b. C does not exist so assume 0.');
end;
Procedure Meh(a, b, c: Integer); overload;
begin
writeln('Function Meh with 3 parameters: a, b, c. User must specify C.');
end;
Procedure Meh(Area: TBox); overload;
begin
writeln('Function With TBox as parameter instead');
end;
Procedure Meh(a, b: Integer; c: TBox); overload;
begin
writeln('Two int paramters and 1 tbox.');
end;
Default Parameters:
There are times when function overloading is more than enough, but there's no need to have two functions just to emit the last parameter. In such times, we can use something called "Default Parameters". Default parameters are different from function overloading in that you get to specify the parameter being used as default. In the previous example, we saw that the user has to enter a value for parameter C or else it'd be emitted by calling the first function which doesn't have C.
With default parameters, you can have the best of both worlds and still specify what value C should be if the user enters nothing.
Take the following for example:
procedure Foo(a, b: Integer; C: Integer = 3);
begin
writeln(C);
end;
We see that Foo is a procedure that takes 3 parameters. The first two aka "a" and "b" must be entered by the user no matter what. Parameter C is special in that it is a default parameter. It is "optional". The user can either enter a value or just only enter a and b. If the user enters a value, that value is used instead of the value we put for C. If the user enters nothing, it uses the value we entered.
Example:
procedure Foo(a, b: Integer; C: Integer = 3);
begin
writeln(C);
end;
begin
Foo(1, 2); //This will print 3 because 3 is the default value and we only entered a value for a and b.
Foo(1, 2, 9); //This will print 9 because we entered a third value and it "overrides" the default value.
end.
You can also have multiple default parameters:
procedure Foo(a, b: Integer; C: Integer = 2; D: Integer = 3);
begin
writeln(C);
writeln(D);
end;
begin
Foo(1, 2); //Prints 2 and 3.
Foo(1, 2, 4); //Prints 4 and 3.
Foo(1, 2, 4, 9); //Prints 4 and 9. Notice that you cannot enter A, B, D. It always goes in order of A, B, C, D.
end.
Or all parameters optional:
procedure Foo(a, b, c: Integer = 2);
begin
writeln(c);
end;
begin
Foo();
Foo(1);
Foo(1, 2);
Foo(1, 2, 3);
end.
As you can see above. If the user enters nothing, it uses the value the developer/scripter set as default. However, if the user enters a value, it will use the user's value instead. As you can see for yourselves, only one function is created and that the last parameter has priority of being optional over all other parameters before it. In other words, parameters before it has to be entered before you can enter the last one. You cannot skip inputting parameters as shown previously.
Downsides and Abuse:
There comes a time when users abuse and misuse default parameters in combination with function overloads. Take the following for example:
procedure Foo(a, b: Integer; c: Integer = 2);
begin
writeln(c);
end;
procedure Foo(a, b, c: Integer); overload;
begin
writeln(c);
end;
begin
Foo(1, 2); //Calls first Foo.
Foo(1, 2, 3); //ERROR! Which one should it call?
end.
As shown above, the first function always has priority over all others because it is known as the "base" function. Thus the first call to foo calls the first foo function and prints 2. However, the second call to Foo produces an error. But WHY?! Here's the thing. The reason the first call succeeded is because it matched the signature the user specified in the function call.
Thus it never had to look for an overload. However, if it finds two functions with the same name and signature, it will get confused on which one the user is talking about. For example, the First function has 3 parameters with the last being optional. The second function has 3 parameters with none being optional.
However, the first function already covers the second one as well. Both have three of the same type of parameters and both have the same name, thus both are the same function. The error you now get is:
Exception in Script: Don't know which overloaded method to call with params (Int32, Int32, Int32) at line 12, column 3
Be careful when using optional parameters and overloading of a particular function. Other than that, use it to your heart's content.
Type Extension:
Remember in Pascal script when you had to write a function to do a certain task for you. For example, to Insert a string into another string, you'd have to call a function to do that and clog up your scripts? Well type-extension allows you to use a type as though it were a class.
Take for example this pascal script example:
procedure InsertIntoString(var StrToModify: String; StringToInsert: String);
begin
StrToModify := StrToModify + StringToInsert;
end;
var
B: String;
begin
InsertIntoString(B, 'Hello World');
end.
And this Lape Example:
Procedure String.Insert(StrToInsert: String);
Begin
Self := Self + StrToInsert;
End;
var
B: String;
begin
B.Insert('Hello World');
writeln(B);
end.
Notice anything cool? Pretty neat huh? Well you can do this for quite a lot of different types. You can do it for records, existing simba types like string, integer, arrays, etc.. Imagine doing Array.erase(0); and it'll erase the first index in the array.. OR
Array.find(1); and it searches the whole array to find a value and return the index.
Such a thing can be done as follows:
function TIntegerArray.find(ValueToFind: Integer): Integer;
var
I: Integer;
begin
For I := 0 To High(Self) do
if (Self[I] = ValueToFind) then
begin
Result := I;
Exit;
end;
Result := -1;
end;
var
TIA: TIntegerArray;
begin
TIA := [1, 2, 7, 4, 5];
writeln(TIA.Find(7));
end.
As you can see, the above will search the whole TIntegerArray to find the value 7 and then prints the index at which it was found (2). Type extensions introduces object oriented programming into scripting! :D
Variable Initialization:
In most languages, we can initialize variables upon declaration. This way, we never forget what value they start with. Also, we avoid having to depend on the compiler initializing them to some default value.
For example, most users would say that the default value for a pointer is null or nil or 0. This is NOT ALWAYS true. Sometimes, they default value of an integer or pointer is some random value or random location in memory. Thus users always give them a value before use so as to prevent problems later down the line.
In Pascal-Script, we do the following:
var
I: Integer; //Sets default value of 0 in Simba.
Str: String; //Sets default value of '' in Simba.
begin
I := 2; //Assignment within the body of the function. Overwrites the default value. Thus a value has been set twice.
Str := 'Hello World'; //Assignment to Str within the body of the function.
writeln(I);
writeln(Str);
end.
Lape introduces variable initialization as shown in the following example:
var
I: Integer = 2; //Integer.
Str: String = 'Hello World'; //String.
TIA: TIntegerArray = [1, 2, 3, 4]; //Dynamic Array.
TIA2: array[0..4] of integer = [1, 2, 3, 4, 5]; //Constant Array.
begin
writeln(I); //Prints 2.
writeln(Str); //Prints Hello World.
writeln(TIA); //Prints 1, 2, 3, 4.
writeln(TIA2); //Prints 1, 2, 3, 4, 5.
end.
As you can see above, the pascal version is longer and does assignment twice. However, the Lape version does an assignment to I only once (at least I think so due to this being the nature of C++ class initialization). I also have a firm belief that it only assigns once since it assigns at compile time. However, there is no need to do an assignment within the body of the function and the variable can be used immediately.
There is one thing to note about variable initialization. Only constant expressions are allowed. For example, you cannot do:
var
I: Integer = BitmapFromString(512, 512, '');
begin
writeln(I);
end.
It simply will not compile because BitmapFromString is a function call and variable initialization is done at compile time whereas BitmapFromString is a run-time function. Lape also does not evaluate or have constexpr functions as C++ does and thus this is not possible.
Records and Inheritance:
Imagine you have an object in the real world such as a "Vehicle". Now we all know there can be many types of vehicles. Examples: Cars, Trucks, Motorcycles, Bikes, etc.. They are all a form of transportation and they all share some characteristics in common.
For example, all of the above listed vehicles may shared "wheels" because they all have wheels and they all have a "name". They may or may not have "doors".
These things listed in quotes are all known as properties. Wheels, Names, Doors are all properties of a vehicle.
To represent such a hierarchy in any programming language, we introduce something known as inheritance. All sub-classes known better as "children" share the same properties as their parent category "vehicle".
Such a hierarchy may be represented as follows:
type Vehicle = record
Name: String;
Wheels: Integer;
Doors: Integer;
Seats: Integer;
end;
type Car = record(Vehicle)
Automatic: Boolean;
MaxSpeed: Integer;
end;
type Truck = record(Vehicle)
OffRoad: Boolean;
FourWheelDrive: Boolean;
MaxSpeed: Integer;
end;
var
V: Vehicle = ['Vehicle', 4, 2, 4];
C: Car = ['Car', 4, 2, 4, true, 700];
T: Truck = ['Truck', 4, 2, 4, true, true, 400];
begin
writeln(V);
writeln(C);
writeln(T);
end;
When you print the above, it prints:
{NAME = Vehicle, WHEELS = 4, DOORS = 2, SEATS = 4}
{NAME = Car, WHEELS = 4, DOORS = 2, SEATS = 4, AUTOMATIC = True, MAXSPEED = 700}
{NAME = Truck, WHEELS = 4, DOORS = 2, SEATS = 4, OFFROAD = True, FOURWHEELDRIVE = True, MAXSPEED = 400}
As you can see, Vehicle is the parent class. It is at the top of the hierarchy. The Car and Truck as "Types Of" vehicles.
Relationships:
To be more clear, we call these relationships. Vehicle is the parent and Car and Truck are the children. Brother and Sister where the Vehicle is Mother.
In programming terms, we call this relationship the "Is-A" relationship.
Take the following for example:
type Vehicle = record
Name: String; //Vehicle "Has-A" name.
Wheels: Integer; //Vehicle "Has" wheels.
Doors: Integer; //Vehicle "Has" doors.
Seats: Integer; //Vehicle "Has" Seats.
end;
type Car = record(Vehicle) //Car "Is-A" vehicle. Thus a car has everything vehicle has + more.
Automatic: Boolean; //Car "Has" automatic or manual mode.
MaxSpeed: Integer; //Car "Has" max speed.
end;
type Truck = record(Vehicle) //Truck "Is-A" vehicle. Thus a truck has everything a vehicle has + more.
OffRoad: Boolean; //Truck "Has" offroad type.
FourWheelDrive: Boolean; //Truck "Has" four-wheel drive.
MaxSpeed: Integer; //Truck "Has" Max Speed.
end;
var
V: Vehicle = ['Vehicle', 4, 2, 4]; //Initialize Vehicle if we wanted to.
C: Car = ['Car', 4, 2, 4, true, 700]; //When initializing Car, we must initialize all its parent properties too.
T: Truck = ['Truck', 4, 2, 4, true, true, 400]; //Initializing truck is the same as initializing car. All parent properties too.
begin
writeln(V);
writeln(C);
writeln(T);
end;
As you can see, a truck and a car "Is-A" vehicle but they both "Has/Has-A" their properties.
A truck and a car "Is" both vehicle but a car is NOT a truck. They only share their parent properties!
Those are the only two types of relationships when it comes to inheritance. As explained in the comments, since a car has everything a vehicle has, you must thus initialize all the parent properties of the car structure when declaring it.
It can also be written like:
var
V: Vehicle;
C: Car;
T: Truck;
begin
//Assign to each 'member' of the vehicle structure. Vehicle structure is know as the 'Base' class.
V.Name := 'Vehicle';
V.Doors := 4;
V.Seats := 2;
V.Wheels := 4;
//Assign to each 'member' of the car structure. Car structure is known as the 'Derived' class.
C.Name := 'Car'; //member is inherited from the vehicle parent structure.
C.Doors := 2; //member is inherited from the vehicle parent structure.
C.Seats := 4; //member is inherited from the vehicle parent structure.
C.Wheels := 4; //member is inherited from the vehicle parent structure.
C.Automatic := true;
C.MaxSpeed := 700;
//Assign to each 'member' of the truck structure using short-hand assignment. Truck structure is known as the 'Derived' class.
T := ['Truck', 4, 2, 4, true, true, 400];
writeln(V);
writeln(C);
writeln(T);
end;
Pointers, Addresses, References, Bytes:
References:
How many of you remember using the "var" keyword in Pascal-Script to modify a variable passed to a function?
Under the hood, the var keyword really says "Pass-By-Reference" this variable such that the function can modify it.
Example:
Procedure ModifyInt(var I: Integer); //Pass I by reference..
begin
I := 10; //Modifies the value of I so that I is now 10. Anything that happens to I, affects the parameter I.
end;
Procedure FailModifyInt(I: Integer); //Pass I by value..
Begin
I := 15; //Only modifies a copy of I. Thus changes to the parameter I never happen outside this function.
End;
var
I: Integer = 5;
begin
writeln(I); //prints 5.
ModifyInt(I); //Changes value of I.
writeln(I); //Prints 10.
FailModifyInt(I); //Failure..
writeln(I); //Still Prints 10.
end.
As you can see, I was passed by reference to the first function and thus it was modified. However, the second function passes it by value and only modifies a copy of I. So how does this var keyword really operate.
Var is a keyword that says, pass the address of this variable to the function such that any modifications the function does, affects the data at that address. Since variable I is at that address, any changes at that address, changes the value of I.
Here's the thing though, a REFERENCE can NEVER be NULL. In other words, a reference must be a variable!
Thus the following is invalid code:
Procedure ModifyInt(var I: Integer); //Pass I by reference..
begin
I := 10; //Modifies the value of I so that I is now 10. Anything that happens to I, affects the parameter I.
end;
var
I: Integer = 5;
begin
ModifyInt(0); //ERROR! 0 is not a variable! The address of "0" does not exist.
ModifyInt(I); //Give the function the address of variable I. Aka the location of variable I in memory.
end.
Now we know that var is used to declare variables and to pass variables by reference to a function, let us check out the next operator.
Address-Of:
The Address-Of operator aka the @ sign, gets the address of a variable or member of a structure or even the address of a function. So far, in pascal-script, we've used it to only get the address of functions so that we can a function to another function such as shown below:
Procedure Caller(Callee: Procedure(Number: Integer));
begin
Callee(416); //Call the Callee function.
end;
Procedure CallMeMaybe(Number: Integer);
Begin
writeln('Calling Carlie Rae-Jepsen: ' + ToStr(Number));
End;
begin
Caller(@CallMeMaybe); //Pass the address of "CallMeMaybe" to "Caller"
end.
However, this is not the only use of the address-of operator (@). You can also grab the address of a variable as shown below:
var
I: Integer;
begin
I := 1502525235;
writeln(@I); //Prints: "I"::0x7D58700 (1502525235)
end.
As you can see above, "I" was located at the memory address represented in hexadecimal-->(0x7D58700). The value at that address is the value of I-->(1502525235). Thus, if we modify what is at that address, we will change the value of I. To do this, read on to the next section.
Pointers to Types and Dereferencing:
To create a pointer to a variable, we first need to get the address of the variable. We then assign the address of that variable to some pointer. What is a pointer you ask? A pointer is a variable which holds the address/memory location of another variable, structure or type. Do you see the irony there? Since a pointer is a variable as well, we can have pointers to pointers and pointers to pointers to pointers and so on..
However, we'll leave that for another time and move on. To declare a pointer to a type, we use the following syntax:
type PInt = ^Integer; //we declare a type call PInt. It is a pointer to an integer.
var
I, J: Integer;
Ptr: PInt; //A variable called Ptr which is a pointer to an int.
Ptr2: ^Integer; //A variable called Ptr2 which is a pointer to an int as well.
begin
Ptr := @I; //Ptr now holds the address of I.
Ptr2 := @J; //Ptr2 now holds the address of J.
writeln(Ptr); //prints: "I"::0x7F601C0 (0).
writeln(@I); //prints the same as above :D
writeln(Ptr2); //prints: "J"::0x7F601E0 (0).
writeln(@J); //prints the same as above.
end.
As you can see above, the pointers can only hold the address of some variable, function, or structure. We use the address-of operator to get such information. Now you may notice that we have used the ^ operator followed by some data-type to declare a pointer of such type.
Example:
^Byte; //Pointer to a byte.
^Integer; //Pointer to an int.
^Single; //Pointer to a floating point type. Aka same as ^Extended.
^Vehicle; //Pointer to a vehicle structure.
^Car; //Pointer to a car structure.
So on and so forth.
Now how do we get the value of what is being pointed at? In other words, how can we get the value stored at the address the pointer is holding? Well, the weird thing is.. It's the same operator. Take a look at the following:
var
I: Integer = 150;
J: Integer = 200;
Ptr: ^Integer; //^ followed by Type. Thus pointer to integer.
begin
Ptr := @I; //Ptr now holds the address of I.
J := Ptr^; //Pointer followed by ^.
writeln(J); //Prints the value of I. Prints 150 instead of 250.
end.
Notices that to get the value the pointer points at, we do "Pointer^" and to declare a pointer we do "^Pointer". Yes. I know it looks similar but keep this in mind all the time! "Pointer^" means to dereference and get the value of it. Whereas "^Pointer" means to declare a pointer of some type.
Here is a little exercise. Try and figure out how the following works without running it. Then run it and see if it you got it right.
Test:
type
PInt = ^Integer;
PPInt = ^PInt;
var
I, J: Integer;
Ptr: PInt;
Ptr2: PPint;
begin
I := 700;
J := 5000;
Ptr := @I;
Ptr^ := 300;
Ptr := @J;
Ptr2 := @Ptr;
Ptr2^^ := 100;
writeln(I); //What is the value of I?
writeln(J); //What is the value of J?
end;
Pointers, Arrays, Bytes:
Now if you are comfortable with pointers and modifying variables using them, it is time to learn how a pointer and an array work together to achieve specific tasks such as modifying an array or swapping the values of an array, etc..
The first task is to understand how pointers operate and their different variable sizes. A pointer on 32-bit operating systems are always 4 bytes in size and on 64-bit systems, they are 8-bytes in size. A byte is the smallest usable amount of memory that a computer has.
4 bits = 1 byte.
1 byte = 1 char.
2 bytes = 1 word/short-int.
4 bytes = 1 integer/double-word/dword.
8 bytes = 1 int64/long-int/qword.
Yes that's right. Every integer is 4-bytes long and every character is 1-byte.
Now why should we know this? Well, when doing pointer arithmetic such as iterating a pointer (moving it to the next memory address), we need to know how much to increase it to get to the next desirable cell.
For example, see the following:
var
Ptr: ^Char;
I: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(Ptr^); //What do you think it prints?
Inc(Ptr);
end;
end;
See we just assigned a pointer of "char" type to an array of integers. An integer is 4 bytes in size but a char is 1. Thus we are only printing the first byte in each integer. The above will print some random garbage to your screen. It isn't random at all because I know how the assembly registers works but for now, lets just leave it as "random garbage". In order to print all the values in the array, we must use a 4-byte pointer or increase the pointer by 4 every loop.
So lets try creating a pointer to an int then as follows:
type PInt = ^Integer;
var
Ptr: PInt;
I, J: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(Ptr^); //What do you think it prints?
Inc(Ptr);
end;
end;
Now what does it print? It of course, prints the values of the array. But why? Well the reason it now prints the value of the array is because Inc(Ptr) increases the Pointer by 1. Since the pointer is an int pointer, it has really increased by 4 bytes.
Thus the above is the same as using a char pointer like so:
type PInt = ^Integer;
var
Ptr: PChar;
I, J: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(PInt(Ptr)^); //Cast our Char Pointer to an Int Pointer and dereference it. Aka get the value it points at.
Ptr := Ptr + 4; //Increase the char pointer by 4 bytes.
end;
end;
Size-Of with Pointers; RawPointers:
Now let us say that you aren't sure what architecture the user is using.. In other words, you're not sure whether they are using x32 or x64 bit operating systems. Then how do you know whether to increase the pointer by 4 bytes or by 8? Well, introducing the sizeof operator/macro:
type PInt = ^Integer;
var
Ptr: PChar;
I, J: Integer;
TIA: array[0..4] of integer;
begin
TIA := [1, 2, 3, 4, 5];
Ptr := @TIA; //Can point to an integer-array.
For I := 0 To High(TIA) do
begin
writeln(PInt(Ptr)^);
Ptr := Ptr + sizeof(Integer); //Increase the pointer by the sizeof(integer) in bytes.. An Integer is 4 bytes so sizeof(Integer) is 4.
end;
end;
As you can see above, using sizeof(DataType) is much safer to figure out how much to increase the pointer by. The above will still print all the values in the array and at the same time introduces pointer-safety. If you ever forget what the size of any data-type is, you can do:
writeln(sizeof(DataTypeHere)); //prints the size of a data-type in bytes.
Now what is a raw-pointer? A raw pointer is a pointer that has no size or type. Wtf? How? Well, in C++, this is known as a void pointer. It is not 4 byte, not 2 bytes, not 8, nothing.. To get the value of a raw pointer, you need to cast it to a pointer with a specific type. To declare a raw-pointer, we do the following:
var
I: Integer;
Ptr: Pointer; //Declares a raw pointer ;)
TIA: array[0..5] of integer;
TCA: array[0..5] of char;
begin
TIA := [1, 2, 3, 4, 5, 6];
TCA := ['a', 'b', 'c', 'd', 'e', 'f'];
Ptr := @TIA;
For I := 0 To High(TIA) do
begin
writeln(Integer(Ptr^)); //Cast our raw-pointer to an integer pointer and get the value at that address and print it.
Ptr := Ptr + sizeof(Integer); //we are iterating an integer array. So we increase our raw pointer by sizeof(Integer);
end;
Ptr := @TCA; //Notice that the raw-pointer can point to any data-type.
For I := 0 To High(TCA) do
begin
writeln(char(Ptr^)); //Cast our raw-pointer to an char pointer and get the value at that address and print it.
Ptr := Ptr + sizeof(char); //we are iterating a char array. So we increase our raw pointer by sizeof(char);
end;
end;
Pointers to structures and arithmetic:
Pointers to structures utilize the sizeof operator/macro and pointer-data size quite a lot. See the following on how to print the entire struct below:
type Struct = record
X: Integer;
Y: Word;
Z: Integer;
end;
var
Ptr: Pointer;
S: Struct = [100, 500, 255];
begin
Ptr := @S;
writeln(Integer(Ptr^)); //Print S.X;
Ptr := Ptr + sizeof(Integer); //Move onto the next data member.
writeln(Word(Ptr^)); //Print S.Y;
Ptr := Ptr + sizeof(Word); //Move onto the next data member.
writeln(Integer(Ptr^)); //Print S.Z;
Ptr := Ptr + sizeof(Integer); //Not needed because we already printed the last member.
end;
Notice that we have declared a pointer to the Struct. The pointer holds the address of S. To print the values of S, we move the pointer to the memory location of each data member and then print it. But this is quite tedious isn't it?
Well here is another pointer arithmetic trick. Not only can we add pointers and sizes, we can also subtract, multiply, and divide them. We can also use member offsets. For example:
Let us assume that S is located at 0. If that is true, then S.X is located at 0 bytes from S's address. S.Y is located at 4 from S's address and S.Z is located at 6 bytes from S's address.
type Struct = record
X: Integer; //0 bytes away from Struct.
Y: Word; //4 bytes away from Struct; aka sizeof(X).
Z: Integer; //6 bytes away from Struct; aka sizeof(X) + sizeof(Y).
end;
var
Ptr: Pointer;
S: Struct = [100, 500, 255];
begin
Ptr := @S;
writeln(Integer(Ptr^)); //Prints S.X
writeln(Word((Ptr + 4)^)); //Prints S.Y
writeln(Integer((Ptr + 6)^)); //Prints S.Z
end;
Another example of pointer arithmetic is the following:
type Struct = record
X: Integer;
Y: Word;
Z: Integer;
end;
var
Ptr: Pointer;
SA: array[0..3] of Struct;
I: Integer;
begin
SA[0] := [100, 200, 300];
SA[1] := [400, 500, 600];
SA[2] := [700, 800, 900];
SA[3] := [1000, 1100, 1200];
Ptr := @SA;
For I := 0 To High(SA) do
begin
writeln('SA[' + ToStr(I) + ']: ');
writeln(Integer(Ptr^));
writeln(Word((Ptr + 4)^));
writeln(Integer((Ptr + 6)^));
writeln('');
Ptr := Ptr + sizeof(Struct); //increment by the sizeof(Struct) aka.. sizeof(Integer) + sizeof(Word) + sizeof(Integer).. aka 10.
end;
end;
Now lets say you wanted to print only every 3rd value in the following, you'd do:
var
TIA: Array[0..23] of integer;
I: Integer;
Ptr: Pointer;
begin
TIA := [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
Ptr := @TIA;
while (Ptr < @TIA[23]) do
begin
writeln(Integer(Ptr^));
Ptr := Ptr + sizeof(Integer) * 3;
end;
end;
Simulating Polymorphism:
Polymorphism is the concept of having values of different types use a single data-type/interface for handling. Confused? Well remember the vehicle, car, truck example previously shown? If we wanted to create an array of all types of vehicles, how could we do such a thing?
The following code will give you errors:
type Vehicle = record
Name: String;
Wheels: Integer;
Doors: Integer;
Seats: Integer;
end;
type Car = record(Vehicle)
Automatic: Boolean;
MaxSpeed: Integer;
end;
type Truck = record(Vehicle)
OffRoad: Boolean;
FourWheelDrive: Boolean;
MaxSpeed: Integer;
end;
type
PVehicle = ^Vehicle;
var
V: Vehicle = ['Vehicle', 4, 2, 4];
C: Car = ['Car', 4, 2, 4, true, 700];
T: Truck = ['Truck', 4, 2, 4, true, true, 400];
VA: Array[0..2] of Vehicle;
begin
VA[0] := C; //Even though a car is a vehicle, we cannot assign it to a vehicle array. This would introduce a problem known as "slicing". Lape stops this by not allowing you to do it.
VA[1] := T; //Same thing here.
end;
So how can we create an array that can hold ALL types of vehicles? The answer is polymorphism through pointers. Since Car and Truck are both "Vehicles" and are children of the vehicle structure, we can create a pointer to both a car and a truck and store it in an array of vehicle:
The following code is 100% valid and polymorphic (closest to at least):
type Vehicle = record
Name: String;
Wheels: Integer;
Doors: Integer;
Seats: Integer;
end;
type Car = record(Vehicle)
Automatic: Boolean;
MaxSpeed: Integer;
end;
type Truck = record(Vehicle)
OffRoad: Boolean;
FourWheelDrive: Boolean;
MaxSpeed: Integer;
end;
type
PVehicle = ^Vehicle;
var
V: Vehicle = ['Vehicle', 4, 2, 4];
C: Car = ['Car', 4, 2, 4, true, 700];
T: Truck = ['Truck', 4, 2, 4, true, true, 400];
VA: Array[0..2] of PVehicle;
PV, PV2: PVehicle;
begin
VA[0] := @C;
VA[1] := @T;
PV := @C;
PV2 := @T;
writeln(PV^);
writeln(PV2^);
writeln(VA[0]^);
writeln(VA[1]^);
end;
As you can see, we just created an array of Pointers to Vehicle type. Since all pointers are the same size (4 bytes on x32 and 8 bytes on x64), we can simply store the address of each vehicle type into any pointer. This is known as a polymorphic array. An array of parent holding pointers to children. You can also see that a pointer to the vehicle type can hold the address of a child type. Thus we can now print out that array and it'll print all the children. Useful for holding an array of "anything" or array of children basically. This can be done with all data-types. Polymorphism has many uses and is a very powerful tool! Unfortunately Lape does not have "typeof" to tell which type a pointer is.. But that is ok we can still use polymorphism simulation and many pointer features.
Cool huh?