Class and memory use

Watching the video

he allocates a class

type
NodeOb = class
Txt: String;
End;

in his procedure
Var
Node: NodeOb;
Begin
and then he points to this class

is this correct the Var statement - The complier is effectively allocating memory to the NodeOb class when the procedure is activated - but where is the NodeOb class when the procedure is finished and the memory deactivated. Is the data in the string safe like this?
or is it in the wild west in memory some were?

When I make an object I write a custom class with my object data and under that I create my final class to secure that allocated memory for the objects data so it is safe without the final class the custom classā€™s data is interfered with by the complier in I guess the stack.

Iā€™m asking is this example video best practice and what is beast practice?

I have come to the understanding with the class statement
What is happening in the complier?

" The complier is effectively allocating memory to the NodeOb class when the procedure is activated "

Not quite.

The compiler is allocating memory to the note variable, which is effectively a pointer to NodeOb.
When you execute

note := NodeOb.Create

you are allocating memory for an object (instance of a class) on the heap and assigning the note variable its address.

ā€œIā€™m asking is this example video best practice and what is beast practice?ā€

Yes, this is how itā€™s done. Not sure about ā€œbest practiceā€ but without class factory implementation, this is pretty much the only way to do it unless youā€™re going to employ global variables.
NB: This allocated memory is now yours to manage, you will need to delete the NodeOb object when you delete the node.

ā€œWhen I make an object I write a custom class with my object data and under that I create my final class to secure that allocated memory for the objects data so it is safe without the final class the custom classā€™s data is interfered with by the complier in I guess the stack.ā€

I cannot picture what you mean here without a code example, however hopefully the above answered your question.

The way I would probably prefer to do things is the the OnCreateNodeClass and pass an class inherited from TTreeNode. eg.

type
  TNodeObTreeNode = class(TTreeNode)
  public
     Txt: string;
  end;

procedure TForm1.TreeView1CreateNodeClass(Sender: TCustomTreeView; var NodeClass: TTreeNodeClass);
begin
  NodeClass := TNodeObTreeNode;
end;

procedure TForm3.Button1Click(Sender: TObject);
var
  node : TNodeObTreeNode;
begin
  node := TreeView1.Items.Add(nil, 'Some String') as TNodeObTreeNode;
  node.Txt := 'txt';
end;

Any nodes in the Treeview will actually be of type TNodeObTreeNode. To access these you will need to cast them to a TNodeObTreeNode type to access them (like shown it Button1Click).

I prefer doing things this way because all the memory management is automatically handled by the TreeView.

I have always used

TCustomObject = class
Private
data ā€¦

end;

TFinal class
methods

end;
to protect my object data

The video is not protecting object data is it?
or why does all of Delphi always use custom class before final class

I understand
Var
note := NodeOb;
Is only a pointer that is destroyed with the procedure

So what space is used to hold the string that is in NodeOb;

If I allocate a global block of data, example Arr: Array [0ā€¦88] of string;
or a dynamic array - and even go further and make an array of a record for data groups

I could organize a string pointer or record pointer into each array and expand the array as I need it.

Then I know where my data is!!!

I know I can use a memory stream to hold data how ever I like
but if I want to allocate purposely used space global data with object data that may be of different sizes to each allocation as like under a class how do you do it?

@Geoff just note that if the treeview control is reparented the window handle is recreated and all tree nodes get destroyed and created again and you lose the data saved with your custom node class (they will become default TNodeObTreeNode objects).

Reparenting can be triggered in a bunch of cases: user changes Windows themes, installs an updated video driver, when undocking/docking controls, when reparenting the parent form (e.g. using LMD Docking components), any time that you reparent the control programmatically, or sometimes randomly (we couldnā€™t track this down but could be during automatic Windows updates w/o restart).

To work around that you can do something like subclass TTreeView and override DestroyWnd and CreateWnd to save and load the node object properties.

Thanks for your comment but is a little too far from the question I have asked

Hi Lex,
I reckon it would help to read up on some of the fundamentals of Delphi memory allocation. How local variables work, the difference between a class and an object, where object memory is stored, how to track and free objects, etc. Thereā€™s a lot to it, and there are more advanced topics like using object containers/lists, and reference counted interfaces but the basics are a good start. Iā€™ll try to explain a little here.

There are three pieces of memory here.

(1) When the procedure is called a few bytes of memory are automatically allocated on the stack for var Node: NodeOb, even before you have used it. The compiler knows how much to allocate on the stack for each of the local variables in the var section. Node is essentially a pointer (memory address value) of where a NodeOb object will be stored (once it is created). In a 64-bit app the compiler will allocate 8 bytes (64 bits) for Node. Note that the initial value of Node when entering the function will be undefined (some random value that cannot be used). When the procedure exits the memory on the stack that was reserved for the local variables is freed up for reuse elsewhere. The compiler generates the code to ensure that the stack clean up is always called regardless of how the procedure exits.

NodeOb is just a data type, in this case a class (that descends from TTreeNode, which descends from TPersistent which descends from TObject) and optionally adds some data fields (in this case a string named Txt) and methods (procedures/functions - none in this case). By itself there is no memory associated with a class (well technically there is a fixed one-time set of data allocated in the exe which will be loaded into memory when the process is started to store some stuff related to the class like runtime type information, constant field values, all the executable code in the methods, a table of pointers to the methods, debug info, etcā€¦ even if you havenā€™t created an instance of NodeOb unless you donā€™t use it at all in your app then the compiler may be smart enough to remove the details about NodeOb from the exe altogether - I am ignoring this memory in this explanation).

