Memory Model Practice — Passing References
If you closed it
and fork it.
Parameters that are References to the Arguments
Okay, now let's look at the second function, passByRef
:
void passByRef(const int& r) {
cout << "passByRef's r is at address " << &r << ", and holds " << r << endl;
}
Notice that here, the type of the parameter r
is const int&
, which we read as “a reference to a constant int
”.
Terminology: We would say that r
is passed by reference (or, more specifically, passed by read-only reference).
Now we'll replace both our calls of passByVal
in main
to be passByRef
:
int main() {
int x = 23;
int y = 19;
cout << "main's x is at address " << &x << ", and holds " << x << endl;
cout << "main's y is at address " << &y << ", and holds " << y << endl;
passByRef(x);
passByRef(y);
return 0;
}
Rerun the code by hand and figure out what it will print.
Can't I just edit the code, then compile and run it to find out?
That's a tempting shortcut, but by thinking it through first you're building your own skills. Taking shortcuts won't help that process.
Our hand-execution says we should expect output like
main's x is at address s1, and holds 23
main's y is at address s2, and holds 19
passByRef's r is at address s1, and holds 23
passByRef's r is at address s2, and holds 19
Remember that r
ends up just being another name for one of our existing variables. In our model, passByRef
doesn't need any stack space.
Run It for Real
Change the code in the online compiler and run it. You'll see that the result is consistent with our model.
Using the Heap: A Single Item
Now, let's use new
. We'll change our test code to
int main() {
int x = 23;
int y = 19;
cout << "main's x is at address " << &x << ", and holds " << x << endl;
cout << "main's y is at address " << &y << ", and holds " << y << endl;
int* p = new int{54};
cout << "main's p is at address " << &p << ", and holds " << p << endl;
passByRef(*p);
delete p;
return 0;
}
Hand-simulate this code; a memory diagram will help.
I think I can just skip the memory diagram.
It's tempting to take shortcuts and run it in your head, but that approach makes mistakes more likely.
Using diagrams to help you think through tricky bits of your code will help you avoid some bugs completely, and also help you figure out ones that happen anyway, instead of just tinkering with your code, hoping that you'll fix the bug along the way.
Here's one way of giving the answer:
p
was allocated at the start ofmain
p
was initialized at the line withnew
, and then, afterwards,p
held a pointer to anint
on the heap that- was initialized to
54
, and then - was destroyed and deallocated by the call to
delete
.
- was initialized to
p
kept pointing to that deallocated memory afterwards.
p
was destroyed and deallocated at closing curly brace ofmain
.
So
delete
doesn't destroy the variable, it destroys the thing it points to?Yes, and deallocates it, too.
delete
needs a pointer (the one that came fromnew
) to do its work.So how do I destroy a variable? Is it like this?
delete &v;
No–o–o–o!! Named variables live on the stack and are destroyed and deallocated automatically. We only use
delete
for things on the heap—pointers that originally came fromnew
, not pointers to data on the stack.Your code would compile, because
&v
is a pointer anddelete
expects a pointer, but it would have undefined behavior, because you're breaking all the rules.
Hand-simulating this code, we'd produce the following output (I arbitrarily picked h101
for the heap location):
main's x is at address s1, and holds 23
main's y is at address s2, and holds 19
main's p is at address s3, and holds h101
passByRef's r is at address h101, and holds 54
Try Running the Code
You can confirm this result by editing the code in the online compiler to match our example and running it.
Using the Heap: An Array
Now let's use the array version of new
. We'll change our test code to
int main() {
int x = 23;
int y = 19;
cout << "main's x is at address " << &x << ", and holds " << x << endl;
cout << "main's y is at address " << &y << ", and holds " << y << endl;
int* q = new int[2]{13, 29};
cout << "main's q is at address " << &q << ", and holds " << q << endl;
passByRef(*q);
passByRef(*(q + 1));
delete[] q;
return 0;
}
It behaves very similarly. If we hand-simulated a run, it would produce
main's x is at address s1, and holds 23
main's y is at address s2, and holds 19
main's q is at address s3, and holds h120
passByRef's r is at address h120, and holds 13
passByRef's r is at address h121, and holds 29
Hay, do those heap locations for the array need to be right next to each other?
Yes. Arrays occupy contiguous memory.
If we run the code in the online compiler, we'll see similar results. For example,
main's x is at address 0x7ffda41b7408, and holds 23
main's y is at address 0x7ffda41b740c, and holds 19
main's q is at address 0x7ffda41b7410, and holds 0x55d1e6aeb2c0
passByRef's r is at address 0x55d1e6aeb2c0, and holds 13
passByRef's r is at address 0x55d1e6aeb2c4, and holds 29
My numbers aren't exactly the same. Is that okay?
Yes. There is no guarantee that the stack or heap will be in exactly the same place on different runs of a program, even on the same machine.
I notice that the stack looks very far away from the heap—the address is much smaller.
That's right. On many systems, you can easily tell just from what the address looks like whether it's a stack address or a heap address.
(When logged in, completion status appears here.)