03-c-model
03-c-model
1
C Model: Memory & Control Flow
Readings: CP:AMA 6.1–6.4, 7.1–7.3, 7.6, Appendix E
The primary goal of this section is to be able to model how C programs execute.
2
Models of Computation: Racket
In CS 135, we modelled the computational behaviour of Racket with
substitutions (the “stepping rules”).
To call (“apply”) a function, all arguments are evaluated to values and
then we substitute the body of the function, replacing the parameters
with the argument values.
(+ 2 (my-sqr x) (+ 3 1)))
=> (+ 2 (my-sqr 4))
=> (+ 2 (* 4 4))
=> (+ 2 16)
=> 18
3
Models of Computation: C
In this course, we model the behaviour of C with two complimentary
mechanisms:
• Control Flow
• Memory
4
Control Flow
Function Calls
Conditionals
Iteration
Control Flow
We use control flow to model how programs are executed.
6
Control Flow
In hardware, the location (register) is known as the program counter or
instruction pointer and contains the memory address of the current
instruction.
7
Control Flow
In this course, we explore three types of control flow:
• Function Calls
• Conditionals (i.e., if-statements)
• Iteration (i.e., loops)
8
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo(int x) {
return 2 * x + bar(x);
}
int main(void) {
int result = foo(2);
}
9
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo(int x) {
return 2 * x + bar(x);
}
int main(void) {
► int result = foo(2); // C program start at entry point
} // (main-function)
10
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo(int x) {
return 2 * x + bar(x);
}
int main(void) {
int result =►foo(2); // Evaluate RHS before executing
} // assignment operator
11
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo( 2 ) { // x: 2
► return 2 * 2 + bar(2); // foo (callee) is called
}
int main(void) {
int result = foo(2); // main (caller) calls foo
}
12
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo( 2 ) { // x: 2
return►2 * 2 + bar(2); // executing statement left-to-right
}
int main(void) {
int result = foo(2);
}
13
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo( 2 ) { // x: 2
return 4 + ►bar(2); // foo calls bar
}
int main(void) {
int result = foo(2);
}
14
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar( 2 ) { // x: 2
► return 2 + 1;
}
int foo( 2 ) { // x: 2
return 4 + bar(2);
}
int main(void) {
int result = foo(2);
}
15
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar( 2 ) { // x: 2
return►2 + 1; // Evaluate RHS before returning
}
int foo( 2 ) { // x: 2
return 4 + bar(2);
}
int main(void) {
int result = foo(2);
}
16
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar( 2 ) { // x: 2
►return 3; // bar returns value 3 and with that
} // the control flow back to foo
int foo( 2 ) { // x: 2
return 4 + bar(2);
}
int main(void) {
int result = foo(2);
}
17
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo( 2 ) { // x: 2
return ►4 + 3; // Evaluate RHS before returning
}
int main(void) {
int result = foo(2);
}
18
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo( 2 ) { // x: 2
►return 7; // foo returns value 7 and with that
} // the control flow back to main
int main(void) {
int result = foo(2);
}
19
Control Flow: Function Calls
When a function is called, the program location “jumps” to the start of
the function. The return keyword “returns” the location back to the
calling function.
int bar(int x) {
return x + 1;
}
int foo(int x) {
return 2 * x + bar(x);
}
int main(void) {
int result =►7; // main continues
}
20
Function Calls: Return
The control flow statement return changes the program location to go
back to the most recent calling function: the control flow returns from the
callee back to the caller.
if (predicate)
statement
if (n < 0) {
printf("n is less than zero\n");
}
Remember: the if statement does not yield a value. It only controls the flow of
execution.
22
Conditionals: if
The if-statement only affects whether the next statement is executed. To
conditionally execute more than a single statement, braces ({}) are used
to create a compound statement block (a sequence of statements):
if (predicate) {
compound_statement
}
For example:
if (n < 0) {
printf("n was less than zero\n");
n = -n;
}
23
Conditionals: if
Using braces is strongly recommended even if there is only one statement. It makes
the code easier to follow and less error prone.
(In the notes, we sometimes omit them, but only to save space.)
if (n < 0) {
printf("n is less than zero\n");
}
24
Conditionals: if – else
Braces are sometimes necessary to avoid a “dangling” else.
if (y > 0)
if (y != 7) printf("You lose");
else
printf("You win!"); // When does this print?
25
Conditionals: if
if (predicate) { stmnt may or may not be
compound_statement executed.
}
26
Conditionals: if – else
if (predicate) { Exactly one of stmnt_t or
compound_statement stmnt_e will be executed.
} else {
compound_statement
}
printf("input is ");
if (input % 2 == 0) {
printf("even\n");
} else {
printf("odd\n");
}
// ...
27
Conditionals: if – else if
if (predicate) { One or none of stmnt_1 ,
compound_statement stmnt_2, or stmnt_n will be
} else if (predicate) {
compound_statement executed.
}
int sign = 0;
if (input < 0) {
sign = -1;
} else if (input > 0) {
sign = 1;
}
// ...
28
Conditionals: if – else if – else
if (predicate) { Exactly one of stmnt_1 ,
compound_statement stmnt_2, stmnt_n , or stmnt_e
} else if (predicate) {
compound_statement will be executed.
} else {
compound_statement
}
printf("input is ");
if (sign == 1) {
printf("positive\n");
} else if (sign == -1) {
printf("negative\n");
} else { // if (sign == 0)
printf("zero\n");
}
// ...
29
Conditionals: if – else
If there is a return statement within an if condition, there may be no
need for an else.
int sum(int k) {
if (k <= 0) {
return 0;
} else {
return k + sum(k - 1);
}
}
30
Conditionals: switch
The C switch control flow statement (see CP:AMA 5.3) has a similar structure to
else if and cond, but very different behaviour.
A switch statement has “fall-through” behaviour where more than one branch can be
executed.
31
Conditionals: goto
The C goto control flow statement (CP:AMA 6.4) is one of the most disparaged
language features in the history of computer science because it encourages writing
“spaghetti code”: code that is hard to understand.
Modern opinions have tempered and most agree it is useful and appropriate in some
circumstances.
if (k < 0) {
goto mylabel;
}
//...
mylabel:
32
Types of Control Flow: Iteration
With mutation, we can control flow with a method known as iteration:
while (predicate)
statement
while (predicate) {
compound_statement
}
Like with if, you should always use braces ({}), even if the loop body only consists
of a single statement.
33
Iteration: while vs. if
void countdown(int start) {
assert(start > 0);
while (start >= 0) {
printf("%d...\n", start);
--start;
}
}
while (predicate) {
compound_statement
}
if (predicate) {
compound_statement
}
34
Iteration: Procedural vs. Recursive Implementation
Using a loop to solve a problem is called iteration.
35
Iteration: Procedural vs. Recursive Implementation
When first learning to write loops, you may find that your code is similar
to using accumulative recursion:
36
Iteration: Procedural vs. Recursive Implementation
When first learning to write loops, you may find that your code is similar
to using accumulative recursion:
• Initializing the accumulator
37
Iteration: Procedural vs. Recursive Implementation
When first learning to write loops, you may find that your code is similar
to using accumulative recursion:
• Updating / mutating the accumulator
38
Iteration: Procedural vs. Recursive Implementation
When first learning to write loops, you may find that your code is similar
to using accumulative recursion:
• Reversed base condition and break condition!
39
Iteration: Nested loops
Loops can be nested.
40
Iteration: Nested loops
While it is okay to use identifier names like i and j, as long as it is clear
what they refer to conceptually, it is oftentimes better to use more
meaningful ones:
41
Iteration: Common while Errors
A simple mistake with while can cause an “endless loop” or “infinite
loop”. Each of the following examples cause an endless loop.
42
Iteration: do ... while
The do control flow statement is similar to while.
do {
compound_statement
} while (predicate);
43
Iteration: do ... while
A do ... while loop executes a block of code and then either repeats
the block or exits the loop depending on a predicate.
stmnt_a stmnt_a
do { while (pred?) {
stmnt_l stmnt_l
} while (pred?); };
stmnt_z stmnt_z
44
Iteration: break
The break control flow statement is useful when you want to exit from
the middle of a loop.
• break immediately terminates the current (innermost) loop.
• break is often used with a (intentionally) infinite loop.
break only terminates loops. You cannot break out of a conditional, e.g., if-
statement.
45
Iteration: continue
The continue control flow statement skips over the rest of the
statements in the current block ({}) and “continues” with the loop.
46
Iteration: for
The final control flow statement we introduce is for, which is often
referred to as a for-loop.
47
Iteration: for
The format of a while-loop is often of the form:
48
Iteration: for
stmnt_a
for (setup, pred?, update) {
stmnt_l
}
stmnt_z
49
Iteration: for
stmnt_a
for (setup, pred?, update) {
stmnt_l
}
stmnt_z
50
Iteration: for
Most for-loops follow one of these forms (or “idioms”):
// counting up from 1 to n
for (int i = 1; i <= n; ++i) {...}
It is a common mistake to be “off by one” (e.g., using < instead of <=). Sometimes
re-writing as a while is helpful.
51
Iteration: for
In C99, the setup statement can be a variable definition. This is
convenient for defining a variable that only has local (block) scope within
the for-loop:
{
int i = 100;
while (i >= 0) {
printf("%d\n", i);
--i;
}
}
52
Iteration: for
You can omit any of the three components of a for statement.
53
Iteration: for
You can use the comma operator (,) to use more than one expression in the setup and
update statements of a for loop. See CP:AMA 6.3 for more details.
54
Iteration: for vs. while
for (int i = 0; i < n; ++i) { ... }
Use for-loops if
• the number of iterations is known beforehand
• you need to keep tally of your current iteration
Usually, the iterator variable (here: i) is not changed in the body of the
for-loop.
55
Quiz time!
Considering the three for-loops on the right. Which one produces a
different output from the other ones? [Select the most appropriate
answer!]
57
Memory
Structure of Memory
Overflow
Structure of Memory
One bit has two possible states: 0 or 1.
A byte consists of 8 bits. Each byte in memory can represent one of 256
possible states.
For example, if you have 256 Bytes of memory (RAM), the address of the
first byte is 0 and the address of the last byte is 255 (28 − 1).
60
Structure of Memory: Defining Variables
Whenever a variable is defined, C
• reserves sufficient space in memory to store the variable,
• associates the location (i.e., address) of that space with the identifier,
• writes the initial value of the variable to that location (i.e., address).
61
Structure of Memory: Defining Variables
For example, with the definition
int n = 136;
63
Structure of Memory: sizeof
The amount of space required to store data depends on its type.
The size operator (sizeof) yields the number of bytes required to store
a type (it can also be used on identifiers).
int n = 0;
trace_int(sizeof(int)); // int is a type
trace_int(sizeof(n)); // n is an identifier
Size may also depend on the environment (the machine and compiler).
64
Structure of Memory: sizeof
In C, the size of an int depends on the machine (processor) and / or the operating
system that it is running on.
Historically, the size of an int was the word size, but most modern systems use a 32-
bit int to improve compatibility.
In C99, the inttypes module (#include <inttypes.h>) defines many types (e.g.,
int32_t, int8_t) that specify exactly how many bits to use.
In this course, you should only use int, and there are always 32 bits in an int.
65
Structure of Memory: Integer Limits
In this course, the size of an integer is 4 bytes (32 bits).
Because C uses 4 bytes (32 bits) to store an int, there are only 232
(4,294,967,296) possible values that can be represented.
The constants INT_MIN and INT_MAX are defined with those limit
values.
Unsigned integer variables represent the values 0, … , 232 − 1, but we do not use them
in this course.
66
Structure of Memory: Integer Limits
In the read_int function we provide, the value of the constant READ_INT_FAIL is
actually INT_MIN, so the smallest value of int that can be successfully read by our
read_int function is −2,147,483,647 (i.e., INT_MIN + 1).
67
Structure of Memory: Integer Overflow
If we try to represent values outside of the int limits, overflow occurs.
You should never assume what the value of an int will be after an overflow occurs:
the value of an integer that has overflown is undefined.
Remember, do not try to “deduce” what the value of an int will be after
overflow: overflow behaviour is undefined!
69
Structure of Memory: Integer Overflow
Racket can handle arbitrarily large numbers, such as (expt 2 1000).
Why did we not have to worry about overflow in Racket?
Racket does not use a fixed number of bytes to store numbers. Racket represents
numbers with a structure that can use an arbitrary number of bytes (imagine a list of
bytes).
There are C modules available that provide similar features (a popular one is available
at gmplib.org).
70
Quiz time!
If a calculation causes an overflow, what will happen?
71
Quiz time!
Answers will be discussed in class!
72
Additional Types
Characters
Structures
Additional Types: char
The char type is also used to store integers, but C only allocates 1 byte of
storage for a char (an int uses 4 bytes).
There are only 28 = 256 possible values for a char, and the range of
values is (−128, … , 127) in our edX environment.
Because of this limited range, char is rarely used for calculations. As the
name implies, it is often used to store characters.
74
The char Type: ASCII
Early in computing, there was a need to represent text (characters) in
memory.
The only control characters we use in this course are line feed (10), which
is the newline-character \n, and later null (0), which is the null-character
\0.
75
The char Type: Simplified ASCII Table
Invisible Punct. Digits Punct. Upper-case Letters Punct. Lower-case Letters Punct.
0 \0 32 ␣ 48 0 58 : 65 A 78 N 91 [ 97 a 110 n 123 {
33 ! 49 1 59 ; 66 B 79 O 92 \ 98 b 111 o 124 |
34 " 50 2 60 < 67 C 80 P 93 ] 99 c 112 p 125 }
35 # 51 3 61 = 68 D 81 Q 94 ^ 100 d 113 q 126 ~
36 $ 52 4 62 > 69 E 82 R 95 _ 101 e 114 r
37 % 53 5 63 ? 70 F 83 S 96 ` 102 f 115 s
38 & 54 6 64 @ 71 G 84 T 103 g 116 t
39 ' 55 7 72 H 85 U 104 h 117 u
40 ( 56 8 73 I 86 V 105 i 118 v
41 ) 57 9 74 J 87 W 106 j 119 w
10 \n 42 * 75 K 88 X 107 k 120 x
43 + 76 L 89 Y 108 l 121 y
44 , 77 M 90 Z 109 m 122 z
45 -
46 .
47 /
76
The char Type: Unicode
ASCII worked well in English-speaking countries in the early days of computing, but in
today’s international and multicultural environments it is outdated.
The Unicode character set supports more than 100,000 characters from all over the
world.
A popular method of encoding Unicode is the UTF-8 standard, where displayable ASCII
codes use only one byte, but non-ASCII Unicode characters use more bytes.
77
The char Type: Relation to int
In C, single quotes (') are used to indicate an ASCII character.
For example, 'a' is equivalent to 97 and 'z' is 122. C “translates” 'a'
into 97.
> true
Always use single quotes with characters: "a" (string) is not the same as 'a' (char).
78
The char Type: Output
The printf placeholder to display a character is "%c".
79
The char Type: Arithmetic
Because C can interpret characters as integers, characters can be used in
expressions to avoid having “magic numbers” in your code.
80
The char Type: Arithmetic
The order of operation can have influence whether and int or char
overflows. While the code below returns the correct value most of the
time (i.e., in edX), there is no guarantee that it always will. After adding
'a', the variable c could potentially overflow!
81
The char Type: Input
In Section 03, we used the read_int function to read integers from
input.
82
The char Type: Input
Consider the following code, which uses READ_WHITESPACE to read
whitespace:
int main(void) {
while (true) {
char input = read_char(READ_WHITESPACE);
if (input == READ_CHAR_FAIL) {
break;
}
printf("%c", input);
}
}
Input: Output:
> This > This
> is > is
> CS 136! > CS 136!
> >
83
The char Type: Input
Consider the following code, which uses IGNORE_WHITESPACE to ignore
whitespace:
int main(void) {
while (true) {
char input = read_char(IGNORE_WHITESPACE);
if (input == READ_CHAR_FAIL) {
break;
}
printf("%c", input);
}
}
Input: Output:
> This > ThisisCS136!
> is
> CS 136!
>
84
Quiz time!
Assume char c contains a lower-case letter. What is the best way to
correctly convert it to upper case?
A. c -= ' ';
B. c -= 32;
C. c = c + 'A' - 'a';
D. c = c - 'a' + 'A';
E. c = c + 'a' - 'A';
85
Quiz time!
Answers will be discussed in class!
86
Additional Types: Symbols
In C, there is no equivalent to Racket’s 'symbol type. To achieve similar
behaviour in C, you can define a unique constant integer for each
“symbol”.
We have provided some tools for working with C “symbols” on your assignments.
87
Additional Types: Symbols
In C, there are enumerations (enum, CP:AMA 16.5) which allow you to create your
own enum types and help to facilitate defining constants with unique integer values.
After this course, we would expect you to be able to read about enum in a C reference
and understand how to use them.
88
Additional Types: Floating Point Numbers
The C floating point number type can represent real numbers.
float pi = 3.14159f;
float avogadro = 6.022e23f; // 6.022 * 10^23
The f-suffix for defining floats (or more precisely, for disambiguating between (32-bit)
floats and (64-bit) doubles) is not part of the official course material. Therefore, you are
not required to use it and you will not see it in assignments and exams. Using the f-
suffix for floats is, however, the preferred (i.e., correct) way.
C’s float type is similar to inexact numbers in Racket (which appear with an #i prefix in
the teaching languages):
90
Floating Point Numbers: Precision
Unfortunately, floats can be susceptible to accumulating precision errors.
91
Floating Point Numbers: Precision
Floats can also be susceptible to precision errors when using values of
different order of magnitude.
92
Floating Point Numbers: Precision
In the previous two examples, we highlighted the precision errors that
can occur with the float type. C also has a double type that is still
inexact but has significantly better precision.
Assuming that the precision of a double is perfect or “good enough” can be a serious
mistake and introduce errors.
Unless you are explicitly told to use a float or double, you should not use them in
this course.
93
Floating Point Numbers: float vs. double
Historically, C distinguishes between integers and real numbers. Both
types of numbers are represented in different bit-patterns: “two's
complement” for integers, “floating point” for real numbers.
Types of integers in C are historically char and int. The type for real
numbers is float (or single-precision floating point numbers). When
programmers needed higher precision, they introduced double-
precision floating point numbers, or double in short.
Both float and double are floating point numbers, just at different
levels of precision (32 bits vs. 64 bits).
94
Floating Point Numbers: Memory
A double has more precision than a float because it uses more memory.
Floating point numbers and their internal representation are discussed in CS 251 / 230
and in detail in CS 370 / 371.
95
Additional Types: Structures
Structures (compound data) in C are similar to structures in Racket.
Because C is statically typed, each field requires its own type information.
The structure type includes the keyword struct. For example, the type
is struct posn, not just posn. This can be seen in the definition of p
below.
struct posn {
int x;
int y;
};
97
Structures: Size in Memory
The amount of space reserved for a struct is at least the sum of the
sizes of each field, but it may be larger.
struct posn {
int x; // size: 4 bytes
int y; // size: 4 bytes
};
98
Structures: Size in Memory
Always use the sizeof operator to determine the size of a structure.
struct data {
int x; // size: 4 bytes
char c; // size: 1 byte (in memory: 4 bytes, see below)
int y; // size: 4 bytes
};
99
Structures: Size in Memory
The size of a structure may depend on the order of the fields and the compiler
settings
struct s1 { struct s2 {
char c; char c;
int i; char d;
char d; int i;
}; };
trace_int(sizeof(struct s1));
trace_int(sizeof(struct s2));
C may reserve more space for a structure to improve efficiency and enforce alignment
within the structure (a.k.a., padding).
10
0
Structures: Size in Memory
struct data {
int x; // size: 4 bytes, padding: 0 bytes, offset: 0 bytes
char id; // size: 1 byte, padding: 3 bytes, offset: 4 bytes
int y; // size: 4 bytes, padding: 0 bytes, offset: 8 bytes
};
10
1
Structures: Initialization
C99 supports an alternative way to initialize structures:
This prevents you from having to remember the “order” of the fields in the
initialization.
Any omitted fields are automatically zero, which can be useful if there are many fields:
struct posn p = {.x= 3}; // p.y is 0
102
Structures: Fields
Instead of selector functions, C has a field access operator (.), which
“selects” the requested field.
103
Structures: Mutation
The assignment operator can be used with structures to copy all fields
from one structure to another. Individual fields can also be mutated.
104
Structures: Mutation
The braces ({}) are part of the initialization syntax and cannot be used in
assignment. Instead, each field must be mutated separately.
On rare occasions, you may want to define a new struct so you can
mutate all fields of an existing one “all at once”.
105
Structures: Comparators
Comparators (e.g., <, ==) do not work with structures.
106
Structures: Output
printf only works with primitive types and does not work with structures.
To print a structure, you must print each field individually or define your
own print function.
// posn_print(p) prints the content of p.
// effects: writes to the console
void posn_print(const struct posn p) {
printf("The value of p is (%d,%d).\n", p.x, p.y);
}
> p: (3,4)
> The value of p is (3,4).
>
107
Quiz time!
Considering the structure declarations below and select all true
statements regarding struct data d.
108
Quiz time!
Answers will be discussed in class!
109
Sections of Memory
In this course we model five sections (or “regions”) of memory:
Other courses may use alternative names. The heap section is introduced in Section 10.
110
Sections of Memory
Sections are combined into memory segments, which are recognized by the hardware
(processor).
When you try to access memory outside of a segment, a segmentation fault occurs
(more on this in CS 350).
111
Sections of Memory: The Code Section
When you program, you write source code in a text
editor using ASCII characters that are “human
readable”.
Converting source code into machine code is known as compiling. It is briefly discussed
in Section 13 and covered extensively in CS 241.
112
Sections of Memory
Earlier we described how C “reserves space” in memory for a variable
definition, such as,:
int n = 0;
All global variables are placed in either the global constant section or the global
variable section.
113
Sections of Memory: Global Constants
Global constants are available throughout the entire
execution of the program.
114
Sections of Memory: Global Variables
Global variables are available throughout the entire
execution of the program.
Both global memory sections are created and initialized at compile time.
115
Sections of Memory: Local Data
Data with local scope, i.e., local constants, local variables, and function
parameters, must be stored somewhere in memory.
116
Sections of Memory: The Return Address
In addition, a function must know where to return to when it reaches a
return-statement (or, for void-functions, when it reaches the end of
the function).
In other words, the system must “remember” the program location to
“jump back to” when a function returns the control flow to the caller.
1. int inc( 2 ) {
2. ►return 3;
3. }
4.
5. int main(void) {
6. int result = inc(2);
7. }
117
Sections of Memory: Stack Frames
All local data of a function and its return address are stored in a stack
frames.
Each function call generates a stack frame. Each stack frame contains:
• the argument values
• all local data (both variables and constants) that appear within the
function block (including any sub-blocks, e.g., for-loops)
• the return address
If the stack grows too large, it can “collide” with other sections of memory. This is
called “stack overflow” and can occur with very deep (or infinite) recursion.
119
Sections of Memory: The Stack Frame
=========================
inc:
1. int inc(int x) { x: 2
2. int result =►x + 1; result: ???
3. return result; r/a: main:7
4. } =========================
5. main:
6. int main(void) { result: ???
7. int result = inc(2); r/a: OS
8. } =========================
In this course, we use the name of the calling function and a line number
(or an arrow) to represent the return address (or short: r/a).
120
Sections of Memory: Stack Frames
As with Racket, before a function can be called, all arguments must be
values.
Whereas space for a global data is reserved before the program begins
execution, space for a local data is only reserved when the function is
called.
When the function returns, the stack frame is removed and effectively
“disappears”.
In C, local variables are known as automatic variables because they are “automatically”
created when needed. There is an auto keyword in C, but it is rarely used.
C makes a copy of each argument value and places the copy in the stack frame. This
is known as the “pass by value”-convention.
121
Stack Frames: Calling a Function
We can now model the entire control flow when a function is called:
• a stack frame is created and stored (“pushed”) in the stack
• a copy of each of argument is placed in the stack frame
• the current program location is placed in the stack frame as the return
address
• the program location is changed to the start of the new function
• the initial values of local data are set when their definition is
encountered
122
Stack Frames: Calling a Function
When a function returns:
• the current program location is changed back to the return address
(which is retrieved from the stack frame)
• the stack frame is removed (“popped”) from the stack
123
Stack frames: Calling a function
The return address is a code location from the calling function.
It has nothing to do with the location of any return statement(s), or if one does not
exist (e.g., a void function).
There is always exactly one return address in a stack frame.
124
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9.
10. int foo(int x) {
11. int b = x / 3;
12. int res = 2 * bar(b) + bat(b);
13. return res;
14. }
15. =========================
16. int main(void) { main:
17. int result =►foo(50); result: ???
18. ... r/a: OS
19. } =========================
125
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b =►x / 3; x: 50
12. int res = 2 * bar(b) + bat(b); b: ???
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
126
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res =►2 * bar(b) + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
127
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res = 2 *►bar(b) + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
128
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4. =========================
5. int bar(int x) { bar:
6. int res =►x * 2; x: 16
7. return res; res: ???
8. } r/a: foo:12
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res = 2 * bar(b) + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
129
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4. =========================
5. int bar(int x) { bar:
6. int res = x * 2; x: 16
7. ►return res; res: 32
8. } r/a: foo:12
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res = 2 * bar(b) + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
130
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res =►2 * 32 + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
131
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res =►64 + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
132
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res = 64 +►bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
133
The Call Stack: Example
1. int bat(int n) {
2. ►return x % 2;
3. }
4.
5. int bar(int x) { =========================
6. int res = x * 2; bat:
7. return res; n: 16
8. } r/a: foo:12
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res = 64 + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
134
The Call Stack: Example
1. int bat(int n) {
2. return►0;
3. }
4.
5. int bar(int x) { =========================
6. int res = x * 2; bat:
7. return res; n: 16
8. } r/a: foo:12
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res = 64 + bat(b); b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
135
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res =►64 + 0; b: 16
13. return res; res: ???
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
136
The Call Stack: Example
1. int bat(int n) {
2. return n % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9. =========================
10. int foo(int x) { foo:
11. int b = x / 3; x: 50
12. int res = 2 * bar(b) + bat(b); b: 16
13. ►return res; res: 64
14. } r/a: main:17
15. =========================
16. int main(void) { main:
17. int result = foo(50); result: ???
18. ... r/a: OS
19. } =========================
137
The Call Stack: Example
1. int bat(int x) {
2. return x % 2;
3. }
4.
5. int bar(int x) {
6. int res = x * 2;
7. return res;
8. }
9.
10. int foo(int x) {
11. int b = x / 3;
12. int res = 2 * bar(b) + bat(b);
13. return res;
14. }
15. =========================
16. int main(void) { main:
17. int result = 64; result: 64
18. ► ... r/a: OS
19. } =========================
138
Memory Snapshots
You may be asked to draw a memory diagram
(including the call stack) at a particular
moment in the code execution.
For example, “draw the memory when line 18
is reached”.
• make sure you show any variables in the global
and read-only sections, separate from the stack
• include all local variables in stack frames,
including definitions that have not yet been
reached (or are incomplete)
• local variables not yet fully initialized have a value
of ???
• you do not have to show any temporary storage
(e.g., intermediate results of an expression)
139
Stack Frames: Temporary Results
When evaluating C expressions, the intermediate results must be
temporarily stored.
Intermediate results and the return value (for non-void functions) are stored in a
temporary memory area, which we are not discussing in this course. This is discussed
further in CS 241.
140
Stack Frames: Uninitialized Memory
In most situations, mutable variables should be initialized, but C allows
you to declare variables without any initialization.
int i;
int global_var = 0;
141
Stack Frames: Uninitialized Memory
A local variable (on the stack ) that is uninitialized has an arbitrary initial
value.
void mystery(void) {
int k;
printf("The value of k is %d\n", k);
}
In the example above, the value of k will likely be a leftover value from a previous stack
frame.
142
Quiz time!
What is the minimum number of distinct elements that would be stored
in the stack frame for the function below?
143
Quiz time!
Answers will be discussed in class!
14
4
Stack Frames: Recursion in C
Now that we understand how stack frames are used, we can see how
recursion works in C.
In C, each recursive call is simply a new stack frame with a separate frame
of reference.
In the following example, we also see the control flow with the if-
statement.
145
Stack Frames: Recursion in C
1. int sum(int n) {
2. if (n == 0) {
3. return 0;
4. } else {
5. return n + sum(n – 1);
6. }
7. }
8.
9. int main(void) { =========================
10. int result =►sum(3); main:
11. ... result: ???
12. } r/a: OS
=========================
146
Stack Frames: Recursion in C
1. int sum(int n) {
2. if►(n == 0) {
3. return 0;
4. } else {
5. return n + sum(n – 1); =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(3); main:
11. ... result: ???
12. } r/a: OS
=========================
147
Stack Frames: Recursion in C
1. int sum(int n) {
2. if (n == 0) {
3. return 0;
4. } else {
5. return n +►sum(n - 1); =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(3); main:
11. ... result: ???
12. } r/a: OS
=========================
148
Stack Frames: Recursion in C
=========================
sum:
n: 1
r/a: sum:5
1. int sum(int n) { =========================
2. if►(n == 0) { sum:
3. return 0; n: 2
4. } else { r/a: sum:5
5. return n + sum(n – 1); =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(3); main:
11. ... result: ???
12. } r/a: OS
=========================
151
Stack Frames: Recursion in C
=========================
sum:
n: 1
r/a: sum:5
1. int sum(int n) { =========================
2. if (n == 0) { sum:
3. return 0; n: 2
4. } else { r/a: sum:5
5. return n +►sum(n - 1); =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(3); main:
11. ... result: ???
12. } r/a: OS
=========================
152
Stack Frames: Recursion in C
=========================
sum:
n: 0
r/a: sum:5
=========================
sum:
n: 1
r/a: sum:5
1. int sum(int n) { =========================
2. if►(n == 0) { sum:
3. return 0; n: 2
4. } else { r/a: sum:5
5. return n + sum(n – 1); =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(3); main:
11. ... result: ???
12. } r/a: OS
=========================
153
Stack Frames: Recursion in C
=========================
sum:
n: 0
r/a: sum:5
=========================
sum:
n: 1
r/a: sum:5
1. int sum(int n) { =========================
2. if (n == 0) { sum:
3. ►return 0; n: 2
4. } else { r/a: sum:5
5. return n + sum(n – 1); =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(2); main:
11. ... result: ???
12. } r/a: OS
=========================
154
Stack Frames: Recursion in C
=========================
sum:
n: 1
r/a: sum:5
1. int sum(int n) { =========================
2. if (n == 0) { sum:
3. return 0; n: 2
4. } else { r/a: sum:5
5. ►return n + 0; =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(2); main:
11. ... result: ???
12. } r/a: OS
=========================
155
Stack Frames: Recursion in C
1. int sum(int n) {
2. if (n == 0) {
3. return 0;
4. } else {
5. ►return n + 3; =========================
6. } sum:
7. } n: 3
8. r/a: main:10
9. int main(void) { =========================
10. int result = sum(2); main:
11. ... result: ???
12. } r/a: OS
=========================
157
Stack Frames: Recursion in C
1. int sum(int n) {
2. if (n == 0) {
3. return 0;
4. } else {
5. return n + sum(n – 1)
6. }
7. }
8.
9. int main(void) { =========================
10. ►int result = 6; main:
11. ... result: ???
12. } r/a: OS
=========================
158
Sections of Memory
So far, we have introduced four of the five sections of memory:
• Code
• Read-only data
• Global data
• Heap ✘
• Stack
160
End of the Session
161