Constructors and Destructors

Constructors

In the earlier lessons, we have been using fraction_new to create new objects. C++ has something built-in called constructors that does a similar thing but better. A constructor is defined and used this way:

  1. #include <stdio.h>
  2. struct fraction {
  3. int numerator;
  4. int denominator;
  5. fraction(int numerator, int denominator) {
  6. this->numerator = numerator;
  7. this->denominator = denominator;
  8. }
  9. };
  10. main() {
  11. fraction f(1, 2);
  12. fraction g = fraction(3, 4);
  13. }

The important thing about defining constructors is that it is just a member function with the same name as your struct. And it’s the name that makes it special. The arguments can be whatever you want. Then when you want to create objects, you can choose one of the two ways to invoke the constructor as demonstrated above.

You can also use constructors to initialize objects dynamically allocated on the heap. Using just what we have covered already covered, you can do it like this:

  1. main() {
  2. fraction* ptr = (fraction*)malloc(sizeof(fraction));
  3. *ptr = fraction(3, 4);
  4. printf("%d / %d\n", ptr->numerator, ptr->denominator);
  5. }

But there’s a better way because malloc is actually from the old days of C. The main reason why we’ve been using it is because it isolates the memory management from the initialization. C++ has something much better that combines dynamic memory allocation and initialization -- a new keyword, the new keyword. Hehehe. I am so clever.

The new keyword works like malloc, but instead of giving it the number of bytes we need, we just tell it the data type we need and it will allocate the correct amount of memory. It then invokes the constructor on that chunk of memory. The usage of the keyword is as follows:

  1. main() {
  2. fraction* ptr = new fraction(3, 4);
  3. printf("%d / %d\n", ptr->numerator, ptr->denominator);
  4. }

Isn’t that so much better? We don’t have to import malloc.h anymore, and we don’t need to use sizeof either. Also, we can use new to allocate arrays. Look at the sample code below to see how this is done:

  1. #include <stdio.h>
  2. struct fraction {
  3. int numerator;
  4. int denominator;
  5. fraction() {
  6. printf("Default constructor called!\n");
  7. }
  8. fraction(int numerator, int denominator) {
  9. printf("Non-default constructor called!\n");
  10. this->numerator = numerator;
  11. this->denominator = denominator;
  12. }
  13. };
  14. main() {
  15. int* arr = new int[10];
  16. fraction* fraction_arr = new fraction[10];
  17. }

You will notice if you run the code above, that it will print a series of "Default constructor called!". That’s because we can’t pass arguments to constructors when allocating arrays, so C++ will call the default constructor for each one. The default constructor is the one with zero arguments, and if you don’t write one, C++ will do nothing and just give you garbage in your memory like you would expect from malloc.

Destructors

Constructors also have a opposite called destructors. Constructors exist to initialize objects. Destructors are there to clean up after objects are done with life. This clean up step can do anything you want, but in contest code, it’s mostly used to free memory. Since the purpose of destructors is clean up, they are called automatically at two possible places. The first and easiest to explain would be when an object that exists on the heap is about to be destroyed. Just like the new keyword, there is a destructor-aware version of free: the keyword delete. Let’s see how destructors are made and how delete is used with some sample code.

  1. #include <stdio.h>
  2. struct leaky_object {
  3. int* pointless_memory_leak;
  4. leaky_object() {
  5. this->pointless_memory_leak = new int[10];
  6. }
  7. // Destructors always have no arguments.
  8. ~ leaky_object() {
  9. printf("Calling the destructor!\n");
  10. delete pointless_memory_leak; // Free the potential memory leak!
  11. }
  12. };
  13. main() {
  14. leaky_object* ptr = new leaky_object();
  15. delete ptr; // This will call the destructor, then free the memory
  16. }

As you can see above, destructors are defined like a default constructor, but with a ~ at the start. The destructor is called when the object is destroyed with delete.

We can also use delete to free an array, but when freeing an array, we have to use a slightly different keyword: delete[]. Using the delete[] keyword on an array will call the destructors one by one on each item in that array, then free all the memory.

  1. #include <stdio.h>
  2. struct leaky_object {
  3. int* pointless_memory_leak;
  4. leaky_object() {
  5. this->pointless_memory_leak = new int[10];
  6. }
  7. ~ leaky_object() {
  8. printf("Calling the destructor!\n");
  9. delete pointless_memory_leak;
  10. }
  11. };
  12. main() {
  13. int* arr = new int[10];
  14. leaky_object* leaky_object_arr = new leaky_object[10];
  15. delete[] leaky_object_arr; // This will call the destructor for ten objects, then free the memory
  16. }

The other place destructors are called is when variables "go out of scope". Going out of scope is difficult to understand if I explain it in technical words, but I'm sure you already know intuitively what that means. So let's just look at some examples of what I mean.

  1. #include <stdio.h>
  2. struct variable {
  3. char* name;
  4. variable(char const* name) {
  5. // Long code that basically does this->name = name
  6. int length = 0;
  7. while(name[length] != 0) {
  8. length++;
  9. }
  10. this->name = new char[length + 1];
  11. for(int i=0; i<length; i++) {
  12. this->name[i] = name[i];
  13. }
  14. this->name[length] = 0;
  15. printf("Creating variable '%s'!\n", this->name);
  16. }
  17. ~ variable() {
  18. printf("Destroying variable '%s' because it is going out of scope!\n", this->name);
  19. delete[] this->name;
  20. }
  21. };
  22. variable global = variable("global");
  23. void function() {
  24. variable function = variable("function");
  25. }
  26. main() {
  27. variable main = variable("main");
  28. function();
  29. for(int i=0; i<3; i++) {
  30. printf("Loop #%d\n", i+1);
  31. variable loop = variable("loop");
  32. }
  33. }

I think after running the code above and looking at the output, you will have an intuitive understanding of what it means when a variable goes out of scope and when destructors are called for those.