C++ Tutorial
C++ Destructors
A destructor runs when an object dies. Use it to release what the constructor acquired.
The matching end of a constructor
A constructor runs when an object is created. A destructor runs when that object is destroyed. Together they bracket the object's life. If the constructor acquired something — a file, a lock, a log line that must be closed — the destructor is where you release it.
The destructor's name is a tilde plus the class name: ~Session. It takes no parameters and has no return type. You do not call it. The compiler calls it when the object goes out of scope, when a block ends, or when the program leaves main.
Watch the order print
This class does not manage a heap pointer. It only prints, so you can see the calls. The constructor writesstart. The destructor writes end. Create one object in main and read the three lines in order: start, the message from main, then end.
Example
#include <iostream>
#include <string>
using namespace std;
class Session {
string label;
public:
Session(string label_in) : label(label_in) {
cout << "start " << label << endl;
}
~Session() {
cout << "end " << label << endl;
}
};
int main() {
Session a("alpha");
cout << "inside main" << endl;
return 0;
}Compile this in /cpp/try. end alpha appears after inside mainbecause a is destroyed when main returns.
Last constructed, first destroyed
Automatic objects in one block are destroyed in reverse order of construction. Create alpha thenbeta. Destruction prints end beta then end alpha. Think of a stack: the last plate you put down is the first you pick up.
Example
#include <iostream>
#include <string>
using namespace std;
class Session {
string label;
public:
Session(string label_in) : label(label_in) {
cout << "start " << label << endl;
}
~Session() {
cout << "end " << label << endl;
}
};
int main() {
Session a("alpha");
Session b("beta");
cout << "inside main" << endl;
return 0;
}Expected output, in this order: start alpha, start beta, inside main, end beta, end alpha. If your output differs, you are not looking at automatic objects in one scope, or a print is buffered oddly. On StudyGrid this order is the one you should see.
A block ends the inner object
Extra braces create a nested scope. The object declared inside is destroyed at the closing brace, not at the end of main. That is how you limit a lifetime: put the object in the smallest block that still needs it.
Example
#include <iostream>
#include <string>
using namespace std;
class Session {
string label;
public:
Session(string label_in) : label(label_in) {
cout << "start " << label << endl;
}
~Session() {
cout << "end " << label << endl;
}
};
int main() {
cout << "before" << endl;
{
Session inner("inner");
cout << "inside block" << endl;
}
cout << "after" << endl;
return 0;
}You should see end inner before after. The destructor ran at the inner closing brace. Nothing was leaked: label is a string member, and that string cleans up its own storage when Session is destroyed.
Release what you acquired
The printouts are a stand-in for real work. If a constructor opens a file, the destructor closes it. If a constructor starts a timer log, the destructor writes the stop line. Pair them. The class then owns the resource for as long as the object lives, and no longer.
Prefer members that clean themselves — string, vector, file streams — over a rawnew in the constructor. Then your destructor might only log, or it might be empty. An empty destructor is still valid; if every member is already safe, you often omit writing one and let the compiler generate it.
Do not new in the constructor unless you delete in the destructor, and even then a container or a smart pointer is the better default. This chapter does not allocate with new. The logger class owns a string and nothing else.
Rules of the destructor
| Rule | Detail |
|---|---|
| Name | ~ClassName with no return type. |
| Parameters | None. You cannot overload it. |
| Call site | The compiler calls it. You do not write a.~Session(). |
| When | End of scope, end of block, or when a container drops the element. |
If you write any of the special members (destructor, copy constructor, copy assignment) you take on more lifetime rules. For this tutorial, write a destructor when you have something to undo, and keep the class small. Skip copying these logger objects; one object per scope is enough to see the order.
Lifetime is a stack of objects
Construct in an order you can explain. Destroy in the reverse of that order. Put cleanup in the destructor so every exit from the scope — including an early return — still runs it. Next: operator overloading, which is how a type you wrote can use + or << like an int.
Worked examples
The short listings above are there so you can see the grammar. The programs here use the same statements on quantities that already have units: a speed, a pH, a count of bases. They are classroom numbers. Air resistance is ignored. g is 9.81 m/s² unless a line says otherwise.
Open them in the C++ editor at /cpp/try. Change one measurement and check whether the result still has the right unit.
Engineering
A logger that closes itself
RAII: the constructor acquires, the destructor releases. When Guard leaves main, ~Guard prints release even if you forget a close() call. That is why fstream closes the file when the object dies.
Example
#include <iostream>
using namespace std;
class Guard {
public:
Guard() { cout << "acquire" << endl; }
~Guard() { cout << "release" << endl; }
};
int main() {
Guard g;
cout << "log a 12 mT reading" << endl;
return 0;
}Physics
A heap sample, then delete
new Sample must meet delete. The destructor runs as part of delete. Printing the mass in ~Sample is a teaching trick so you can see the lifetime; production destructors usually only free resources.
Example
#include <iostream>
using namespace std;
class Sample {
double grams;
public:
Sample(double g) : grams(g) {}
~Sample() { cout << "done with " << grams << " g" << endl; }
};
int main() {
Sample *s = new Sample(0.250);
delete s;
return 0;
}