Before You Start…
I wanna learn MORE new stuff!!
Sure, but before we do, let's make sure we can remember some of the old stuff.
Review: Using Constructors
Let's remind ourselves about how we use constructors.
But we'll also start thinking a bit about a topic we'll look at more deeply in this lesson: The idea of “temporary objects”.
Suppose we have a LabNotebook
class that supports the idea of a default lab notebook, as well as copying and assigning lab notebooks. How would you create a new default LabNotebook
called myNotes
, allocated on the stack of the current function? Provide the necessary declaration.
To create a default LabNotebook
, we can either say
LabNotebook myNotes;
or
LabNotebook myNotes{};
How about:
LabNotebook myNotes();
No—that would be a declaration of a function called
myNotes
that takes no arguments and returns aLabNotebook
.!!!
If we use curly braces for constructor arguments, we'll never be caught out by this C++ “irregular verb”.
What are all the things that would happen if we'd written
LabNotebook myNotes = LabNotebook{};
instead?
The code above is a call to the copy constructor, and it's pretty much the same as writing
LabNotebook myNotes{LabNotebook{}};
This version creates a redundant temporary (default) LabNotebook
object. We haven't spoken much about temporary objects or drawn them on our memory diagrams, but they're a real thing, and to run this code C++ has to have space for not one but two LabNotebook
s behind-the-scenes—it needs space for myNotes
and also a temporary space to hold the object created by LabNotebook{}
.
Can we make MORE temporary objects? Just for fun?
Sure.
Noooo…
This code would also initialize myNotes
but with lots of redundant object creation and copying:
LabNotebook myNotes{LabNotebook{LabNotebook{LabNotebook{}}}};
We make one temporary default lab notebook with LabNotebook{}
, and then copy the value three times, twice into additional temporary LabNotebook
objects, before finally copying the value from our final temporary object into our newly declared variable myNotes
.
Actually, the answer on real C++ systems may be zero. The compiler is allowed (and sometimes required) to perform “copy elision” to avoid wasting time in redundantly copying data around.
Thanks Bjarne, that's a cool optimization, but in this class we won't worry too much about remembering it.
If you're asked to count the number of copies that happen, count all the ones you can see.
But if you're drawing a memory diagram and want to take a shortcut and skip creating an object in one place only to immediately and unconditionally copy it somewhere else and throw away the original, it's okay to just draw it where you know it will end up—the shortcut is probably what would happen anyway.
But no one will mind if you don't take shortcuts.
Could I throw in MORE
*
s and write it like this?LabNotebook myNotes = *(new LabNotebook{});
Technically, it would compile…
Yes, it would. But no, don't do that! Don't just scatter
*
s into your code to try to make it compile. That code would have a serious problem.Argh! I don't want to even think about this!
Neither do we! Please, folks, never write anything like that.
That code would cause a memory leak, because we'd create a temporary object on the heap and never be able to
delete
it.We'll come back to this piece of code a bit later when we have some more tools in our memory-diagram toolbox and we can trace through its awfulness in detail.
Review: Allocating Things on the Heap
If we wanted to make a new LabNotebook
object on the heap, we could do so by running:
LabNotebook* myNotesPtr = new LabNotebook{};
Review: Iterating Over An Array
Suppose that inside a LabNotebook
, there is a data member that is an array of one hundred LogEntry
objects, declared as
LogEntry entries_[100];
Imagine that we want to print out these log entries, and that there is a printToStream
member function for the LogEntry
class.
One option would be to write the code
for (size_t i = 0; i != 100; ++i) {
entries_[i].printToStream(std::cout);
}
Another option would be to write
for (size_t i = 0; i != 100; ++i) {
(entries_ + i)->printToStream(std::cout);
}
What's the deal with
->
again?Saying
ptr->memberOfSomeKind
is a convenient shorthand for saying(*ptr).memberOfSomeKind
.So the code above could be
(*(entries_ + i)).printToStream(std::cout);
instead, but that's much uglier.
Yes, that is ugly.
There is also another way of writing this loop where our loop variable is a pointer rather than an integer:
Here's how to do it this way:
for (LogEntry* p = entries_; p != entries_+100; ++p) {
p->printToStream(std::cout);
}
This start/past-the-end pointer style is widely used in C++ coding.
In Week 5 there was an optional exercise using this technique, which shows some of the built-in C++ functions that support working with an array using a start pointer and a past-the-end pointer.
Check out the example code on Online GDB:
That was a lot of review!
Yes, it was, but we're going to use some of these concepts as foundations we'll build on in this lesson.
If you're unsure about any of what you've seen here, you can go back to the previous weeks' lessons to refamiliarize yourself.
(When logged in, completion status appears here.)