(2) When an instance (object) of the NodeOb class is created by calling NodeOb.Create the compiler generates code to allocate enough memory to store the data associated with the NodeOb class (not the code). This memory is allocated on the heap (not the stack). The compiler knows how much data is needed in the class and all base classes (combined). The base TTreeNode needs a bunch of memory to store all of its and its parent class data and your NodeOb needs a little bit more data for the Txt string. If you create two instances of NodeOb then a second block of memory is allocated on the heap to store the data for the second instance.

So far we have:

var Node: NodeOb;
Node := NodeObj.Create

(i) 8 bytes are allocated on the stack to store the memory address in Node
(ii) A block of memory is allocated on the heap for an instance of NodeObj (e.g. 100 bytes)
(iii) The memory address (8 bytes) of that block is assigned to Node (set the value where it is stored in the stack)

(3) Now for the Txt string. Strings are a bit special because they donā€™t have a fixed length or memory size. Other types like Integer or Double have a fixed size. String variables are basically a pointer (memory address) to the memory that holds some ā€œhiddenā€ information about the string (unicode info, length and a reference count), and the contents of the string plus a null terminator character (e.g. 30 bytes, 12 for internal use, 18 for some a short string value and a null terminator). So Txt is essentially an 8 byte pointer to a separate block of memory with the auto managed string info plus the bytes needed to store the contents which depends on the length of the string. When the string address is first allocated (on the stack for a local variable or heap for a field within an object), it is always pre-initialised to nil (marked as unused) and no memory is allocated to store the string details or contents. When you assign or reassign a value to the string the compiler adds a bunch of magic code to your app to automatically allocate or reallocate the memory, and to guarantee to free the memory when the string is no longer needed. Itā€™s a bit of an advanced topic but the string memory can be shared by multiple string variables if the contents are identical. The hidden reference count is used under the hood to manage that.

So putting this all together, assuming that a value has been assigned to Node.Txt:

(i) Node is an 8-byte memory address on the stack
(ii) Node points to a 100 byte block of memory on the heap that stores the data for a NodeOb object
(iii) Inside the NodeOb object memory block is an 8-byte memory address for Txt
(iv) Txt points to a separate 30 byte block of memory that stores the internal details and the contents of the string

When your code says Node.Txt := 'abc' this translates (simplistic view) to:

(i) Get the memory address of a NodeObj instance from Node
(ii) Add the memory offset within the object for the Txt field
(iii) Get the memory address of the string from that location
(iv) Set the hidden length of the string in this separate memory block to 3 and copy characters ā€˜abcā€™ (and null terminator) to the contents in this block

I had a flick through the video to find the code. Itā€™s a reasonable approach to adding your own data to the nodes in the TreeView. Unfortunately, at least in that video but may be fixed in a later video, the NodeOb object that is created is never freed and the application has a memory leak. This is because the TreeView does not manage the objects for you. Using Items.AddObject (or assigning to a TTreeNode.Data property) just stores the pointer to the object that you created so that you can read back that pointer later to access the object. You need to track the lifetime of the object and free it yourself. A simple way to do that is using the generic TObjectList class which can free the instances for you, something like this:

uses Generics.Collections;
TForm
private
  NodeObjects: TObjectList<NodeOb>;
...
end;

FormCreate:
  NodeObjects := TObjectList<NodeOb>.Create;
FormDestroy:
  NodeObjects.Free;

When adding a node:

var
  Node: NodeOb;
begin
  Node := NodeOb.Create;
  NodeObjects.Add(Node);  // Keep track of the instances to free them
  TreeView1.AddObject(...);
  Node.Txt := ...
  ...
end;

If you delete a tree node then you should also remove the node object from the object list just to keep your list clean and w/o stale objects: NodeObjects.Remove(Node).

I hope that helps.

1 Like

I have made full objects from the bottom up because I know that works!!!
I believe the truth needs to be found and if the string is not safe/stable data the video needs pulling down because its miss leading to developers.
to me a standard TCustomObject followed with a TFinalObject is not hard to build add a few properties to the final code with data and wa la - its safe dependable rather than spending lots of time developing a final product and latter finding a major issue of loosing string data and this continues as a problem developer after developer!!! Helps need to help and not to create problems!!!

I recently went away from Lazarus because I could only hold more than 30 characters in a string contained in a competed object - very standard object. I believe Delphi is dependable and that is why Iā€™m hear ok - so may be I have been burned from non dependable stuff as it distroys the love of coding for me

I am not sure what you mean by full object and bottom up and using TCustomObject and TFinalObject. The problem with the video is not in how the class is defined or object created but in how the objects are tracked and freed. That would be the same issue with the objects created from your classes. See if his later videos add that.

In any case I think that there is still a gap in understanding how to define classes and manage the lifetime of objects, so some introduction or refresher tutorials/videos would likely help you with code design. Itā€™s good to see you reaching out on these forums. Keep at it.

TCustomObject = class
Private
data ā€¦

end;

TFinal class
methods
Create
destroy
end;

so the data of the first object is hidden by final object - I totally agree clarity is needed and so I have posted this issue to discuss it

How exactly are TFinal and TCustomObject related?

its obvious one is a descendant of the other