In this video, we'll look at the difference between a shallow copy
and a deep copy in the context of the copy constructor.
First, we'll assume that the object we're copying has a raw pointer.
When the object is constructed,
we'll likely allocate storage for the data that that pointer is pointing to.
And then when we're done with the copy we'll release the memory as per best practices.
But what happens in the copy constructor.
We can do a shallow copy or a deep copy. Let's see what that means.
Let's first look at doing a shallow copy and the problems we can run into.
A shallow copy is the default behavior provided by the compiler
generated copy constructor.
It basically does member-wise copying of all the object attributes.
So you end up with the newly created object,
and the object being copied both pointing to the same area of storage in the heap.
So what's the problem?
Well, the problem comes into play when one of those objects is destroyed and its destructor is called.
When this happens the object's destructor releases the memory allocated on the
heap as it should.
However, the other object is still pointing to this released area
and thinks the area is still valid.
This could lead to subtle errors.
The best thing that can happen is that the program crashes
while in development so that you can fix the error.
But we'll learn how to do a deep copy and avoid the error completely.
But first, let's walk through an example that has a class with a pointer attribute
that does a shallow copy so we can really understand the problem.
Here's the class we'll use in this example.
The class is called shallow and it has a single data attribute
called data that's a pointer to a single integer.
This class is about as simple an example as we could write that demonstrates the problem
with a shallow copy.
But any class that has a raw pointer as a data member will have the same issues.
This class has a constructor that expects an integer,
a copy constructor and a destructor. Let's see how these are implemented.
Here's the code for the constructor.
Notice that the constructor allocates storage for the integer,
and stores the passed in integer d where that data pointer is pointing.
Since we allocated this storage dynamically,
then we must be sure to release it in the destructor. Let's do that now.
Here's the destructor.
It simply frees the storage that was allocated by the constructor as it should.
It also displays a message to the console.
Okay. So now let's implement the copy constructor.
Here's the copy constructor.
This copy constructor is user defined
but it provides the same semantics as the default
compiler generated copy constructor if we hadn't provided this one.
Notice that in the initializer list we simply copy the pointer.
This is member-wise copy. Now we have a problem.
In this case, the source object and the newly created object both point
to the same area and heap that was allocated for the data.
But why is this a problem?
Well, the problem comes into play when we make a copy of a shallow object
using the copy constructor that's doing a shallow copy.
Suppose we have a function display shallow that expects a shallow object by copy.
We make the copy
with the copy constructor as usual,
but when that local object s goes out of scope its destructor is called,
and the destructor releases the storage in the heap that s is pointing to.
You see the problem now.
The object that was copied into this function still points
to this now released storage, but it thinks that the storage is valid.
So all kinds of bad things can happen.
Now here's a sample main we create object1 as a shallow object.
Object1 will be created and an integer will be allocated on the heap and 100 stored in it.
Now we call display shallow,
and a copy of object one will be created inside that function.
And when that function is done, the copy of object one will be destroyed.
Since object one and the copy that was just destroyed
pointed to the same memory area,
object one now points to invalid storage.
If we try to access that storage from object1, our program could crash.
Also when the destructor for object1 eventually gets called
it will try to release memory that's no longer valid and will probably crash.
Let's head over to the IDE, and we'll walk through this example and I'll draw out exactly what's happening.
Okay so I'm back in the IDE. I'm in the section 13 workspace
in the ShallowCopy project.
And we've got a program, and the class is called shallows,
the same one that I put in the slides.
I made a couple little changes in the main just to streamline it a little bit.
Anyway, this program does a shallow copy in the copy constructor,
and I'll walk you through that right now, but first let's just run it.
And I'm going to run it without the debugger. So you can see that it does indeed crash.
And there's the crash right here in windows,
and it's a bad crash. Okay. It's not the kind of thing we want.
So the point of
this video is to try to understand exactly why this is crashing,
why we need a deep copy and why we've got to be really careful when we use raw pointers.
So let's take a look at the class. The class is shallow.
It's got a raw pointer here to a single integer. We could have created an array of integers,
a pointer to a some sort of object, it doesn't really matter.
In this case, it's about as simple as examples you can get.
And here we have a set value and a get value that we're using in my main.
We have a constructor that just expects the integer.
And what it does is you can see the source code for the constructor right here on line 21.
It allocates storage on the heap dynamically for that one integer.
And the integer is d. It's being passed in. Then I'm just de-referencing the pointer and storing that integer
where data's pointing. Really, really simple.
Okay. The destructor, let's take a look at that. Obviously,
whenever we allocate storage in the constructor,
the best practice is to release that storage in the destructor.
That's what we're doing here. And in this case, we're just displaying destructor freeing data
so that we can see when it's being called.
We're going to step through this in the debugger,
so we will definitely be able to see when it's being called.
And then we've got the copy constructor. In this case, we're doing a shallow copy.
You can see right here on line 27
in the constructor initialization list, all we're doing is we're copying the value in
that data variable which is the pointer from source
to this, which is the object we're creating right now so.
It's copying the pointer not what it's pointing to.
What's gonna happen is both these objects when we copy them
will be pointing to the same area, and I'll show you that in the debugger.
Then we've got a real simple function right here called display shallow,
which we pass in an object by value. So we're gonna make a copy of it,
and that's where the problem shows up and again I'll walk you through that.
Okay. So let's debug this, and we'll step through it so we can see exactly what's going on.
And I'll move the output window over here to the right so we can
see our constructors and destructors being called.
All right. So I'm right here on line 42.
And here, we're just doing a regular constructor call,
and we're passing in the integer to us so this constructor right here on line 21 will be called.
So let's step through this.
And you can see now we're on line 22.
We're going to allocate the new integer on the heap.
We just did that and now I'm storing 100 into that area.
And I'm back to line 43 so at this point obj1
has been created, it's right here.
And you can see that data is a pointer to an integer.
And we just stored 100 where that integer was pointing.
Okay. Now we're going to run into a little bit of a problem. This is where the
deep copy is necessary rather than the shallow copy,
and I'll do another video after this where we'll do a deep copy
and I'll explain how that exactly works.
But let's walk through this. We're going to call display shallow,
which is this function right here on line 36.
And this is just a regular function, it's not a member function.
And we're going to pass object1 into it.
Well, we're doing that by value. You can see right here there's no pointer, there's no reference.
It's just by value, so we need to make a copy.
So what we expect is that c++ needs to make a copy,
so it's going to call the copy constructor to do that.
And let's do that right now. So we'll step through it.
You can see right here we're in the copy constructor.
And we're doing that shallow copy.
So at this point, you can see let me just step one more line.
Okay. So what we've got now is let's take a look over here.
We've got source.
Now remember, source is what's being passed in so source in this case is obj1.
So we've got -- so obj1 right here
has a pointer data
that points to an area in memory
and we stored 100 in there.
Now the location of that memory is right there
foxtrot 0 0. So I'll just write that in there those last three hex digits.
So it's foxtrot 0 0, that's where that data is on the heap.
Okay. Now look what happened. I just made a copy of obj1,
but I didn't copy what it's pointing to, I only copied the data.
So if you can see as I expand this, which is right here inside here,
this is the new object we're creating this right there,
you can see that the address of
that pointer data is exactly the same,
right. So what we just did we just created a copy of this.
That's pointing to the same area in memory.
So let's walk through this a couple more steps and then we'll just keep going here.
Great. Now we're here. We've just created the copy.
So what we just did was we created a copy and it's called s,
it's right here. Now s has
a pointer called data. And guess what? It's pointing to the same area in memory as obj1.
So what we just did was,
yeah, sure we made a copy of obj1 absolutely,
but we didn't make a copy of what that data pointer was pointing to.
Ideally, what we would want with a deep copy is for s to be pointing
to its own copy of that data over here,
but that's not what's happening here.
Okay. And what we can see is let me clear the screen real quick,
what we can see here is that if we look at the s right here,
that's that local variable.
You can see that foxtrot 0 0. It's pointing to exactly the same memory location.
So here's the problem. Right now s is just about to go out of scope.
So again, let me draw that one more time just to be very very clear here.
We've got obj1 pointing to this area right here where it's got a 100.
And again that's the data attribute that's pointing,
right.
And then we've got s, which is also pointing to that area.
Well, now s goes out of scope. What happens when s goes out of scope?
We get the destructor called,
right. So let's walk through that so we can see that destructor being called.
It's out of scope right now. Boom, there's the call to the destructor on line 32,
and the destructor deletes data. So what did the destructor just do.
It freed up this area in memory right here.
So as far as the c++ runtime system is concerned,
this is no longer valid memory,
right. We just deleted it.
So this guy right here now is pointing to invalid memory.
Now hopefully,you can see that that's a big problem here. So what's going to happen.
Well, let's keep going.
We're going to destructor freeing the data.
Now we go back to main right here.
And notice where we're at right now. At this point,
this is now gone right because we were -- all that's been cleaned off the stack,
and I'll take those x's off just so you can see what's going on here.
This is the situation we've got. We've got obj1 now
has a pointer called data
that's pointing to an area on the heap that has 100 in it because that hasn't changed,
but that memory is invalid.
So now if we try to make a copy of obj1 and
call it obj2 again, this calls the copy constructor again.
Let's see what happens here. Let's walk through this.
Again, copy constructor gets called because we're making a copy.
And what did we just do? Well, we just copied it again. So now we've got
two objects pointing to invalid data. And I'll walk you through this,
and we're back to main here. So what I've got now is I've got
obj2 right here pointing to that same invalid data.
And that's not good. And you can obviously see what's going on here. You can see obj1
and obj2
are pointing to exactly the same memory locations.
Right here you can see foxtrot 0 0, foxtrot 0 0.
That's the situation we've got right here.
So this could be problematic in a lot of ways.
Suppose that on line 46,
I'm setting the data value for obj2 to 100, right.
So I'm coming this way -- sorry, to 1000. So I'm coming this way
through this area here. I'm trying to put a 1000 in there.
Well, what did I just do assuming that this worked, right,
assuming that we haven't allocated any other objects and they've reused that space.
But what did I just do? I just changed obj-1's data 2 to 1000
indirectly without even knowing it because they're both pointing to the same area in memory.
So let's walk through that, and you'll see exactly what I'm talking about.
Right now, you can see they're both pointing me refresh this here.
Since they're both pointing to the same area in memory,
I just changed ob2's data to a 1000.
But look what happened here with obj1's data,
it changed to a 1000 too, of course, it did. It's -- we're pointing to the same area in memory,
right. Now comes the problem, and this is where the crash comes in, and I'll clear the screen real quick.
As we unwind now and start
freeing up these two objects, right, I mean they're going out of scope right here in main,
so we need to delete the data for both of them, we're calling both the destructors.
Well, that pointer is pointing to invalid data.
So when we try to delete it, we're going to have a problem. That's where the crash comes in.
And I'll just step through it. You can see the destructors being unwound now.
And at some point, we will crash.
There's the crash right there.
Okay. So you can see what's happening here.
You can see how important it is to really understand
what's going on with your heap memory when you're using raw pointers.
What we'll do in the next video is we'll go over deep copy and then we'll
do another example of this that we just did
now using deep copy so you can see how that works better.