Destructor and default destructor

Pages: 12
Hi all,

I'm by this trivial code just trying to comprehend the destructor of C++ programs regarding memory leak. Please see.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Test {
	int* myPtr;

public:
	Test(int& i): myPtr(&i) { }
	void print() { cout << *myPtr << endl; }
};

//***********************

int main()
{
	int n = 5;
	Test myT(n);

	myT.print();

	system("pause");
	return 0;
}


I this simple code, we have a pointer in the class and we want to print its contents. So far so good. After printing the data, the programs finally terminates. But since we didn't use a delete for the pointer created in the class, that is dangling even after closing the program (naked pointer).

I know that most compilers will create a default destructor like this:

~Test() { }

But it doesn't do anything because there is nothing in its body!

So what is the correct way of writing such a program, or programs like this, so that we have no memory leak, please?
Last edited on
Your program does not have a memory leak. delete is only used for things that has been created with new.
Well it isn't a dangling pointer as is, because the object being pointed to maintains it's scope at least as long as myT does.

Nor do you make use of new, so there is nothing to delete.

But consider this, if you were to have a copy-assignment.
1
2
3
4
5
6
7
8
9
10
11
int main()
{
    Test copy;
    {
        int n = 5;
        Test myT(n);
        copy = myT;
    } // oops, n goes out of scope, and invalidates all &n pointers
    copy.print();
    return 0;
}

