Say we have a class X that does not have an overloaded operator=()
function.
class X {
int n;
X() {n = 0;}
X(int _n) {n = _n;}
};
int main() {
X a; // (1) an object gets constructed here
// more code...
a = X(7); // (2) another object gets constructed here (?)
// some more code...
a = X(12); // (3) yet another object constructed here (?)
return 0;
}
Is a new object constructed at (2)? If it is, what happens to the old object that was constructed at (1)? Is it automatically destructed or deallocated (which is it)? Is it overwritten?
And what happens farther down the code at (3)?
And most importantly, is there ever a chance of causing a memory leak by writing code like in the above?
A variable can only hold one value, so if you assign a new value to a variable the old value is gone.
what happens when you assign one object to another object ?? It assigns the reference of the Object. In other words, both variables 'point' to the same Object. For example/ int [] a = new int[5]; int [] b = a; b and a now 'point' to the same array.
Assigning one variable to another variable creates an alias of each variable. An alias is variable that points to the same object in memory as another variable. In the example above, both variables var1 and var2 are aliases of each other. In Python, it is possible to destroy references.
Short answer: no.
What you need to understand is that there's a lot of "implicit" code generated by the compiler that you, as a novice, don't know about. We'll use your code for class X
as a direct example:
class X {
int n;
public: //You didn't include this, but this won't work at all unless your constructors are public
X() {n = 0;}
X(int _n) {n = _n;}
};
Before the code gets turned into Object Code, but after your compiler gets ahold of your class definition, it transforms your class into something that looks (roughly) like this:
class X {
int n;
public:
X() {n = 0;} //Default-Constructor
X(int _n) {n = _n;} //Other Constructor
//GENERATED BY COMPILER
X(X const& x) {n = x.n;} //Copy-Constructor
X(X && x) {n = x.n;} //Move-Constructor
X & operator=(X const& x) {n = x.n; return *this;} //Copy-Assignment
X & operator=(X && x) {n = x.n; return *this;} //Move-Assignment
~X() noexcept {} //Destructor
};
The rules for when these members are automatically created are not super-obvious (A good starting reference here), but for now, you can trust that in this case, that's exactly what happens.
So in your main
function, let's go over what happens, with the specifics called attention to with comments:
int main() {
X a; //Default-Constructor called
a = X(7);//Other Constructor called, then Move-Assignment operator called,
//then Destructor called on temporary created by `X(7)`
a = X(12); //Same as previous line
return 0;
//Destructor called on `a`
}
We'll add a few more lines to show most (if not all) of the various permutations of these calls:
int main() {
X a; //Default-Constructor
X b = a; //Copy-Constructor (uses copy-elision to avoid calling Default + copy-assign)
X c(5); //Other Constructor
X d{7}; //Also Other Constructor
X e(); //Declares a function! Probably not what you intended!
X f{}; //Default-Constructor
X g = X(8); //Other Constructor (uses copy-elision to avoid calling Other + move-assign + Destructor)
X h = std::move(b); //Move-Constructor (uses copy-elision to avoid calling Default + move-assign)
b = c; //Copy-assignment
b = std::move(d); //Move-assignment
d = X{15}; //Other Constructor, then Move-Assignment, then Destructor on `X{15}`.
//e = f; //Will not compile because `e` is a function declaration!
return 0;
//Destructor on `h`
//Destructor on `g`
//Destructor on `f`
//Destructor will NOT be called on `e` because `e` was a function declaration,
//not an object, and thus has nothing to clean up!
//Destructor on `d`
//Destructor on `c`
//Destructor on `b`
//Destructor on `a`
}
That should cover the basics.
And most importantly, is there ever a chance of causing a memory leak by writing code like in the above?
As written, no. However, suppose your class did something like this instead:
class X {
int * ptr;
public:
X() {
ptr = new int{0};
}
};
Now, your code would leak, because every time an X
was created, you'd have a pointer that never gets deleted.
To solve this, you need to make sure that A) The destructor properly cleans up the pointer, and B) that your copy/move constructors/operators are correct.
class X {
int * ptr;
public:
X() {
ptr = new int{0};
}
X(int val) {
ptr = new int{val};
}
X(X const& x) : X() {
*ptr = *(x.ptr);
}
X(X && x) : X() {
std::swap(ptr, x.ptr);
}
X & operator=(X const& x) {
*ptr = *(x.ptr);
return *this;
}
X & operator=(X && x) {
std::swap(ptr, x.ptr);
return *this;
}
~X() noexcept {
delete ptr;
}
};
This code will not leak memory if used as-is in either your main
function or mine. But of course, it doesn't stop the leaks if you do something like this:
int main() {
X * ptr = new X{};
return 0;
//Whelp.
}
In general, if you never need to use pointers at all, it's recommended you use something like std::unique_ptr
instead, as it gives most of this stuff for free.
int main() {
std::unique_ptr<X> ptr{new X{}};
return 0;
//Destructor called on *ptr
//`delete` called on ptr
}
And it's a good idea in your original class, with a caveat that, unless you explicitly change it, your class won't by copyable anymore (though it'll still be movable):
class X {
std::unique_ptr<int> ptr;
public:
X() {
ptr.reset(new int{0});
}
X(int val) {
ptr.reset(new int{val});
}
//X(X && x); //auto generated by compiler
//X & operator=(X && x); //auto generated by compiler
//~X() noexcept; //auto generated by compiler
//X(X const& x); //Deleted by compiler
//X & operator=(X const& x); //Deleted by compiler
};
We can see the changes in my previous version of main
:
int main() {
X a; //Default-Constructor
//X b = a; //Was Copy-Constructor, no longer compiles
X c(5); //Other Constructor
X d{7}; //Also Other Constructor
X f{}; //Default-Constructor
X g = X(8); //Other Constructor (uses copy-elision to avoid calling Other + move-assign + Destructor)
X h = std::move(c); //Move-Constructor (uses copy-elision to avoid calling Default + move-assign)
//b = c; //Was Copy-assignment, no longer compiles
c = std::move(d); //Move-assignment
d = X{15}; //Other Constructor, then Move-Assignment, then Destructor on `X{15}`.
return 0;
//Destructor on `h`
//Destructor on `g`
//Destructor on `f`
//Destructor on `d`
//Destructor on `c`
//Destructor on `a`
}
If you want to use std::unique_ptr
, but also want the resulting class to be copyable, you'll need to implement the copy constructor yourself using the techniques I discussed.
And that should be about it! Let me know if I missed anything.
At point (2) three things happen:
X(int _n)
constructor.a
.The same thing happens at point (3).
At the end of the function, the default destructor on a
is invoked.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With