You’re ready now to apply what you’ve learned about class definitions, pointers, and constructors and destructors to be able to create new instances in the main computer memory. There are some fundamental differences between C++ and C# that we’re going to again focus on just C++ first and then explore C# in tomorrow’s episode.
You learn what an instance of your class is, where it lives in memory, how small can it actually be, and how to dynamically allocate the memory to create instances and then delete them when you’re done.
Along the way, we’ll explore the stack again. You’ll learn a bit more about global variables and something new called static variables. And you’ll learn when and how to use the heap to store your variables.
I also explain how the C++ runtime interacts with your operating system to manage memory. You can listen to the full episode or read the full transcript below.
The basic structure of your class data is defined by what data members you included in your class definition. You’ll usually find class definitions in header files which get included by your source files. The source files will normally contain the implementation of the class methods. When you put the methods together with the data, you get the fundamental concept of object-oriented programming. But all this is just a definition.
Think of it like a stencil that you can use to paint images on walls. One stencil can be used over and over to create multiple image instances. And the images can even use different colors. The stencil just defines the basic shape and size of the images.
Classes and structs are like these stencils. They define what data and methods make up the class instances. Any particular instance can have its own values for the data that the class defines will exist. For example, a class can declare that it has a data member to hold a color. But you need an actual instance of that class in order to get a specific color. The class says that there will be a color and the instance says what the color will be. You could have hundreds of instances of this class each with a slightly different color. But they all have a color.
What if a class has no data? Is this allowed? For that matter, does a class need methods? It’s possible to define a completely empty class with no methods and no data. The compiler will cheat a bit here and will give you a few methods anyway. Anytime you have no constructor declared, you’ll get a default constructor. You’ll also get a basic copy constructor and a destructor.
So you have this empty class that’s not so empty after all but it still defines no data members. How much memory will be required for an instance of this class? In other words, how much wall space will this stencil use when painted?
There’s a rule in C++ that the smallest size anything can be is one byte. So even an empty class will require a single byte even if nothing will ever be written to that byte.
Notice that I’m not talking about the size of the instructions that make up the class methods. An instance of a class consists of its data. When we get to inheritance, you’ll also learn about virtual functions. If a class has any virtual functions, then its instances will be a little bigger.
The term function is really just another word for a method. At least in C++. Some languages might have different concepts or make some distinction between a method and a function. And some languages may prefer one term or another. I tend to use them interchangeably.
So why can an instance be no smaller than a byte? C++ is such an efficient language, why start wasting bytes now? It’s because when you create an instance, that instance needs to live somewhere in memory and must have a memory address that is unique to just that instance.
It’s like walking into a restaurant that has a maximum occupancy of one. You’ll have the whole restaurant to yourself. You can’t have multiple instances all sitting at the same memory address. And since a single byte is the smallest unit of memory that has its own address, that’s also the smallest an object instance can be.
Okay, on to constructing an instance.
You already know that the constructor is just a method that runs to make sure the data of your instance is in a good state. But where does this data live? Running the constructor is just half of what you need to create an instance. You also need memory. You need memory that will be dedicated for the new instance.
There’s three places where you can get this memory. From the stack. Statically allocated as part of your application. And from the heap.
Before we get too far, let me first say that this whole podcast is based on my experience and understanding. And I’m still learning after 25 years. The examples I provide come from how I’ve taught myself over the years how to program. This understanding has served me very well so far. But it’s not like I’ve written a research paper on the deep internals of memory management. So if I get something wrong or leave some small detail unsaid, then the way I see it is that you should do just fine with this explanation or any of my explanations. And if you someday learn more than me about programming, well, then that’s only to be expected. I say, take what I’m explaining, learn from it, and go do more.
Alright, back to the memory.
The stack is available for you to use when you declare a local variable inside of a method. When you do this, the compiler just needs to adjust the stack pointer to make room for the instance. The memory is already devoted for your use. That is, as long as you don’t overflow the stack. As long as you use local variables wisely and avoid an endless loop where you just keep calling methods without ever returning, then you should never have to worry about running of stack space.
Whenever you declare a variable to be global or static, then its memory becomes part of your program memory that gets setup when your application is first loaded into memory. The compiler prepares this, the operating system loader sets up the process in memory, and the C++ runtime calls the constructors for any global and static variables at the right time. You can’t depend on the exact order that your global variables will be constructed but I can show you in a future episode a way to get around this.
One thing to note is that the stack and any global or statically allocated memory can only be used for fixed sizes of memory. The compiler must know ahead of time exactly how much memory is needed.
If you need a lot of memory or if you need some amount of memory that you only know how much when your program is running and about to ask for the memory, that’s when you turn to the heap. In previous episodes, I simplified this a bit by referring to the heap as the main computer memory that you get from the operating system. I said that whenever you need a million bytes, you just ask the operating system for the memory.
That’s sort of correct. Let’s advance your understanding a bit though. What actually happens is you ask your language runtime for the memory. Hold on, I can hear you ask already, “I thought C++ didn’t have a runtime. Doesn’t it get compiled directly to native code that the processor can execute directly?”
Yes, it does get compiled directly. But there’s no processor instruction that knows how to grab a million bytes to give to your program. The processor can’t even give you one byte. That’s just not its job.
The operating system does prepare memory for a running application and makes sure that the memory is isolated from other programs also running on the same computer.
When I say that your C++ program asks the runtime for memory, I’m referring to some code that gets compiled and added to your program as part of the standard library runtime. It’s not a runtime that interprets your code or manages your code.
The C++ runtime is just some functionality that, among other things, you can make use of to request memory. It works with your operating system to do this and you interact with it through the syntax of the C++ language. Depending on your operating system, you’ll have C++ libraries that make up the runtime that are designed to work with that operating system.
In C++, you create an instance of your class on the stack by just declaring a local variable of your class type within a method. And you create an instance that’s part of your program’s loaded memory by declaring a global variable or by declaring a variable within a method to be static.
And you create an instance that’s on the heap by using the C++ keyword, new. New is actually an operator, or a method, that does three things. It first allocates the required memory on the heap. It’ll work with the operating system to accomplish this and might ask for just the memory needed or it might ask for memory from the operating system in larger chunks that it will manage itself. Regardless of how it does this, it will get some memory that’s big enough for your class instance.
Once it has the memory, it calls the constructor which will setup the memory just as your class needs.
And the third thing operator new does is to return a pointer to the memory.
Your code will need to work with this variable instance through the pointer that’s returned to you. This is different than when you declared your class variable as a local variable in a method. As a local variable, you can work with your instance directly and don’t have to worry about pointers.
The reason you don’t have to worry about pointers is because you don’t have to return local variables back to memory yourself. They get destructed when the variable goes out of scope and the memory gets reclaimed when the method returns.
This means that local variables cannot live past the end of the method. That’s actually another reason why you might need to allocate variables from the heap. Any variable that you create through operator new will live until you destroy it or until your application ends and the operating system swoops in and reclaims everything.
There’s another operator called delete that acts as the opposite of new. You call new to create a new instance and get a pointer back with your freshly constructed instance. When you’re done with this instance, you pass the same pointer to operator delete to clean everything up.
Delete will perform two tasks. It’ll first call your destructor. And it will then return the memory back to the heap so the memory will be available the next time you ask for it.