I'd be careful with the term garbage collector, bacause not all automated memory management is using garbage collection, e.g. a popular alternative is reference counting. But no matter if it's GC or Ref Counting more generally most modern languages have some form of automated memory management. Including Pascal.
Pascal does have automated memory management for three types (of types): Strings, Arrays and COM Interfaces (not counting managed records as they are not working correctly right now). For example:
procedure WriteAsString(data: Integer);
var
s: String;
begin
s := IntToStr(data); // Creates a new string, includes allocating memory for it
WriteLn(s);
end; // The memory used by s will automatically be freed because strings are managed types
But you are right that for other types, namely classes and typed and untyped pointers this is not the case.
So here is some advice on how to deal with that. Always think in terms of ownership. Everytime you allocate memory there must be an "owner", some piece of code that is responsible for that memory. You then must make sure that that owner always frees the memory. Also important is that you must make sure that whenever the owner "dies" no one else has access to that memory anymore.
The easiest case is when you create a temporary object, e.g. a TStringList to read a config file:
procedure ReadConfigFile(const FileName: String);
var
sl: TStringList;
begin
sl := TStringList.Create;
try
sl.LoadFromFile(FileName);
// Do something with the data in SL
ConfigValue1 := sl.Values['Config1'];
ConfigValue2 := sl.Values['Config2'];
finally
sl.Free;
end;
end;
In this case the owner of that SL is the function ReadConfigFile. It is created by that function, and in the end freed by that function. The try-finally ensures that this will always happen, no matter if you have an early break, exception or whatever in your code.
The important part here is that sl cannot leave this function. after the finally block sl should not be used anymore. This also includes passing SL to some other object, this is fine as long as this other object can only use SL while the function is still active. Example:
function EncodeBase64(data: Array of Byte): String;
var
ss: TStringStream;
enc: TBase64EncodingStream;
b: Byte;
begin
ss := TStringStream.Create;
try
enc := TBase64EncodingStream.Create(ss);
try
for b in data do
enc.WriteByte(b);
enc.Flush;
ss.Seek(0, soFromBeginning);
Result := ss.DataString;
finally
enc.Free;
end;
finally
ss.Free;
end;
end;
Here this function owns two objects, the enc and ss. Again both are ensured to be freed through try-finally. But enc uses ss, so basically what you are doing here is "borrowing" the ss, which is owned by the function to the enc object to be used. It is important to ensure that enc is not using ss after it is freed. The only way to ensure this is to make sure that SS is not freed before enc is.
So here it is important for your owner to make sure that if you "borrow" your memory to another object, that object does not have access to the borrowed memory after it is freed. Most of the time this means either freeing the object that borrows the memory before freeing the borrowed memory, in other cases this may mean to revoke access to said object:
var
GlobalLogger: TStream;
procedure LogString(str: String);
begin
if Assigned(GlobalLogger) then // If logger is registered use it
begin
GlobalLogger.Write(str[1], Length(str));
GlobalLogger.WriteByte(10); // Newline
end
else // otherwise use writeln
WriteLn(str);
end;
procedure FileLoggerTest;
var
fs: TFileStream;
begin
fs := TFileStream.Create('test.log');
try
// Set global logger to "borrow" fs
GlobalLogger := fs;
LogString('Test log file');
finally
// ensure global logger is reset to revoke access before freeing fs
GlobalLogger := nil;
fs.Free;
end;
LogString('Test log WriteLn');
end;
Here you borrow the filestream owned by the function to a global variable, which is used by another function. So when the filestream is freed, to ensure it cannot be used by the global variable anymore, it must be overriden.
Thats for local objects, this is generally quite easy because here the owner is the creating function, and can usually be "solved" with just 2 rules of thumb: 1. Always use try-finally after the creation to free (so as soon as you write a Create, write the try-finally-free afterwads) and 2. make sure that the object is not used outside the lifetime of that function. To do the second (which is easier said than done), just keep in mind who borrows the object and check that the lifetimes of those borrows are shorter than the lifetime of the owner and you are fine.
Now to the more complex situation, when the owner is not the creator, or ownership is transferred. The easiest example of this is a list:
uses
classes,
Generics.Collections;
type
TStringListList = specialize TObjectList<TStringList>;
var
list: TStringListList;
begin
list := TStringListList.Create(True); // The parameter AOwnsObjects is True so now the list owns the object
try
list.Add(TStringList.Create); // creates new object that is owned by list
finally
list.Free; // TObjectList with AOwnsObjects will Free all objects it owns when freed
end;
end.
Here TObjectList is the owner of the StringList, meaning even though it is created by the code block of the program, it does not need to be freed there, because the ownership is transfered to the list via the .Add.
You can also take back ownership of an object from a list:
type
TStringListList = specialize TObjectList<TStringList>;
var
list: TStringListList;
sl: TStringList;
begin
list := TStringListList.Create(True); // The parameter AOwnsObjects is True so now the list owns the object
try
list.Add(TStringList.Create); // creates new object that is owned by list
sl := list.Extract(0); // Now the list does not own the StringList anymore
try
// ...
finally
sl.Free; // Because we taken back ownership, we must free it here
end;
finally
list.Free; // TObjectList with AOwnsObjects will Free all objects it owns when freed
end;
end.
A usual use-case for that is in GUI development, every component has the Owner property. So when you create a Button and set it's owner to be the form it's located on, when the Form is freed, it will also be freed.
So the next important rule of thumb is: always know when some other entity claims ownership of an object. There must always be exactly one owner. When you put your elements into a TObjectList, that list becomes the owner until you take the object away (with Extract). If you put a component on a Form, that form becomes the owner of that component.
It is very important to not have multiple owners. E.g. if you put the same object into two object lists, it will crash:
type
TStringListList = specialize TObjectList<TStringList>;
var
l1 ,l2: TStringListList;
sl: TStringList;
begin
l1 := TStringListList.Create(True);
try
l2 := TStringListList.Create(True);
try
sl := TStringList.Create;
l1.Add(sl); // Transfers ownership to l1
l2.Add(sl); // Transfers ownership to l2
finally
l2.Free; // L2 thinks it owns sl so it frees it
end;
finally
l1.Free; // l1 thinkts it owns sl so it tries to free it
// But sl was already freed by l2 -> error
end;
end.
Following these simple rules will get you through 80%-90% of your programming just fine, just always remember to check that there is always exactly 1 owner, when giving away the object, check if it's a borrow, if so make sure that the borrow does not live longer than the owner, and if it's not a borrow and you transfer ownership, make sure that you don't end up with two owners because you forgott that you already transfered ownership.
Because this post is already getting ungodly long, I will write how to handle the remaining 10%-20% of cases (the "hard" cases) in another post. But this should be fine for most use cases.
PS: One big problem with failing to manage your memory properly is that it may cause bugs that are unpredictable and do not cause crashes:
var
sl1, sl2: TStringList;
begin
sl1 := TStringList.Create;
try
sl1.Text := 'Hello';
finally
sl1.Free;
end;
sl2 := TStringList.Create;
try
sl2.Text := 'World';
WriteLn(sl1.Text); // sl1 is already freed, so this is an error
finally
sl2.Free;
end;
end.
Even though this is a clear use-after-free error, where you use an object after it's lifetime was ended by it's owner (through try-finally), it does not crash or report any error, but rather prints out "World". The reason: sl2 happend to get the same memory allocated that was just freed by sl1.
And this is the really tricky thing about memory management. Errors in memory management may not lead to any easy to spot crashes, but may just cause some weird unexplainable behavior, which makes them much harder to track down.
For this there are tools like Heaptrc or Valgrind, which can help you spot these errors, but 1. they are not foolproof either, and 2. they only show you the errors when you encounter them. Both of those tools are not useful for production code, so if you don't spot the error during testing, and you roll out your software to the users/customers, they may encounter this weird behavior and it's nearly impossible for you to track it down afterwards.
So always be aware, it's trickier than it might seem.