CISC105: Reading Notes
Tan and D'Orazio, Chapter 5

by Phill Conrad, Asst. Professor,
CIS Dept, University of Delaware

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.

What to emphasize in Chapter 5

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.

  1. Chapter introduction (pp 265-267, along with table 5.1 (pp 266-267) and figure 5.2 p. 270). For the exams, you don't need to memorize these tables and figures, but you do need to understand them, and be able to answer questions about them if I "give you" all or part of the table.

    In particular, I want you to be able to explain the analogy between building a wooden bridge and building a C program, and what this has to do with using functions.
  2. All the lessons in Chapter 5 (L5.1, L5.2, L5.3, and L5.4). You simply can't be an effective C programmer without knowing and understanding all of material covered here.

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.

Chapter 5, Tan and D'Orazio (1999)

Introduction to Chapter 5 (p. 265-267)

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.)

Notation: main vs. main(); printf vs. printf()

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.

Possibilities for the definition of main()

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.

Lesson 5.1 Functions That Do Not Return a Value (p. 267-283)

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.

When do you need a function prototype?

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.

A case where I disagree with Tan and D'Orazio strongly:
Variable in function prototypes

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);

they suggest that an "equally acceptable function prototype" is:
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,

  1. Variables, while optional for the function prototype, are absolutely necessary when writing the function definition.
  2. It is almost always the same programmer writing both the function definition and the function prototype.
  3. Cut and paste can take the function header and turn it into a full function prototype (with variables) in a few keystrokes.
  4. With proper choice of variables, full function prototypes provide valuable documenation.

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.

Other important points from Section 5.1

Item 18 on p. 280 is a very important point. I will often include exam questions that test your understanding of this crucial point.

Lesson 5.2: Functions That Return Just One Value (pp. 284-290).

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:

  1. The first call to a programmer defined function is isNearZero(x). It occurs in the expression of an if statement.
  2. The second call to a programmer defined function is the first call to recip(). The call, recip(x), occurs as an argument to a printf().
  3. The third call to a programmer defined function is the call to squared(x). It occurs as an argument to the second call to recip().
  4. The fourth call to a programmer defined function is the second call to recip(). That call, 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:

  1. A call to fabs(), which returns double, and occurs in a boolean (true/false) expression inside a return statement. (Note that "return" is not a function call, but rather a C keyword.)
  2. Four calls to printf(), which occur as standalone statements. As it turns out, printf() returns an int. However, most often, the return value of printf() is ignored.
  3. One call to scanf, which also occurs as a standalone statement.. Like printf(), scanf() returns an int. Typically we ignore the return value of scanf(), however, when using files, we can check the return value of scanf() to determine whether we have reached the end of a file or not.

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.

Lesson 5.3: Scope and Mechanics of Passing Values to Functions (p. 290-297)

A real-world story about global variables

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.

For this course, the use of global variables is prohibited
(unless you are otherwise instructed)

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.

Scope is important

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.

Formal Parameters vs. Actual Parameters

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.

Lesson 5.4 Functions That "Return" More Than One Value (p. 298)

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.

Lesson 5.5: Mechanics of "Returning" More Than One Value from a Function—Addresses and Pointer Variables (p. 302)

This is a very thorough treatment of this topic. I encourage you to read it through carefully.

A few points:

  1. The book refers on p. 306 to the "*" as the "also called the indirection operator". That's absolutely true, however most textbooks and programmers call this the "dereference operator".
  2. In the table on p. 305, the textbook indicates that the type of c and t are "address to int" and "address to double". I kind of like this notation, because it re-enforces the idea that c and t contain addresses.

    However, most textbooks and programmers would probably use a different notation. More commonly, the way to refer to the type of c and t would be as follows:

    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
  3. An important thing to remember about c and t is that what they contain are addresses. They contain addresses because in the function call we use the & operator to find the address of the variables k and z in the main function. Inside the function, we treat the variables c and t as "pointers" to the actual locations of k and z in the main program. Any time we want to "follow the pointer" to the address of k and z, we must use the "dereference operator" (*) in front of c or t. For example, the statement:
    (*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.


  4. Item 5 on p. 307 points out that this whole concept of the & operator and the * operator can be a bit confusing. They offer a visual explanation by way of Figure 5.11. I agree with the authors that a visual explanation is helpful here. However, I must confess, that it took me a while to understand Figure 5.11 (even though I've worked with C pointers for over 20 years!) My hope is that if I add a few words, I might be able to help you decode this table more easily.

    In figure 5.11, the arrow pointing from right to left by the & symbol is intended to show that when you write &k, you are taking the address of the variable k. What is not shown so easily in the figure is where that address ends up, namely in the variable c. (Notice that the address of k, FFF0, is also stored in the variable value column next to c.)

    Also in Figure 5.11, the intent of the authors is to show the * operator (the dereference operator) as taking you from the address FFD8 (which is the contents of the variable t) to the actual location FFD8 in memory (which is the variable called z). That's the arrow that points upward diagonally. Then, the line that points horizontally to the right is part of that same operation, that is, it takes you over to the value of z (which is shown as a blank space in the table.) This raises some questions:

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.

Application Program 5.1: Passing Many Values to a Function and Returning One Value-Integration with the Trapezoidal Rule (Numerical Method Example) (p. 311)

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:

  1. The statement of the problem is short and sweet: just seven lines of text on p. 311.
  2. The program itself is also quite short: just 31 lines, not counting blank lines.
  3. The output from the program (not counting the prompts for input) consists of only a label and single number, e.g. "Area = 191.016000".
  4. The calculation done by the program is something that would be difficult to do by hand (up until now, that was not true of many of the programs we have written.)
  5. In spite of the shortness of the problem statement, the final program, and the ultimate output, the actual development of the algorithm takes three pages to explain. This serves to illustrate that computer programming and software development is not only about writing code. Very often, you need to do quite a bit of planning and analysis with pencil and paper and your brain before sitting down at a computer screen and starting to type.
  6. I also like that the program shows a nice application of functions. The use of a function to calculate a single trapezoid allows that part of the program to be separated from the part that loops through adding up the total area of all the trapezoids.

Application Program 5.2 Using Functions with Complex Loops, Working with Grids, a Logic Example (p. 316)

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.

Application Program 5.3 Modular Program Design-Area of Parallelogram, Volume of Parallelepiped (Numerical Method Example) (p. 325)

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.

Exercises

The following exercises are recommended for you as pratice, and are fair game for exam questions (except where noted):

Application Exercises (p. 338)

 

(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.