It’s useful to have some understanding of how function calls work in C++. Sometimes you may run into bugs or behaviour that can only be made sense of if you understand the details of function calls.
First, lets discuss how C++ organizes the memory a program uses while it’s running. C++ divides a program’s memory into three main regions:
Static memory. This is where global variables (i.e. variables not defined in a code block) and constants are put, i.e. things that, at compile-time, are known to used a fixed amount of memory. The size of static memory doesn’t change as the program runs.
Stack-based memory. Local variables and function calls use a region of memory know as the stack, or call stack. Essentially, when a local variable is defined it is pushed onto the stack. When a local variable disappears, i.e. it goes out of scope, then it is popped off the stack.
Similarly, function calls also use the stack for memory. Every time you call a function information related to the function call is pushed onto the stack, including input parameters. The return-value of the function is pushed onto the stack when the function ends.
Stack-based memory grows and shrinks as your program runs. Happily, it is automatically managed by C++. You never need to explicitly get or give back stack-based memory. It’s all automatically done based on function calls and scopes.
Note
Sometimes local variables are called automatic variables because their creation and deletion is done automatically.
We should note that in C++ a scope is essentially a block of code, i.e. code between a { and }. Local variables declared within a block of code exist only from their definition point until the end of the block, i.e. } roughly means “pop local variables”.
The free store. The free store, also know as the heap, is memory that can be allocated and de-allocated by the programmer while the program is running. In C++, free store memory is managed manually, i.e. the programmer is repsonsible for requesting (i.e. allocating) memory from the free store, and for giving it back when it is no longer needed.
In C++, the new operator is used to allocate free store memory, and delete (and delete[]) is used to de-allocate it.
While that sounds simple, it is very easy to make mistakes managing the free store. For example, memory leaks occur when you allocate memory but forget to de-allocate it when it’s no longer needed. Memory leaks can be difficult to debug because they might not cause noticeable problems until the program has been running for a long time.
C++ provides a number of techniques for simplifying free store memory management, such as object constructors and destructors, and so-called “smart pointers” that de-allocate memory when they go out of scope.
Many newer programming languages, such as Java, provide automatic management of the free store known as garbage collection. A special program called the garbage collector runs in the background and de-allocates unusable memory on the free store. While this significantly simplifies free store memory management, it can cause performance problems with some programs: when the garbage collector runs the main program’s execution is halted.
For the purposes of this discussion, we’ll use these functions:
double safe_divide(double a, double b) {
if (b == 0) error("can't divide by 0");
return a / b;
}
double invert(double x) {
double r = safe_divide(1, x);
cout << "(debug: inverse calculated)\n";
return r;
}
Now consider this code fragment:
int main() {
double x = 10;
cout << "begin ...\n";
double r;
r = invert(x);
cout << "... end\n";
}
Let’s trace through this code in some detail, paying particular attention to how C++ handles memory.
The first thing that happens when main is called is that memory space for x is created in a special region of memory C++ calls the stack. The amount of memory must be enough to hold one double.
Next, 10 is assigned to x.
Next, “begin” is printed.
Next, space to hold r is created on the stack, on “top” of x. Recall that C++ does not give just-defined local variables any default value, so immediately after x is created it has some random value (i.e. whatever value happens to be in the memory location it has been assigned).
Currently, the stack looks like this:
x: 10 stack grows down
r: ? ||
||
\||/
\/
Next, we call this statement:
r = invert(x);
A number of things happen here, so we will step through it in detail.
When we call invert it is pushed onto the stack:
x: 10 stack grows down
r: ? ||
call to invert(x) ||
x: 10 \||/
\/
The parameter to invert is called x, and it is assigned a copy of the other x variable (the one from main). It’s okay to have two different x variables here because one is local to invert, and the other is local to main.
Now r is pushed onto the stack:
x: 10 stack grows down
r: ? ||
call to invert(x) ||
x: 10 \||/
r: ? \/
We don’t know the value of r until safe_divide(1, x) is evaluated, so for now its value is unknown.
After r is pushed, safe_divide(1, x) is called:
x: 10 stack grows down
r: ? ||
call to invert(x) ||
x: 10 \||/
r: ? \/
call to safe_divide(1, x)
a: 1
b: 10
a and b are the parameters for safe_divide. After they are initialized, the code in the body of safe_divide runs, and, as long as b is not 0, a / b is returned.
Returning a value corresponds to popping the stack. When safe_divide returns, everything that was pushed from the call to safe_divide onwards is popped and replaced by the value of a / b:
x: 10 stack grows down
r: ? ||
call to invert(x) ||
x: 10 \||/
r: ? \/
return value: 0.1
Now the flow of control jumps back to double r = safe_divide(1, x); in invert, and safe_divide(1, x) is replace by its return value from the stack. Thus, r gets the value 0.1:
x: 10 stack grows down
r: ? ||
call to invert(x) ||
x: 10 \||/
r: 0.1 \/
Notice that r sat with an unknown value for the entire time that safe_divide was running.
Now that r inside invert has a value, its cout is called. Then it returns r, which causes everything from the call to invert(x) on the stack to be popped and replaced by the return value:
x: 10 stack grows down
r: 0.1 ||
||
\||/
\/
Finally, “end” is printed.
It’s possible that in some cases safe_divide will throw an exception instead of return a value. The behaviour on the stack is quite different. Lets go back to the stack the way it was just after safe_divide was called, and this time lets suppose that x in main was set to 0:
x: 0 stack grows down
r: ? ||
call to invert(x) ||
x: 0 \||/
r: ? \/
call to safe_divide(1, x)
a: 1
b: 0
Since b is 0, the if-statement in safe_divide causes error to be called, which throws a runtime_exception.
When an exception is thrown, the function immediately ends without executing a return. Instead, it looks to see if the code was inside a try/catch block. It’s not, and so the call to safe_divide is popped and replaced (conceptually) by the exception:
x: 0 stack grows down
r: ? ||
call to invert(x) ||
x: 0 \||/
r: ? \/
runtime_exception
Now the program is in invert, but because an exception has been thrown it does not execute normally. Instead, it looks to see if the program is currently inside a try/catch block, and, because it’s not, it pops everything from the call to invert onwards:
x: 0 stack grows down
r: ? ||
runtime_exception ||
\||/
\/
Now we are back to main, and the same thing happens: we check to see if the code is inside a try/catch block, and, because it’s not, the entire stack is popped. This causes main to halt the program with an error.