What's the difference between a class and a struct?
Under the hood, there's no difference! Anything you can do with a class you can do with a struct.
But, there are a few things that are easier with classes:
- inheritance—classes can build on other classes to extend functionality without duplicating code
- access control—struct members can be accessed or overwritten anywhere; access to class members can be restricted at compile time
There are three types of access controls for C++ class members.
- Public members can be accessed anywhere.
- Private members can only be accessed within the class definition.
- Protected members can be accessed within the class definition and in the class definition for any inheriting class.
If you're not sure which one to use, go with a class—it's more common and kind of the default choice for C++ projects. But, if you're sure you don't need either of those features, structs may work too.
Let's check out some examples.
First, say we wanted to represent a credit card. Should we use a class or a struct?
- Do we need inheritance? Maybe not now. But in the future we might expand to have different classes for different types of cards.
- Do we need access control? Yes—we'll want to restrict access to private information, like credit card numbers.
Here's what our class could look like:
Now, say we wanted to represent a grocery list. Class or struct?
- Do we need inheritance? Probably not. It's hard to see how we would extend this class in the future.
- What about access control? Not really—there's nothing really private there. (Unless we're cheating on a diet!)
So, let's use a struct, like this:
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 functions to manipulate the list of stops and print out the bus information.
What do you think happens when we run this code?
Okay, so what happens?
Is the express bus a shallow copy of the local bus? Or a deep copy?
Since we haven't specified otherwise, C++ will make a shallow copy of the local bus when creating the express bus. That means that both buses reference the same route list, like this:
Since they share the same route list, our route change for the express bus shows up in the local bus too. Oops.
We can avoid this if we use deep copying to make sure that the express and local buses each have their own copy of the route list.
Okay, so how can we get deep copies?
There are two ways an object can be copied: the assignment operator (Bus express = local) and the copy constructor (Bus express(local)). If a class doesn't implement functions specifying how these copy cases should be handled, then the C++ compiler fills them in with functions that make shallow copies.
Since we want deep copies, we need to implement 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>(*obj.routeList_); creates new space so that the new Bus object can have its own route list.
Now, if we do something like this:
Then, our buses will look like this:
Notice how, in that code, we used the Bus constructor to make our copy—Bus express(local). But, in our original code, we used the assignment operator:
These are different! Even though we've created a copy constructor, it won't get called when we use the assignment operator like this. So, express is still a shallow copy local, since we haven't said otherwise.
To fix this, let's override the assignment operator by adding our own definition to the class. We'll just have our assignment operator call the copy constructor we've already made:
One last thing to note: copy constructors don't always create deep copies. 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:
Technically, this isn't a deep copy since the two buses will still share a reference.
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 ints, 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 function 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 function—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 functions, 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 functions. When a class function 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() function. Since we'll be overriding the eat function in inheriting classes, we need to declare it as virtual, like this:
When developing classes, most public or protected functions will be declared virtual so that inheriting classes can override them as needed. In fact, the only function 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 function 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.