You need to be very careful (read: don't do it) when holding raw pointers to memory you don't own. Such pointers can be invalidated without warning. Or you take the conservative approach and just live with memory leaks because you don't know which objects might still have live references somewhere in the system.

std::shared_ptr certainly helps in this respect.

Thanks so much, Peter and Salem.
Peter, what if our class is this way, please?

1
2
3
4
5
6
7
8
9
class Test {
	int* myPtr;

public:
	Test(int& i): { 
                myPtr = new int (i); 
}
	void print() { cout << *myPtr << endl; }
}; 


Now, I assume we have a dangling pointer, so what is the correct way of writing this code so that there is nothing wrong, please?

Salem, you’ve created an object this way:

Test copy;

Since we haven’t a default constructor, the compiler creates one for us.
But the question is, what does that default constructor do on our pointer ptr in the class, please.
Now, I assume we have a dangling pointer, so what is the correct way of writing this code so that there is nothing wrong, please?


You still do not have a dangling pointer. You do have a memory leak, though, whenever an instance of class Test is destroyed. When the instance of class Test is destroyed, the pointer myPtr is also destoyed, and if a pointer doesn't even exist, it can't be a dangling pointer.

so what is the correct way of writing this code so that there is nothing wrong, please?


Best; just don't use dynamic memory.
1
2
3
4
5
6
7
8
9
class Test {
	int myInt;

public:
	Test(int& i): { 
               myInt = i;
}
	void print() { cout << myInt << endl; }
}; 


If you HAVE to use dynamic memory - for example, you're making a really big object, then use modern C++ smart pointers. You wouldn't do this for an int, but for the purposes of an example:
1
2
3
4
5
6
7
8
9
10
11
class Test 
{
        std::unique_ptr<int> myPtr;

public:
	Test(int& i)
	{ 
            myPtr = std::make_unique<int>(i);
        }
	void print() { std::cout << *myPtr << std::endl; }
};  


A unique_ptr will, when it is destroyed, call delete for you on the object it is pointing at. The memory is released properly and there is no leak.

For the purposes of learning, using new and delete:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Test 
{
        int* myPtr;

public:
	Test(int& i)
	{ 
            myPtr =  new int (i); 
        }
	void print() { std::cout << *myPtr << std::endl; }

        ~Test()
        {
           delete myPtr; // prevent memory leak
        }
};  




I know that you're just experimenting for learning purposes, but it's still worth saying this; ising new and delete manually is bad. Don't do it. If you find yourself typing new or delete, stop. This is C++ in the year 2019; we have smart pointers and containers and so on. Manual memory management in new code is a sign that something has gone wrong.

But the question is, what does that default constructor do on our pointer ptr in the class, please.

Nothing. You get a pointer, with no guarantees whatsoever about where it is pointing.
Last edited on
The member myPtr is default initialized: https://en.cppreference.com/w/cpp/language/default_initialization
In other words, its value remains unknown.

You OP program would look like this without the class:
1
2
3
4
5
6
7
int main()
{
  int n = 5;
  int* myPtr = &n;

  cout << *myPtr << endl;
}

You latest program would look like this without the class:
1
2
3
4
5
6
7
8
int main()
{
  int n = 5;
  int* myPtr; // uninitialized
  myPtr = new int (n);

  cout << *myPtr << endl;
} // dynamically allocated memory was not deallocated 


If the class owns a resource, then it must manage that resource:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Test {
  int* myPtr {nullptr}; // default value
public:
  Test() = delete;

  Test( int i ) // no reason to pass by reference
  : myPtr{ new int (i) } // initialize
  {}

  ~Test() {
    delete myPtr; // deallocate
  }

  Test( const Test& rhs ) // copy constructor
  : myPtr{ new int (*rhs.myPtr) } // this is not safe. Why?
  {}

  Test( Test&& rhs ) // move constructor
  : myPtr{ rhs.myPtr }
  {
    rhs.myPtr = nullptr;
  }

  Test& operator= ( const Test& rhs ) // copy assignment
  {
    if ( myPtr ) {
      if ( rhs.myPtr ) *myPtr = *rhs.myPtr;
      else {
        delete myPtr;
        myPtr = nullptr;
      }
    } else {
      if ( rhs.myPtr ) {
        myPtr = new int (*rhs.myPtr);
    }
    return *this;
  }

  Test& operator= ( Test&& rhs ) // move assignment
  {
    myPtr = rhs.myPtr;
    rhs.myPtr = nullptr;
    return *this;
  }

  void print() {
    if (myPtr) std::cout << *myPtr << '\n'; // without flush
  }
};
Thank you, Repeater.
1- So, in C++ 20 or even 17, when I need allocating memory from heap, I need to use either “smart-pointers” or “shared-pointers”, and old “new-delete” are to go and not used anymore, right?

The second point is where you said:
“When the instance of class Test is destroyed, the pointer myPtr is also destoyed, and if a pointer doesn't even exist, it can't be a dangling pointer.”
2- Why when the instance of class Test is destroyed, is the pointer myPtr also destroyed, please?!
I think only “delete” destroys “new” deallocating memory. Not?
3- I suppose the difference between memory leak and a dangling pointer is that, in the dangling pointer case, we have a pointer but it’s not initialized, but memory leak happens only when the pointer is initialized but after closing the program it still exists. Have I understood it correctly?

Thanks to you too, Keskiverto.
I need some more time to figure out your codes properly.
(1) Modern smart pointers (unique and shared ptr) have been a thing since C++11. Yes, it is suggested to use smart pointers instead of new/delete.

(2) You're confusing the pointer with the memory the pointer points to.
In your post http://www.cplusplus.com/forum/general/254549/#msg1117557 the Test copy; will be destructed automatically when it goes out of scope, and this Test object includes the pointer itself, but not the dynamic memory it points to. That's where the memory leak happens, because you now have no way to access that memory (through the pointer) again.

Don't think of it as a "pointer existing". What matters is in what manner the memory the pointer points to exists. If a pointer doesn't point to valid memory, that's what it means to be "dangling".
Last edited on
, but memory leak happens only when the pointer is initialized but after closing the program it still exists.


No.

A memory leak happens when a running program, that allocated an object with new, no longer has a pointer to that object and thus has no way to delete it.

On modern operating systems, when your program is closed, all the memory is recovered. Memory leaks only happen in running programs.
@keskiverto in your code, ¿when a valid `Test' object may hold a nullptr in `myPtr'?
Formally never, but I was naughty and did add the move-members to hint into Rule of Zero/Three/Five and if you give the user a button with label: "Do not press", you do know what will happen ...
Thanks to all.
1- Do we yet need a destructor while we’ve decided not to use raw memory assignment, I mean using the bygone “new-delete”, but “smart/shared pointers, instead”?

2- What’s the advantage of a default destructor, which the compiler in the absence of an explicit destructor creates implicitly for us, when there’s normally no code in its body?
Last edited on
Conceptually every class has and uses a destructor, but they can be so trivial as to do absolutely nothing. For example, a class which merely holds a few floats, and nothing else, the destructor doesn't do anything, and the compiler emits no code for it, but the implication is that it does still exist.

The default destructor extends this notion such that if member varibles are not merely simple data types like int and float, but of other class instantiations which themselves have complex destructors, the default destructor performs that destruction of those members as required. For example, as you know, various smart pointers automatically delete allocated memory, but if YOUR class only has a few ints and a few smart pointers as members, but no destructor of your authorship, the default destructor of your class calls the destructors of the smart pointer members so as to propagate destruction appropriately.

It is not that there is an advantage to default destructors, but they exist so that the expected behaviors you'd assume for int or float members also apply exactly the same for more complex members.

Destructors apply to all forms of object termination, and is not limited to only memory management. Where an object represents files, the destructor may be closing files. Where an object represents more complex notions, the destructor does anything representing that moment where the class ends its existence, whatever that might be.


Last edited on
Thank you.

So we only use smart or shared pointers to grasp memory from heap in modern C++, and because those modern pointers’ allocated memory is freed by the default destructor defined implicitly by the compiler, hence we even needn’t explicitly define a destructor either. Right?

Also for closing the files in a C++ program, I think they can be closed when the program closes/terminates. Not?
You have the general idea with smart pointers. std::shared_ptr is one type of smart pointer, std::unique_ptr is another. If you study std::shared_ptr, you'll need to be aware of std::weak_ptr for a solution to a particular issue, but it is rare.

Generally, yes, when you use smart pointers you can rely on the smart pointer to handle destruction (and therefore freeing memory automatically).

The more general subject is called RAII. The inventor admits this isn't a good marketing term, but it means that any resource (the R) that is acquired (the A) should be done at the initialization (one of the I's) of an object that owns it so that the destructor will release or destroy that resource automatically (and, in a point we've not mentioned, even under exception conditions).

When it comes to files, you have missed the point. Yes, not only files but even memory will be reclaimed by all modern operating systems, but to relying on that behavior is incorrect. It is likely that an application could leave files open without closing them, but then continue to operate by opening other files, and continuing to do so builds up open files that aren't being released (they are said to be leaked). There are a limited number of file handles available so the program can easily consume all available handles, leading to a condition where the program can no longer continue (because it can't open more files). As such, it is important to close files when no longer needed, and NOT to rely on the operating system to clean that up for us. It is well known from experience and empirical observation (at a scientific quality) that this is a requirement, and should not be trivialized.

RAII is rather unique to C++. It is based on the strong association between the constructor and destructor. Languages like C# and Java do not have destructors which act this way, and so RAII is not a paradigm which can be implemented in those languages. The only exception is their use of references. In those languages the behavior of references is nearly identical to the general use of std::shared_ptr. It is the only time a C# or Java programmer enjoys what RAII means. For classes written in Java and C#, the language offers no leverage which applies to files or other resources like device context (in graphics) or connections (for Internet), etc.

As you study smart pointer in C++ you'll discover something curious. Some examples will still use the "new" keyword, assigning the result to a smart pointer. That's valid, and was the only way to do that in the now ancient versions of C++ before the 21st century. You will otherwise discover the use of std::maked_shared (or std::make_unique). These take the place of the "new" keyword. They make a more efficient creation of the new resource (the details are beyond this post). Favor the "std::make_shared" approach over using the keyword "new" in all of your code. Where you might see the "new" keyword used in older code, just realize it is an older style, is less efficient, and has been "replaced" in most coding standards.
Last edited on
For classes written in Java and C#, the language offers no leverage which applies to files or other resources like device context (in graphics) or connections (for Internet), etc.
C# provides the "using" clause and Java provides "try-with-resources", which automatically call dispose, and both have try-finally syntax, where the "destruction" (e.g. disconnecting from a database) is handled in the finally clause. But it's messier than C++ RAII for sure.
Last edited on

But it's messier than C++ RAII for sure.


I get this point a lot. I find "messier" a kind way to put it ;)
@keskiverto

