University of Michigan at Ann Arbor
Last Edit Date: 01/09/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Amir Kamil, Andrew DeOrio, James Juett, Sofia Saleem, and Saquib Razak. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
In stack-based memory management, activation records are stored in a data structure called a stack.
A stack works just like a stack of pancakes: when a new pancake is made, it is placed on top of the stack, and when a pancake is removed from the stack, it is the top pancake that is taken off. Thus, the last pancake to be made is the first to be eaten, resulting in last-in, first-out (LIFO) behavior.
Activation records are similarly stored: when an activation record is created, it is placed on top of the stack, and the first activation record to be destroyed is the last one that was created.
Take the following code as an example:
1 void bar() { 2 } 3 4 void foo() { 5 bar(); 6 } 7 8 int main() { 9 foo(); 10 }
When the program runs, it begins with main()
function, put main()
in the stack. Then it calls the foo()
function, put foo()
on the top of main()
. After that, the foo()
function calls the bar()
function, put bar()
on the top of foo()
.
When returning values, bar()
function goes first, then foo()
, and finally main()
.
The following picture shows the process.
Stack can also grow downward as the following picture shows.
Here is a more complex example:
1 int plus_one(int x) { 2 return x + 1; 3 } 4 5 int plus_two(int x) { 6 return plus_one(x + 1); 7 } 8 9 int main() { 10 int result = 0; 11 result = plus_one(0); 12 result = plus_two(result); 13 cout << result; // prints 3 14 }
Try it out:
Click here to open in new windowWhen the program runs, it first calls main()
. Initializing int result = 0;
. Then call function plus_one(0)
. After that, plus_one(int x)
return 1
into main()
, so that result = 1
after executing line 11. The process is shown as the following picture.
At line 12, the program calls the function plus_two(in x)
. Since result = 1
at this point, so we have plus_two(1)
. In the plus_two(int x)
function, we call plus_one(x + 1)
function again, which is equivalent to plus_one(2)
. The process is shown as the following picture.
Then plus_one(2)
returns x first (x = 3
), then plus_two(1)
returns x to main()
. Finally main()
assign result = 3
and print it out. The process is shown as the following picture.
The whole process of the program above is shown as the following picture.
In summary, the following steps occur in a function call:
For pass-by-value parameters, the argument expressions are evaluated to obtain their values.
For a pass-by-reference parameter, the corresponding argument expression is evaluated to obtain an object 1 rather than its value.
Note: C++ allows references to const
to bind to values (i.e. rvalues in programming-language terms) rather than objects (lvalues). So a reference of type const int &
can bind to just the value 3, as in const int &ref = 3;
.
The order in which arguments are evaluated is unspecified in C++.
A new and unique activation record is created for the call, with space for the function’s parameters, local variables, and metadata. The activation record is pushed onto the stack.
The parameters are passed, using the corresponding arguments to initialize the parameters. For a pass-by-value parameter, the corresponding argument value is copied into the parameter. For a pass-by-reference parameter, the parameter is initialized as an alias of the argument object.
The body of the called function is run. This transfer of control is often called active flow, since the code actively tells the computer which function to run.
When the called function returns, if the function returns a value, that value replaces the function call in the caller.
The activation record for the called function is destroyed. In simple cases, implementations will generally just leave in memory what is already there, simply marking the memory as no longer in use.
Execution proceeds from the point of the function call in the caller. This transfer of control is often called passive flow, since the code does not explicitly tell the computer which function to run.
Take the following code as an example of pass by reference:
1 void swap(int &x, int &y) { 2 int tmp = x; 3 x = y; 4 y = tmp; 5 } 6 7 int main() { 8 int a = 3; 9 int b = 7; 10 cout << a << ", " << b << endl; // prints 3, 7 11 swap(a, b); 12 cout << a << ", " << b << endl; // prints 7, 3 13 }
Try it out:
Click here to open in new windowThe program begins with main()
. It first initializes a
to 3 and b
to 7 and then print out their values. After that, the program calls function swap(a, b)
.
Notice that the function swap(int &x, int &y)
has x
and y
passed by reference. It first initilalizes a local variable tmp
equal to x
, which is 3. Then performs x = y
. After executing line 3, x
should equal to 7. Then the program performs y = tmp
. After executing line 4, y
should equal to 3. Since the swap()
passes by reference, return is required.
Finally, going back to main()
and print out 7, 3
.
The process is shown as the following picture. Here, the reference parameters are depicted with a dotted line between the names and the objects they reference.