One of the biggest differences between C++ and C# is in how object lifetimes are managed. I was going to have a general topic on destructors just like constructors but there are just too many differences. So today, we’re going to look at C++ destructors.
A destructor is just a method that belongs to a class or struct that gets called by the compiler at a specific time when an instance of your class goes out of scope. It gives you the ability to clean up or put things back the way you found them. It’s the last method your instance will run.
There are two main uses of destructors in C++, to free memory that was allocated, and to release other resources such as file handles.
You also learn some of the philosophy that guides how C++ decides what order to perform things such as calling destructors. This is important when multiple destructors all need to be called.
Listen to the full episode or read the full transcript below.
The first thing to understand is what does it mean to destruct an object and why is this necessary.
If all you had to work with are just a bunch of ints, chars, bools, etc. sitting on the stack, then you wouldn’t need to worry about object lifetimes or destructors. This is because all the local variables get cleaned up when the stack pointer is adjusted back to its position before a method was called.
Sometimes this is not enough. Let’s say you open a data file on your computer and have a local variable that keeps a handle to the open file. A handle is just some value in memory that the operating system uses to identify which file is open. The operating system will keep this file open until you close it or until your application exits. This means that when you’re done with a file, you should close it. If you don’t, then the operating system will think you still need it and this could cause problems later. Just letting the handle memory be reclaimed won’t actually close the file. You have to take this step yourself.
Think of it like this. You’re at home when the phone rings. You answer the phone and start talking. At some point, you’re done talking and you hang up the phone. But what if you don’t hang up the phone? The call will end soon after the other person hangs up. But your phone line will stay open and other calls will get a busy signal. Answering the phone is just like opening a file. And hanging up is just like closing a file when you’re done. If you just stop talking and put the phone on the table, then that’s almost exactly what happens when you stop using a file and let the file handle be reclaimed without closing it.
You need to sometimes take action to clean up things.
This could mean calling other methods to close handles or freeing memory. We haven’t talked about memory allocation yet beyond just what you get with the stack. Now seems like a good time.
Sometimes, you need a lot of memory.
Either a big chunk for a single purpose or many smaller chunks that all need to be next to one another. The stack is a poor place to rely on large amounts of memory. You can get much more by asking the operating system for whatever you need. Of course the operating system has to have the memory you are asking for available or it can’t fulfill your request.
Let’s say you ask for a million bytes of memory. With modern computers, you’ll probably get it. But here’s the thing. You might have a lot of memory, but it’s not unlimited. When you’re done with that million bytes, you need to return the memory to the operating system which will then add it back to the available pool of memory. You need to take action to free the memory. Just letting your variable that tracks your million bytes be reclaimed will only cause your program to forget all about the million bytes. But the operating system won’t forget. It will keep that memory dedicated to your program even if your program has completely forgotten about it. This is called a memory leak.
When your program ends, any memory that was leaked will be made available again. That’s the only time that the operating system can take the memory back. It’s almost like the operating system forecloses on the memory. Just like in real life, foreclosures are bad for everybody.
What’s all this have to do with destructors? A destructor is a method with a special purpose. It gives you the opportunity to do whatever cleanup actions you need. Only you know what you need to do here. If you’ve opened files, then make sure they are closed. If you’ve allocated memory, then make sure it gets freed. If you’ve written some data to a database, then make sure your changes get committed and the database gets closed.
The really nice part about all this, at least for C++, is that you don’t have to actually call your destructor. You just write your destructor to do whatever it needs to do and let it be called by the compiler at the right time. There is an exception to this rule about calling your destructor but it’s extremely rare.
Only classes and structs can have a destructor and they can each have just a single destructor. That’s all you need. The problem though is that a file handle is not a class and has no destructor. And a pointer to that million bytes is also just a pointer and has no destructor. You get around this by saving the file handle or saving the memory pointer to a data member in a class. Then you create a destructor for the class that checks if the data member contains anything that needs extra cleanup action. If so, the destructor method does the cleanup.
The only other thing to know at this point is when your destructors get called. There’s actually a bit more to learn about destructors but we’ll cover those details later.
Whenever an instance of a class goes out of scope, you can count on the destructor being called. If you have multiple instances all going out of scope at the same time, well, there’s a rule for that too.
In general, C++ likes to destruct things in the reverse order that they were constructed. For a real life example even if it is a bit made up, imagine you construct an office building and then construct a very expensive piece of equipment on the second floor. Sometime later, you need to tear everything down. Would you tear the building down first? Or would you remove the valuable equipment first? It really helps if you remove the equipment first while the building is still standing. C++ just follows this same philosophy. The last object to be created will be the first object to have its destructor called.
And this goes to one of the biggest differences between C++ and C#. You see, with C#, you have no idea when your objects will be destructed. Or even if they are ever destructed. That’s such a huge difference, that we’ll need a whole episode just for C# destructors.
C++ sometimes gets a bad reputation for leaking memory. And if you’re working with raw pointers to allocated memory, then it can indeed sometimes be difficult to properly manage your memory allocations. The solution though is simple. Stop using raw pointers and stop using raw handles. The C++ Standard Library has excellent support for smart pointers. And it’s not that hard to write your own smart pointer. I’ll create a future episode dedicated to smart pointers and walk you through the code needed to write your own. This will be similar to the WordGuess game that I explained in episodes 13, 14, and 15.
We’ve got a lot more to cover before then. Stay tuned for tomorrow where you’ll learn all about destructors in C#.