swap(&int1, &int2); /* arguments are addresses of 2 integers */
/* this is a call; this code is identified */
/* as the caller or parent */
/* function swap */
/* interchanges two integer values */
/* return value: none */
/* parameters: */
/* *px pointer to an one integer */
/* *py pointer to the other integer */
void swap(int *px, int *py) /* when called or invoked, this code is identified */
/* as the callee or child */
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}
C uses call by value for all parameters. This implies that the function receives copies of all parameters. These parameters are local variables within the function's block.
Therefore, the following code may not do what you expect.
#include <stdio.h>
void increment(int a);
main()
{
int x;
int y;
int larger;
x = 1;
printf("before call, x = %d\n", x);
increment(x);
printf("after call, x = %d\n", x);
return (0);
}
void increment(int a){
a++;
}
The output of this program will be:
before call, x = 1
after call, x = 1
To change the value of the variable, pass a pointer to it.
#include <stdio.h>
void increment(int *a);
main()
{
int x;
int y;
int larger;
x = 1;
printf("before call, x = %d\n", x);
increment(&x);
printf("after call, x = %d\n", x);
return (0);
}
void increment(int *a){
(*a)++;
}
Now, the output will be:
before call, x = 1
after call, x = 2
The address of the variable allows the function to modify the variable pointed to. The language only implements call by value, but we get call by reference functionality.
Any and every function returns a value. The type of this value must be declared in the definition of the function. Here is an example of the type declaration.
int dosomething()
{
/* code here to do something */
}
This function returns an integer to its parent.
Within the function definition, the return value must be set
and/or identified.
This is accomplished with a return
statement.
Here are three examples that all accomplish the same thing.
int dosomething()
{
/* code here to do something */
return 6;
}
int dosomething()
{
int returnvalue;
/* code here to do something */
returnvalue = 6;
return returnvalue;
}
int dosomething()
{
/* code here to do something */
return (2 + 4);
}
Plenty of functions have no value to return to the parent.
This case is called a procedure by many high level
languages.
The return type is void
in this case,
and there will be no return
statement.
return
" from main
As a program is executed, consider what occurs at a less abstract level. The operating system (OS) is running and knows that it is to execute a specific program. The OS does administrative things that work towards running the program.
argv
and argc
).
See more below about the stack.
main
identifies it.
jal main
All this implies that the program to be executed is treated
as a function that is called by the OS.
As a function, it returns a value.
The return value ought to be explictly set by the program.
In C, this may be done by the function exit()
or by using return
.
By convention, a return value of 0 means that the program is exiting without error. Other return values are used to identify error conditions.
Style guidelines say that
all programs should explictly use a return
or exit()
from main
.
Here are two simple examples showing this.
#include <stdio.h>
#define MAX 100
main()
{
int x;
x = 1;
while (x <= MAX) {
printf("%d\n", x);
x++;
}
exit(0);
}
#include <stdio.h>
#define MAX 100
main()
{
int x;
x = 1;
while (x <= MAX) {
printf("%d\n", x);
x++;
}
return 0;
}
A topic difficult for (Java) programmers who only have a vague notion of Java exceptions and their proper use deals with what functions/programs should do when they encounter conditions that indicate an error.
NOTE: The use of the term exception within the Java language is unfortunate for computer architects. It has a definite, and somewhat different meaning when discussed in terms of computer architecture (this class!). Therefore, this short section of notes talks of error conditions, as opposed to exceptions, to attempt to avoid later confusion.
In general, a program should be written such that every possible error condition is covered. Examples of error conditions:
switch
statement,
where the default
case is not defined.
It is optional in C; without it, a program might erroneously continue
execution.
A common implementation of C programs utilizes the return value to return an indication of whether an error was encountered, and if so, what the error was. At an appropriate function, the return value becomes an error code that is used to print an error message, or cause the program to repeat a request for user input, or set variable values in a default way, or possibly exit the program. The appropriate program response to the error condition depends on the program.
The stack is a totally cool data structure (or abstract data type) that is extremely useful to make programs work. A data structure is a way of organizing data. The two major things that are done with a data structure are putting data into it (often called insert), and getting data back out of it (often called delete or remove). A stack is an organization that gets the data back out in the reverse order that the data is put into it. The code that uses the stack data structure (the client) does not have the ability to order the data. The stack implementation enforces the ordering of the data.
Putting data into a stack is an operation called a push. Getting data out of a stack is an operation called a pop.
Here is a little example that represents the data with letters. Assume the following operations are done in the given order:
push a
push b
push c
push d
After these 4 push operations, there will be 4 items in the stack,
a
,
b
,
c
, and
d
.
If the next operation is a pop
,
item d
is retrieved, and it is taken out of the stack.
Yet another pop
retrieves item c
.
Push and pop operations can be done in any order;
the application that uses a stack determines this order.
Theoretically, a stack can never be full,
meaning that a new item can always be pushed onto the stack.
A stack can be empty,
and an implementation will need to do something specific
(like returning an error code), when a pop operation is
requested on an empty stack.
When implemented, a stack utilizes memory for the items pushed onto the stack, and an extra variable that identifies the top of stack. It is also commonly called the stack pointer, usually abbreviated sp. Both push and pop operations work on the item at the top of the stack.
Sometimes, a stack is called a LIFO, where the acronym stands for Last In First Out.
The reason for introducing the stack when discussing the C programming language has to do with the implementation (at the assembly/machine language level) of functions.
As viewed from a high-level language perspective, function calls, returns, parameter passing, and returning values just works. From the assembly language perspective, there is a lot of code to make it work. Nothing is automatically done; it is all explicit. Understanding what must be done and why is important, to be able to implement it in assembly language.
rtn_value1 = fcn1(int x);
a = b + c;
.
.
.
rtn_value1 = fcn1(int x);
y = x + z;
.
.
.
int fcn1 (int local_int) {
k = fcn2(local_int * 2);
return (k - 3);
}
int fcn2 (int local_int2) {
return (local_int2 - 85);
}
This is a sizeable example that does not do much. It could easily be done in a single expression evaluation. The point of this example it to notice the call and return calculations. Here are the steps that occur, in the order that they occur.
fcn1
is called.
Upon the return from fcn1
, the code must
continue with
the assignment of fcn1
's return value,
followed by the statement a = b + c
.
fcn1
is begins execution.
fcn1
calls fcn2
.
Upon the return from fcn1
, the code must
continue with the assignment to k
of fcn2
's
return value.
fcn2
is begins execution.
fcn2
completes, returning a value.
fcn1
.
fcn1
completes, returning a value.
fcn1
.
fcn1
,
but with a different return address;
upon the return from fcn1
, the code must
continue with
the assignment of fcn1
's return value,
followed by the statement y = x + z
.
An important aspect to the call and return deals with return addresses. At the level of a computer executing a program, a call requires two things:
These 2 steps may also be expressed as (and will make more sense once we incorporate the use of a stack into the discussion)
The return from a function requires using the saved and remembered return address as the target of a branch (jump).
A key point here is that the return addresses are used (for returning) in the reverse order that they are saved/remembered. The implication is that a stack is the perfect data structure to use for the saving/use of return addresses. A call pushes the return address onto a stack, then branches to the first instruction within the function. A return pops the top of stack, obtaining the (correct) return address. It is then used as the target of a branch.
Knowing about how calls and returns are implemented at the assembly language level makes the understanding of recursion the same as understanding nested function calls.
A variable that is local to a function is one that resides within the scope of the function. When code not within the function is executing, the local variable does not exist. While the function executes, the local variable does exist, and can be set or used by the function.
Like local variables, (call by value) parameters exist and can be set and used only while the function is executing.
To compiler writers, as well as computer architects, local variables, parameters, and return addresses are all utilized in the same way. Calls and returns order the usage of these things, and they can all be grouped together and used as a set. This set is a single entity called an activation record (AR) or stack frame. An AR consists of the set of variables that may be accessed and modified while a program is executing within the scope of a function.
A stack is the correct data structure to use to hold ARs, where the top element (an AR) within the stack belongs to the currently executing invocation of a function. The use of the stack is slightly more complex than the simplified push and pop operations as described.
When executing, a function call, execution, and return becomes
Please note that I have simplified the steps and ordering a little bit to abstract away some difficulties of the explanation.
If a function contains another call to a function (a nested call), then the nested function would occur during step 5. When the nested invocation returns, step 5 continues. The remainder of the steps are administrative work that allows programs to work.
These notes have now referred to "the stack." Making nested calls (and recursion) work requires the use of a stack. Since our program paradigm presumes (and assumes) that we can nest function calls, every executing program needs access to a stack that can be used for ARs. Therefore, every computer system is set up such that the operating system allocates memory for use by each program of a stack. The use of the stack is such a common operation by executing programs, that many architectures provide hardware support for making stack operations more efficient (fast).
v = a(i);
.
.
int a (int r){
.
.
w = b(j);
.
.
}
int b (int s){
.
.
x = c(k);
.
.
}
int c (int t){
.
.
y = d(l);
.
.
}
int d (int u){
.
.
}
In this code fragment,
a
calls b
,
b
calls c
,
and
c
calls d
.
There are several levels of nested calls.
After
c
calls d
, the stack will appear
something similar to
| |
|-------------------|
| RA within C | <---- <------- Top of Stack
|-------------------| |---- AR for d
| parameter u | -----
|-------------------|
| RA within B | -----
|-------------------| |
| parameter t | |---- AR for c
|-------------------| |
| local variable y | |
|-------------------| |
| local variable l | -----
|-------------------|
| RA within A | -----
|-------------------| |
| parameter s | |---- AR for b
|-------------------| |
| local variable x | |
|-------------------| |
| local variable k | -----
|-------------------|
| RA within main | -----
|-------------------| |
| parameter r | |---- AR for a
|-------------------| |
| local variable w | |
|-------------------| |
| local variable j | -----
|-------------------|
| |
|-------------------|
Once we understand how call and return work for a function, as well as for nested function calls, recursion is just a specialized case of a nested function call. A function contains a call to itself.
When the recursive function is executing, it places data into and uses data from its AR. Each new call (a recursive call) does exactly the steps as a nested call would, such as remembering the return address, and creating (pushing) a new AR to be used by the recursive function invocation.
These notes have discussed the use of the stack. It becomes an excellent way of doing one type of dynamic memory allocation. Memory for various items (such as return addresses, local variables, parameters) is required to be allocated as the program is executing. This must be accomplished while the program is executing because there is no way to know how much memory will be required before that time. As evidence of this, consider a simple recursive function. Each invocation (call) of the function requires a new AR to be allocated. If the number of recursive calls is not known until the program is executing, then the space required cannot be determined until that time.
Dynamic memory allocation can be contrasted with static memory allocation. Static allocation is well defined, and can be done at the time when a program is compiled. The memory space for global variables can be statically allocated.
User-allocated variables
(those created with new
in Java)
are another example that require dynamic allocation of memory space.
Examples of this are lists, trees, and queues.
The amount of memory space required will not be known until the
program is executing.
For these dynamic memory requirements,
a heap is utilized.
It is a large portion of memory set aside for a program to use.
The allocation and deallocation of chunks of this portion of
memory are handled (in C) by library routines
(malloc
and free
).
A program is compiled and assembled. This results in data (probably a file kept somewhere on a disk) that represents all information about what memory must look like for the program (sometimes called a memory image). This contains
Copyright © Karen Miller, 2006 |