1- You used Test() = delete; to prevent a default constructor from being defined or called, right? But why for my simple code VS 2019 doesn't create a default constructor and gives an error in this case, please?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using namespace std;

class Test {
	int* myPtr = nullptr;

public:
	Test(int i): myPtr {new int (i)} { }

	void print() { cout << *myPtr << endl; }

	~Test() { delete myPtr; }
};

//***********************

int main()
{
	int n = 5;
	Test myT;

	myT.print();

	system("pause");
	return 0;
}


2- In the move constructor Test( Test&& rhs ), what is the name of && there? I mean how to read it?

3- "Rule of Zero/Three/Five". I now believe that every class must be defined to have 7 methods:
1) Default Constructor = delete;
2) Normal Constructor // when we set arguments for the class
3) Destructor
4), 5) Copy and Move Constructors
6) , 7) Copy and Move Assignments


Wrong idea?

@Niccolo
Thank you. I got your explanations. They are beneficial.
You also talked about other languages. What concerns me as a C++ programmer is that still a language like Java or ancient C is more popular than C++. I don't know why! :(
https://www.tiobe.com/tiobe-index/



1. The Test() = delete; is explicit and unnecessary, because the base rule is:
IF you supply a custom constructor
THEN the compiler does not generate the default constructor

2. rvalue reference https://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html

3. The Rule: https://en.cppreference.com/w/cpp/language/rule_of_three
You should note that the rule is not "must have 7". All classes do not have to be both copyable and movable, and many classes can be default constructible.
The Test() = delete; is explicit and unnecessary

It could serve a purpose in showing that leaving out the default constructor is intentional and that it was not just forgotten. This should make anyone think twice before adding a default constructor to the class.
Last edited on
Pages: 12