What is the difference between a shallow and a deep copy of a class?
Shallow copying is faster, but it's "lazy" in how it handles pointers and references. Instead of making a fresh copy of the actual data the pointer points to, it just copies over the pointer value. So, both the original and the copy will have pointers that reference the same underlying data. (Ditto for references.)
Deep copying actually clones the underlying data; it's not shared between the original and the copy.
Let's get specific. Here's a class representing a bus:
Pay close attention to our routeList_ variable. We've made it a pointer so that it can be updated remotely (in case there's a detour). We've also included methods to manipulate the list of stops and print out the bus information.
What do you think happens when we run this code?
Here's the output:
Oops. Looks like the local and express bus were both sharing the same route list. Like this:
Okay, so when we created the express bus (Bus express = local), we made a shallow copy, where both objects ended up with a pointer to the same route list.
We want the express to be a deep copy of the local bus, so that they each have their own route list.
How can we get deep copies?
Side note: Although learning the answers to these common C++ questions is important, it's so much more important to be able to quickly solve C++ problems you've never seen before. Interviewers want to see that you can do more than just memorize facts!
If you really want to take your prep to the next level, and learn the right way of thinking to quickly solve new problems, check out our free 7-day email course:
Cool, watch your inbox!
Well, what happens when we run this line of code:
In C++, there are two functions that copy the values from one object to another: copy constructors and copy assignment operators.
The copy constructor gets called whenever we are constructing a new object from an existing one. So, this declaration uses copy constructors:
And, this code block also uses the copy constructor (even though there's an equals sign):
The copy assignment operator gets called when we're updating an existing object:
Let's look back at the line where we created our express bus. Would it call the copy constructor or the copy assignment operator?
Since we're creating the express bus, we'll use the copy constructor here.
Wait a second though. We didn't ever define a copy constructor! Where's the function that's getting called?
When writing a class, defining a copy constructor and a copy assignment operator is optional. If you don't specify one, then the C++ compiler will generate it for you, filling in a method that makes shallow copies.
If we want deep copies, we can get them be defining a copy constructor and assignment operator for our Bus class. Here's what our copy constructor could look like:
The line of code: routeList_(new vector<string>(*src.routeList_)) creates new space so that the new Bus object can have its own route list.
Now, when we create our express bus, it'll call the copy constructor we just wrote:
Under the hood, our buses will look like this:
That covers the copy constructor. What about the copy assignment operator?
We probably want to define that too. Otherwise, we'll get deep copies whenever we use the copy constructor but shallow copies whenever we use the copy assignment operator. That sounds like a surefire way to get some bugs.
So, let's write out a copy assignment operator that will make deep copies. It'll look a lot like the copy constructor, except we'll want to clean up the existing object before we start assigning new values. Like this:
One last thing to note: copy constructors don't have to create deep copies. Sometimes, we want a separate copy of some of the class members and a shared reference for others.
For instance, if we add information about the bus supervisor, we'll probably want the same supervisor for all buses. So, we can let them share a reference to the supervisor name, while creating a separate copy of the route list:
What is a template function?
C++ makes us specify the types for function arguments and return values. Templates let us write functions that take in any type. This helps us keep our code DRY.
For example, let’s look at this swap function. Sometimes, we need to swap two integers:
But what if we wanted to swap two chars?
Or two strings?
Aside from the types, these functions are exactly the same! That's annoying—it means that our code is harder to read and there are more places for us to introduce bugs. It would be nice if we could write one function to swap two things of the same type that we could use instead of these three versions.
Template functions to the rescue!
Using templates, we can combine all three swap functions above, like this:
So now we can write code like this:
Careful: C++ templates can make programs "bloated" by increasing the amount of code that needs to be compiled. Remember the three different swap functions we created earlier? Behind the scenes, the C++ compiler is generating all three of those functions: one for integers, one for strings, and one for characters. Using templates saves us time and makes our code shorter, but we're definitely not saving any space.
What is the Diamond problem? How can we get around it?
The diamond problem is a common question used to test your understanding of multiple inheritance.
The diamond problem refers to an issue when a base class is inherited by two subclasses, and both subclasses are inherited by a fourth class. When this happens, we need to give the compiler a bit of guidance about the exact structure of inheritance we want.
Let’s use the example of lions, tigers, and ... ligers:
We'll start with a base class, Mammal, with the method eat:
Lion and Tiger both inherit from Mammal:
Finally, Liger inherits from both Lion and Tiger:
This is the inheritance structure we're going for. (See how it looks like a diamond!)
Satisfied with our structure so far, we decide to write some code using the Liger class:
But, when we compile it, we get this error message:
What's happening here?
The compiler is complaining that the Liger class has two versions of the eat method—one from the Tiger class (which inherited it from the Mammal class) and one from the Lion class (which also inherited it from the Mammal class). So, the compiler sees two different eat methods, and it doesn't know which version it should use for Ligers.
This happens because C++ doesn't recognize that the Lion and Tiger classes are inheriting from the same Mammal class. What it sees instead is something like this:
We can ensure that the compiler inherits the same Mammal class into the Lion and Tiger classes with the virtual keyword, like this:
When a base class is virtually inherited, the C++ compiler makes sure that this class is created only once. That means that all subclasses, like Lion and Tiger, will lead back to the same Mammal base class, giving us the diamond structure we've wanted all along.
Note that we have to put the virtual keyword in the definition of both Tiger and Lion. If we don't then we'll get the incorrect structure we had before.
The virtual keyword can also be used with class methods. When a class method is declared virtual, it can be overridden by inheriting classes.
Going back to our example with animals, we might want to let different mammals define their own eat method. Since we'll be overriding the eat method in inheriting classes, we need to declare it as virtual, like this:
When developing classes, most public or protected methods will be declared virtual so that inheriting classes can override them as needed. In fact, the only method that can't be declared virtual is the class constructor. (Clearly, we don't want others to override that!)
Why are destructors important?
Destructors are important because they give us a chance to free up an object's allocated resources before the object goes out of scope. Since C++ doesn't have a garbage collector, resources that we don't free ourselves will never be released back to the system, eventually making things grind to a halt.
A resource leak is when a program finishes using some resource (allocated memory, open files, network sockets) but doesn’t properly release it, hoarding resources it's not using any more. Mozilla was famous for having memory leaks with Firefox: the browser would cache recently visited sites so that users could quickly go back to them, but it wouldn't delete saved sites. Over time, the amount of data saved would get pretty large, slowing down the browser.
We're focusing on C++98. We should note, though, that newer versions of C++ have introduced "smart pointers," which are automatically freed when they go out of scope. If you can use smart pointers, they're generally preferable since you can avoid some of the headaches of manual memory management. That said, many projects still use manual memory management, so it's important to understand destructors.
To make this concrete, let's look at an example: shopping carts. Suppose our shopping cart class keeps track of its items in a dynamically allocated vector, like this:
A destructor method is declared with the same name as the constructor, with a ~ in front. For our ShoppingCart class, that's ~ShoppingCart. The C++ compiler exclusively reserves this syntax for destructors, so it's important to not use this name for anything else.
The items vector is dynamically allocated and could take up a lot of space, so we need to free it when we're done. How do we do that?
We can just call the vector's destructor!
Careful, though! We don't call destructors directly.
- If we allocated an object dynamically (using new), then we call the object's destructor using the delete keyword.
- For objects on the stack (not allocated using new), the destructor will be called automatically once it is out of scope.
Let's see how this works if we have a few shopping carts.
This shopping cart is allocated on the stack, so its destructor gets called automatically once it goes out of scope.
This shopping cart is dynamically allocated, so we have to manually call its destructor when we're done with it.
Okay, but what should actually go in the destructor for ShoppingCart? Since we dynamically allocated the items vector, we need to use the delete keyword to call its destructor. We'd do that in ~ShoppingCart, like this:
The nice thing about destructors is they centralize object cleanup in one place. As we make the ShoppingCart class more complex, we can always make sure we're not leaking resources by updating our destructor to do any necessary cleanup. That's much easier to maintain than having cleanup code scattered around our code base.
Pro tip: Learn how to avoid the dreaded "whiteboard freeze." In our 7 day email crash course, we'll give you the strategy for quickly breaking down and solving any coding interview question.
Cool, watch your inbox!