See reading notes for Chapter 1 for discussion of the purpose of these reading notes.
Chapter 5 deals with functions. Understanding how functions work is a key concept that you need to take with you from this course.
Crucial Material: The following sections are crucial to your success in this course. Material from these sections will be useful not only in this course, but also in later courses as well. It definitely be on assignments and exams.
Important Material: The following sections contain concepts and terminology that are important enough that they might appear on a quiz or exam, but are not as important as the information in the "Crucial Material" list.
Application Programs AP5.1, AP5.2 and AP5.3.
I strongly encouage you to read through these sections to help you see the "big picture" of how larger programs are built up from smaller pieces. The authors take you through the development of three moderately sized C programs, each of which solves a non-trivial problem. If you don't spend some time with each of these programs, you are cheating yourself out of a valuable and important opportunity, one that will definitely improve your chances of success in this course, and with programming in general beyond this course.
A C program, at its core, its basically
a collection of functions. There is one function, called main,
which is where execution starts. Then the main function calls other functions,
each of which has a specific job to
do. These functions may, in turn, call other functions. At every moment in
time, some function is executing.
Some functions are ones that you, as the programmer must write yourself. This
nearly always includes the main function, as may include other
programmer defined functions as well. While it is often the case that programs
written by beginners have no programmer defined functions other than main,
this is considered a very poor programming style. The introduction to Chapter 5
is one of the best explanations I've ever read of why this is the case. The
analogy with building a wooden bridge out of a single (very large) log vs.
out of multiple boards is an excellent one, and I encourage you to read and
understand this analogy in detail.
So in Chapter 5, you'll learn about how to write your own programmer defined functions.
Other functions are ones that are already provided for you as part of the
libraries that come with the C language. In fact, from your very first program,
you have been using the printf function. You may not have known
it at the time, but printf and scanf are functions.
When you write printf("Hello World\n"); you are
calling a function that is defined inside the stdio.h include file. Other functions
include sqrt, sin, cos, etc. which were
discussed back in Chapter 3 of your text (Section 3.7). These come from the
include file math.h (i.e., from the math library.)
Note the following notation: it is traditional among Unix programmers to write function names with a set of empty parentheses after them, for example printf().
This set of empty parentheses would not necessarily ever appear in actual C code (unless we are calling a void function, something you'll read about later in the chapter.)
Instead, the empty parentheses are just a way of signalling the reader that what is being referred to is the name of a function.
So, it is more in keeping with Unix tradition to refer to printf(), scanf(), sqrt(), sin() and cos() as library functions, rather than to refer to printf, scanf, sqrt, etc. We also refer to the "main function" as main() (this saves us have to write the word "function"). From now on, I'll expect you to be comfortable with that notation.
By the way, we can use the notation main() to refer to the main function (sometimes also called the "main program") regardless of what the actual first line of the main program actually looks like. There are a limited number of possibilties for that line; here is a list that I think is fairly complete. By the end of this set of reading notes, you should be able to explain, at least to some extent, what the meaning of each of these notations is, and why one might choose one over the other.
int main()
int main(void)
int main(int argc, char **argv)
int main(int argc, char *argv[])
void main()
void main(void)
void main(int argc, char **argv)
void main(int argc, char *argv[])
(Actually, the difference between char **argv and char *argv[] is something we won't really get to until Chapter 7, but its a difference that doesn't amount to much anyway; both can be used interchangably.)
When the return type of main is "int", then you need a return value inside main, usually either return (0) to indicate success, or return (-1) to indicate a failure. This return value can be used to communicate some "status" information back to the operating system. Many programs don't use this mechanism. Nevertheless, it is a good programming habit to develop the "(0) means success, (-1) means failure" convention.
When the return type is "void", this means that any "return" statements inside main must be plain old "return", not "return 0" or "return 1". Some newer compilers will complain if you use this form, since it is no longer considered good style. This is one of the few areas in which the Tan/D'Orazio text, with its 1999 copyright date, is a bit behind the times.
int argc refers to the number of command line parameters.
char *argv[] or char **argv[] refers to an
array of character strings, each of which is one of the command line
parameters.
Each command line parameter can be either used as a string, converted to an int using atoi(), or converted to a double using atof(). (In spite of the name atof, or "ascii to float", the atof function returns a "double". Go figure.)
Your instructor may tell you more about int argc, char **argv in lecture or in one of the lab exercises.
This section focuses on functions that do not return a value, i.e. have void
as their return type. Such functions are typically used to generate output.
There are several important concepts in this section. One that deserves a bit
of extra comment is the issue of function prototypes, where I want to share
a few things that are not in your text, and in one case, to disagree (rather
strongly) with one of the statments of the authors.
Before a
function can be called inside another function (including main()), that function
must be declared. If the entire function definition (that is, the function
header and the function body) appears before the function call, then a
function prototype is not needed. So, if we list all the functions called
from main()
first, and then put the definition of main() last in the file, we might be
able to avoid function prototypes completely.
I say might, because of situations like the following example:
> cat 5.1.example.c
/* Example for section 5.1, P. Conrad */
#include <stdio.h>
void printLine(void);
void printHeader(void)
{
printLine();
printf("Time\n");
printLine();
}
void printLine(void)
{
printf("-----\n");
}
void printTimes(void)
{
int i;
for (i=8; i<=17; i++)
printf("%02d:00\n",i);
printLine();
}
int main(void)
{
printHeader();
printTimes();
}
>
|
Here's the output:
> make 5.1.example
cc -o 5.1.example 5.1.example.c
> ./5.1.example
-----
Time
-----
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
-----
>
|
Note that in this example, there are no function prototypes for printHeader()
and printTimes(). This is because these functions are not used until after
they are defined. However, a function prototype is required for
the function printLine(). That's because printLine() is called inside printHeader,
before printLine is defined. If we were to rearrange the file so that the
definition of printLine() came before the definition of printHeader(), then
no function prototypes would be required at all.
However, there is nothing
wrong with listing function prototypes for all the functions in a C program.
in fact that is considered by many programmers to be very appropriate style.
That way, you have more flexibility about the order in which you define
your functions. Also, the function prototype list provides a kind of "table
of contents" of your program; look ahead, for example, at the list of function
prototypes at the top of the program listing on page 332.
On page 276, the authors make a statement that is technically true from
the standpoint of the C programming language definition, but from a style
standpoint is something that I most strongly disagree
with.
The authors correctly state that in a function prototype, ANSI C does not
require the argument names to be specified. So, for the
function prototype:
void function2(int n, double x);
void function2(int, double);
Equally acceptable to the compiler perhaps, but NOT from the standpoint
of good style! It is here that I depart from Tan and D'Orazio for a
brief sermon on this point.
In fact, I feel so strongly about this issue
that I have a policy of a mandatory minimum sentence of a 10% deduction
from any
lab, assignment,
exam answer, or project containing such an abominable piece of awful
code.
Why do I feel so strongly? Here's why.
First, you need to understand that as programmers, we sometimes have access
to the function prototype, but NOT to the function definition. This is
the case, for example, with functions such as the sqrt() function in the
math library. The actual source code for sqrt has already been compiled
and stored in binary form. The file math.h provides the prototype:
double sqrt(double x);
but we have no access to the actual function header and body for sqrt.
With that in mind, consider a case where we have access to a library of functions for doing simple geometric calculations. We are provided a list of function prototypes, and a binary file for the library. Which of the following (partial) lists of function prototypes is more useful; the one on the left (without variables) or the one on the right (with variables) and why?
| double areaSquare(double); | double areaSquare(double side); |
| double areaCircle(double); | double areaCircle(double radius); |
| double volumeCylinder(double, double) | double volumeCylinder(double radius, double height); |
Consider especially the function: volumeOfCylinder(). In the list on the
left, we only know that we are to pass in two
double values. The list on the right tells us that the first value is
the radius, and the second value is the height.
Even "r" and "h" would
give us a pretty good idea! How are we to know from the function prototype
on the left whether the parameters are "radius, height", or "height,
radius", or for that matter, "diameter, height"?
Consider another example:
double windChill(double, double); |
double windChill(double windMph,
double tempF); |
In the first place, extra documentation is needed to let a programmer know how to use the function. In the second case, the function prototype tells us nearly everything we need to know about how to use the function: the order of parameter, and even the units (note that a wind chill formula designed for Celsius and Km/hour will not produce correct results for Farenheit and Miles/hour.)
Consider all of the following facts,
Given these facts, is their any excuse for not including varaibles in a function prototype?
To summarize: writing a C or C++ function prototype without variables is an example of programmer laziness deserving nothing but utter contempt. I invite you to join me in a crusade to eradicate this damnable practice within our lifetime.
Item 18 on p. 280 is a very important point. I will often include exam questions that test your understanding of this crucial point.
This section deals with functions that return just one value. This is actually literally true of all functions in C. In Lesson 5.4, the authors introduce functions that "return" more than one value, but they place the word "return" in quotes. This is because a function can be made to appear to return more than one value (through the passing of addresses/pointers) though in reality in can never truly "return" more than one.
Exercise 1d on p. 289 raises an interesting point. It asks, "True or false: The call to an int or double type function must appear on the right hand side of an assignment statement". The answer given is "false." How many other contexts can you think of in which a call to an int or double type function can appear (legally) without being on the right hand side of an assignment statement? Here is an illustration in which there are four calls to programmer defined int and double functions, none of which appears on the right hand side of assignment statements:
/* Example program for Section 5.2, P. Conrad */
#include <stdio.h>
#include <math.h> /* for fabs() */
double recip(double x)
{
return 1.0 / x;
}
int isNearZero(double x)
{
return (fabs(x)<0.0001); /* returns true if x
is within 0.0001 of 0 */
}
double squared(double x)
{
return x * x;
}
int main(void)
{
double x;
printf("Enter value for x: ");
scanf("%lf",&x);
if (isNearZero(x))
printf("x is too close to zero\n");
else
{
printf("1 / (x) = %8.4lf\n", recip(x));
printf("1 / (x*x) = %8.4lf\n", recip(squared(x)));
}
}
|
Here is the output:
> cc -o 5.2.example 5.2.example.c -lm > ./5.2.example Enter value for x: 4 1 / (x) = 0.2500 1 / (x*x) = 0.0625 > ./5.2.example Enter value for x: 2 1 / (x) = 0.5000 1 / (x*x) = 0.2500 > ./5.2.example Enter value for x: 0.000001 x is too close to zero > ./5.2.example Enter value for x: -2 1 / (x) = -0.5000 1 / (x*x) = 0.2500 > ./5.2.example Enter value for x: -0.00001 x is too close to zero > |
Here is an explanation:
isNearZero(x). It occurs
in the expression of an if statement.recip(x),
occurs as an argument to a printf().squared(x).
It occurs
as an argument to the second call to recip().recip(squared(x)) occurs as an argument to printf.Note, however, that these are only the calls to programmer defined functions. In fact there are six other functions calls:
So, we typically write code such as:
printf("%d", x);
which makes it appear that printf() returns void. In fact, printf() returns
an int, but we can treat printf() as if it were returning void. The point that
is
made in item 8 at the top of p. 289 is that the reverse is not valid:
we can't use a void function as if it returned an int or a double.
In this section, among other things, your text tells you about global variables. Global variables are variables declared outside any function, which can therefore be accessed in all functions.
Your text asks you, on p. 291 to consider the question "If you were working
on a long program with a large number of people, each writing different modules
[functions], can you envision any difficulties in declaring variables outside
the body of any function?"
I can answer that question from considerable real-world personal experience.
Global variables create real problems in large software systems. As an example,
I once worked on a large "mission-critical" application for a company that
at the time, was in the top ten of the Fortune 500. By large, I mean
more than 10 million lines of code, and between 50 and 70 programmers
making changes on any given day. By mission critical, I mean that if the application
crashed, this company lost market share by the minute—it was the system used
to enter, ship, and invoice orders, so if it was down, this large manufacturing
company couldn't sell any product. The customers could easily switch to a competitor
if their computers weren't "down", and we knew it.)
Rather often, something would fail in the system, and we would have to track it down. Trouble, the system was built around the idea of a "common block": that is, a 4K (Kilobyte) area full of global variables. Rather than passing parameters between functions, all commucation between functions took place by changing values in this common block.
Between the time you entered an order in the system, and the time that order went through final shipping, several hundred functions were called. Each of those functions had access to change any of the values in this common block. And between 50 and 70 programmers were making changes to the system constantly. In this environment, it was something like a miracle when the system ever worked at all. Consider if there were two variables in that common block called invDate, and invcDate. The first might stand for "inventory date", and the second might stand for "invoice date". Since both variables are visible to all functions in the system, a programmer working on invoicing might accidentally type invDate instead of invcDate. The code would compile, since both variables are global, and hence legal for the programmer to access. This of course would cause complete chaos in the inventory system. Of course he/she wouldn't be testing the inventory code; it might be hours or days before he/she had any idea that something previous operational was now broken.
A programming approach that bans the use of global variables requires communication between functions to only take place through the actual function arguments. This can be inconvenient at times. However, the increase in the clarity and safety of your programs is well worth it. When all communication between one function and another function takes place only through parameters, it is much easier to track down bugs, and isolate the effect of changes in one part of a system on another part of the system.
I strongly discourage the use of global variables. While you are still learning
C programming, avoiding global variables will help you learn good habits insteaed
of developing bad ones.
If you think you need to use global variables to solve a given programming
assignment, lab, or homework assignment, ask your instructor. There is probably
another way to solve the problem that is what she/he intended. On an exam,
think it through yourself; there is very likely another way to solve the problem
without global variables.
In my sections,
using them in
a programming assignment, lab, or exam question will result in major
deductions
of points. So don't do it.
Be sure you understand the concept of "scope". Scope is the "region in which a declaration is active". Page 293 contains a discussion of scope, including block, function, file and prototype scope. Be sure you can identify examples of all four kinds of scope.
Item 5 on page 294 defines "formal" and "actual" parameters. Be sure you know
these definitions. Some programmers and textbooks use the word "parameter"
only for "formal parameters", and use the word "argument" only for "actual
parameters".
However, it is more common to hear the terms "argument" and "parameter"
used interchangably. Therefore the terms "formal parameter" and "actual parameter"
are useful in distinguishing the list of variables in the function definition,
and the list of expressions in the function call.
This section presents the concept that you can write functions that will change the value of the variables in the calling function by:
The concept presented in this secton is called "simulated pass-by-reference". The mechanics of the process are not discussed until the next section (Lesson 5.5). So, in this section try to focus on "what" is happening (i.e. the values of the variables k and z, variables that are local to main(), get changed inside function1()), and don't worry so much about "how" it is happening. The "how" gets covered in gory detail in Lesson 5.5.
This is a very thorough treatment of this topic. I encourage you to read it through carefully.
A few points:
| void function1(int a, int b, double r, double s, int *c, double *t) { ... } | ||||
| variable name | the textbook says the variable type is | most texts would say the type is: | pronounced ths way | or sometime prounced this way |
| c | address to int | int * | "int star" | pointer to int |
| t | address to double | double * | "double star" | pointer to double |
(*c) = 7;would assign the value 7 to the variable k in the main program, while the statement:
(*t)= 23.1;
would assign 23.1 to the variable z in the main program. The variables c and t contain the address of k and z respectively.
Exercise 2 on p. 311 is strange. I am not sure what the textbook authors intend here. I suggest you skip over that exercise for now.
This program calculates the "integral" (area under the curve) of a third order polynomial function using the trapezoid method. If you've taken a calculus course, the topic of this program will be very familiar to you. If not, you should still be able to understand the basic idea of what's happening here if you've had at least two years of high school algebra.
There are several nice things about this example:
This program is a typical program that you might have to solve in a Computer
Science course. And, it has covers many of the aspects of programming that
are needed for game programming; although the program does not involve sophisticated
graphics, as most current games do, it does involve the logic of checking feasible
moves and following rules of play. So working through this example from problem
statement to final solution should be instructive for all CISC105 students.
This example has a very definite engineering bent to it.
For the engineering majors in the course, this is an excellent example
of the type of programming that arises in engineering. One can imagine, for
example, in a mechanical or civil engineering context, needing to calculate
the volume of metal or concrete needed for some part of a machine, or structural
support in a bridge, where the shape of that part is a parallelpiped (a sort
of sloping boxy shape... see the picture on p. 325.)
I would recommend going over this example in detail only if you have had
engineering or mathematics courses that cover vector and matrix operations
(e.g. linear
algebra) and are comfortable with terms such as "dot product", "determinant" and
so forth.
However, even for students without that background, you can still benefit
by
reading through the source code starting on p. 332. Even without a detailed understanding
of the mathematics involved, you can still benefit from looking over the structure
of a large program.
Notice in particular, the main program on p. 332. Trace through how it calls the various functions. Compare this with the diagram on p. 330 (Figure 5.20). This program is a good example of how large programs are assembled with functions, and is a model of good programming practice.
Notice also the use of the comment boxes at the top of each function. These help someone reading the code to navigate through the listing. I encourage you to develop a similar programming style.
Finally, the comments starting on p. 335 are well worth reading. This is particularly true of the discussion of the arguments to scanf(), &*g1, &*g2, etc.
The following exercises are recommended for you as pratice, and are fair game for exam questions (except where noted):
(end of reading notes for Chapter 5).
These reading notes are based on C Programming for Engineering and Computer Science, by H. H. Tan and T. B. D'Orazio, Copyright 1999 by WCB McGraw-Hill. The notes themselves are Copyright 2004, by Phillip T. Conrad, All Rights Reserved.
Prof. Conrad takes sole responsibility for any views or opinions expressed in these notes. Such view and opinions do necessarily reflect those of the University of Delaware, its Department of Computer and Information Sciences, or any other organization.