Module 3: Intermediate C
Functions
Let's start with an example:
(source file)
#include <stdio.h>
// Constants needed for random generation
const long m = 2147483647L;
const long a = 48271L;
const long q = 44488L;
const long r = 3399L;
// A variable used to initialize the generator (should never be 0).
long r_seed = 12345678L;
// A function that returns a random number between 0 and 1.
double uniform ()
{
long t, lo, hi;
double u;
hi = r_seed / q;
lo = r_seed - q * hi;
t = a * lo - r * hi;
if (t > 0)
r_seed = t;
else
r_seed = t + m;
u = (double) r_seed / (double) m ;
return u;
}
int main ()
{
int i;
double u;
// Initialize the generator using some integer between 1 and m.
r_seed = 1;
// Print out the first 10 values.
printf ("Ten random numbers between 0 and 1:\n");
for (i=0; i < 10; i++) {
u = uniform ();
printf (" %lf\n", u);
}
}
Note:
- The function uniform takes no parameters and returns
a double value.
- There are four global constants used by the function
and one global variable.
- The main function initializes the seed and prints
out 10 random values. This is the output:
Ten random numbers between 0 and 1:
0.000022
0.085032
0.601353
0.891611
0.967956
0.189690
0.514976
0.398008
0.262906
0.743512
- Note that the function appears before its use.
- C does not support forward references.
- The following does not work:
int main ()
{
// ...
u = uniform ();
// ...
}
// uniform declared after its use in main.
double uniform ()
{
// ...
}
- However, C lets you specify a function prototype:
(source file)
double uniform ();
int main ()
{
// ...
// Function prototype is used by compiler.
u = uniform ();
// ...
}
// Actual body of function can come later, or in another file.
double uniform ()
{
// ...
}
- Most experts recommend using function prototypes when
multiple files are used.
- To avoid name clashes, some of the constants can be moved
inside the function:
(source file)
double uniform ()
{
// Constants needed for random generation, not needed anywhere else.
const long m = 2147483647L;
const long a = 48271L;
const long q = 44488L;
const long r = 3399L;
// ...
}
Note:
- However, this will create variables on the activation record
each time the function is invoked.
- A better approach is to use static variables:
(source file)
double uniform ()
{
// Constants needed for random generation, not needed anywhere else.
const static long m = 2147483647L;
const static long a = 48271L;
const static long q = 44488L;
const static long r = 3399L;
// ...
}
- This way, there's only one copy of the four variables.
Next, let's add a couple of functions:
- A function that returns a random value in a given range (as
opposed to just the range [0,1]).
- A function to return a random integer in a specified range.
Here's the code:
(source file)
#include <stdio.h>
#include <math.h>
// ...
double uniform ()
{
// ... as before ...
}
// Sets the seed to given value.
void setSeed (long newSeed)
{
r_seed = newSeed;
}
// Returns a random double in the specified range.
double uniform_range (double a, double b)
{
// ... not shown ...
}
// Returns a random int in the specified int range.
int discrete_uniform (int a, int b)
{
// ... not shown ...
}
int main ()
{
int i;
double u;
int d;
setSeed (1);
printf ("Ten random numbers between 10 and 20:\n");
for (i=0; i < 10; i++) {
u = uniform_range (10, 20);
printf (" %lf\n", u);
}
printf ("Ten random integers between 10 and 20:\n");
for (i=0; i < 10; i++) {
d = discrete_uniform (10, 20);
printf (" %d\n", d);
}
}
Note:
- We have added the math.h "include" to access the math
library - the floor function is used in discrete_uniform.
- C does not permit function name overloading. The following
won't work:
// Returns a random double in the specified range.
double uniform (double a, double b)
{
// ...
}
// Returns a random int in the specified int range.
int uniform (int a, int b)
{
// ...
}
In-Class Exercise 3.1:
First, write a function to compute the area of a circle given its
radius. This function should take a
double
(the radius)
as parameter and return a double (the area).
Then, use this function in main to compute the average area of a
circle whose radius is randomly chosen between 0 and 1.
For this exercise, you can download uniform.c, and add
both a main() method,
and your own area-computing method.
Types of function parameters
There are two kinds of function parameters:
- Call-by-value parameters, in which the original values
do not get modified.
- Call-by-reference parameters, in which they do get modified.
All the examples we have seen so far have been
call-by-value.
Let's look at a classic call-by-reference example:
(source file)
void swap (int *first, int *second)
{
int temp = *first;
*first = *second;
*second = temp;
}
int main ()
{
int i = 5, j = 6;
printf ("i=%d j=%d\n", i, j);
// Pass the addresses to i and j.
swap (&i, &j);
printf ("i=%d j=%d\n", i, j);
}
Note:
- The parameters to swap are pointers (addresses).
void swap (int *first, int *second)
- To actually access the data, the pointer dereferencing operator
must be used, e.g.,
*first = *second;
- The call to swap must pass the addresses of the desired target
variables:
// Pass the addresses to i and j.
swap (&i, &j);
In-Class Exercise 3.2:
What happens in each of the following two modifications of the
swap program?
For the first example, print out the addresses from within
swap().
-
void swap (int *first, int *second)
{
int temp = *first;
*first = *second;
*second = temp;
}
int main ()
{
int i = 5, j = 6;
swap (i, j);
printf ("i=%d j=%d\n", i, j);
}
-
void swap2 (int first, int second)
{
int temp = first;
first = second;
second = temp;
}
int main ()
{
int i = 5, j = 6;
swap2 (i, j);
printf ("i=%d j=%d\n", i, j);
}
Global, static, local and parameter variables
There are four types of declarations in C:
- Global: variables declared outside functions.
- Static: variables declared as static inside functions.
- Local: variables declared inside functions.
- Parameter: function parameter variables.
Let's look at an example:
(source file)
int a = 0; // Global - accessible anywhere in file.
void test (int b) // Parameter.
{
static int c = 0; // Static - retains its value over successive function calls.
int d = 0; // Local.
if (b >= 0) {
int e; // Local inside block (at top of block) - ANSI C99 only.
e = a + b + c + d; // a,b,c,d are accessible anywhere in test.
// e is accessible only after this declaration inside the if-block.
printf ("e=%d\n", e);
}
c++;
}
int main ()
{
a++; // "a" is accessible in all functions in the file.
test (1);
test (2);
}
In-Class Exercise 3.3:
Hand-execute the code above and identify what gets printed out.
Math library
C includes a standard math library.
Some examples:
(source file)
#include <stdio.h>
#include <math.h> // Must include math.h header.
// To compile:
// gcc math.c -lm
int main ()
{
double x = 1.5;
double y = 2.0;
printf ("x = %lf ceil(x) = %lf\n", x, ceil(x) ); // 2.0
printf ("x = %lf floor(x) = %lf\n", x, floor(x) ); // 1.0
printf ("x = %lf sqrt(x) = %lf\n", x, sqrt(x) ); // 1.224
printf ("x = %lf exp(x) = %lf\n", x, exp(x) ); // 4.481
printf ("x = %lf log(x) = %lf\n", x, log(x) ); // 0.405
x = -1.5;
printf ("x = %lf fabs(x) = %lf\n", x, fabs(x) ); // 1.5
printf ("x = %lf y = %lf x^y = %lf\n", x, y, pow(x,y) ); // 2.25
}
Screen I/O
Screen input and output in C:
- We have already seen examples of screen output using printf.
- The rules for printf are
somewhat cryptic initially, but are easy to learn.
- Screen input uses scanf.
- Other than simple numbers and text lines, screen input can be
more complicated.
An example:
(source file)
int main ()
{
// Some variables: an int, a double, a char and a string.
int i = 1;
double x = 2.5;
char ch = 'a';
char *str = "hello";
// A char array to hold input lines.
char inputLine [100];
// Print the four variables.
printf ("i = %d x=%lf ch=%c str = %s\n", i, x, ch, str);
// Scanning in numbers:
printf ("Enter an integer followed by a double: ");
scanf ("%d %lf", &i, &x);
printf ("i = %d x = %lf\n", i, x);
// Scanning in a string:
printf ("Enter a string: ");
scanf ("%s", inputLine);
printf ("You entered: %s\n", inputLine);
}
Note:
- Reading from the screen is done using scanf.
- To read "into" variables, scanf requires the addresses
of the variables.
- The same data-type specifiers used for printf are used
for scanf.
- To read a string, you have to create the space for the string
ahead of time. In the above example, we could not have done this:
scanf ("%s", str); // Only 5 chars of space.
- In reading a string, scanf appends the end-of-string
character '\0' to the string.
- Reading is more complicated than writing because you can
specify field information in the scanf format string, e.g.,
int main ()
{
// Some variables: an int, a double, a char and a string.
int i = 1;
double x = 2.5;
// A char array to hold input lines.
char inputLine [100];
// Read only the first two digits of the integer:
printf ("Enter an integer followed by a double: ");
scanf ("%2d %lf", &i, &x);
printf ("i = %d x = %lf\n", i, x);
// This reads only three characters from the string:
printf ("Enter a string: ");
scanf ("%3s", inputLine);
printf ("You entered: %s\n", inputLine);
}
It works if you enter "23 5.6" but see what happens when you enter "234 5.6".
File I/O
Let's modify the above example to also write the data to a file
called "data.txt"
(source file)
int main ()
{
// Some variables: an int, a double, a char and a string.
int i = 1;
double x = 2.5;
char ch = 'a';
char *str = "hello";
// A char array to hold input lines.
char inputLine [100];
// Declare a file pointer. Note the capitalization.
FILE *dataFile;
// Open the file.
dataFile = fopen ("data.txt", "w");
// Print the four variables.
printf ("i = %d x=%lf ch=%c str = %s\n", i, x, ch, str);
fprintf (dataFile, "i = %d x=%lf ch=%c str = %s\n", i, x, ch, str); // Write to file.
// Scanning in numbers:
printf ("Enter an integer followed by a double: ");
scanf ("%d %lf", &i, &x);
printf ("i = %d x = %lf\n", i, x);
fprintf (dataFile, "i = %d x = %lf\n", i, x); // Write to file.
// Scanning in a string:
printf ("Enter a string: ");
scanf ("%s", inputLine);
printf ("You entered: %s\n", inputLine);
fprintf (dataFile, "You entered: %s\n", inputLine); // Write to file.
// Close the file.
fclose (dataFile);
}
Note:
- The example shows how to write to a text file.
- A file (text or otherwise) is opened using fopen():
- The first argument is the name of the file.
- The second is a mode string that must be
one of the following:
"w" - for writing
"r" - for reading
"a" - for appending
"b" - for a binary file.
- ANSI C99 also allows:
"r+" - for reading and writing
"w+" - for reading and writing to a new file
"a+" - for reading and appending
- Both "w" and "w+" create a new file, overwriting a possibly
existing file with the same name.
- Modes can be combined as in:
dataFile = fopen ("blah.txt", "rb");
Next, let's read a text file line by line and write to screen:
(source file)
#define MAX_CHARS_PER_LINE 100
int main ()
{
int lineNumber; // We'll track line numbers.
char inputLine [MAX_CHARS_PER_LINE + 1]; // Need an extra char for string-terminator.
char ch; // Variable into which we'll read each char.
int cursorPosition; // Position in inputLine for current char.
FILE *dataFile; // The file.
// Open the file.
dataFile = fopen ("file.c", "r");
// Initial value - first line will be "1".
lineNumber = 0;
// Initial position of cursor within inputLine array:
cursorPosition = 0;
// Get first character.
ch = fgetc (dataFile);
// Keep reading chars until EOF char is read.
while (ch != EOF) {
// If we haven't seen the end of a line, put char into current inputLine.
if (ch != '\n') {
// Check whether we have exceeded the space allotted.
if (cursorPosition >= MAX_CHARS_PER_LINE) {
// Can't append.
printf ("Input line size exceeded - exiting program\n");
exit (0);
}
// OK, there's space, so append to inputLine.
inputLine[cursorPosition] = ch;
cursorPosition ++;
}
else {
// Need to place a string-terminator because that's not in the file.
inputLine[cursorPosition] = '\0';
// Print.
lineNumber ++;
printf ("Line %4d: %s\n", lineNumber, inputLine);
// Reset cursor.
cursorPosition = 0;
}
// Get next char.
ch = fgetc (dataFile);
} // end while
// Done.
fclose (dataFile);
}
Note:
- The convention for constants is to write them in caps
(MAX_CHARS_PER_LINE) and to use underscores to separate out "meaning".
- Unfortunately, there is no simple read-line function in C, so
we will read char by char and detect lines ourselves.
- The function fgetc is used to read the next char from
the given stream.
- Every file has a special EOF char at the end, upon
whose detection we stop reading the file:
while (ch != EOF) {
// ...
}
- Note how you can terminate execution by calling exit(0).
There's a sublety to be aware of with regard to the end of a line:
- Unix files use a single character, \n (line feed), to
mark the end of a line in a text file.
- Windows (DOS) uses two characters, linefeed followed by
carriage-return, in text files.
- This is why, when you copy over a Windows file to Unix, you
sometimes see ^M at the end of every line.
- The problem is partially solved in that C libraries for Windows
strip out the carriage-return when reading and append a
carriage-return while writing to text files.
- However, this filtering is only done for text files. Thus, while
the above code will work on Windows, an equivalent byte-oriented
program will not.
- If you read (using fread, for example) or write byte-files that
are also used as text files, you need to do your own filtering on Windows.
Commandline arguments
An example that uses commandline arguments:
- Consider a Unix program like cp:
cp test.c test2.c
Here, the file test.c is copied into a new file called test2.c.
- The program is cp and the commandline arguments are the
strings test.c and test2.c.
Here's the code: (source file)
// argc: number of commandline arguments.
// argv: array of strings.
int main (int argc, char **argv)
{
if (argc != 3) {
printf ("Usage: copy \n");
exit (0);
}
printf ("Copying from %s to %s ... \n", argv[1], argv[2]);
// ... Do actual copying ...
printf ("... done\n");
}
Note:
- The first string is always the program name itself (in argv[0]).
- This is why we check whether the number of arguments
argc is 3.
In-Class Exercise 3.4:
Explain why
argv is declared as
char **argv (pointer to
a pointer to char).
Then, fill in the code to perform the actual copying above.
Use the function putc (char, FILE) to write a character at a time.
You do not need to write EOF to the destination file, but you do
need to close the file.
Enumerated types
An example:
(source file)
// Define the enum type:
enum colorEnum {blue, orange, gray};
void forecast (enum colorEnum c) // Parameter variable declaration.
{
if (c == blue) {
printf ("Sunny and warm!\n");
}
else if (c == orange) {
printf ("Enjoy the sunset\n");
}
else if (c == gray) {
printf ("Stay inside\n");
}
}
int main ()
{
enum colorEnum skyColorToday = blue; // Local variable declaration.
forecast (skyColorToday);
forecast (gray);
printf ("color = %d\n", orange); // Prints 1.
}
Note:
- An enum type is actually implemented with integers,
starting with 0, in the order of declaration.
- C uses unusual syntax for variable declarations. Instead of
colorEnum skyColorToday;
what's required is:
enum colorEnum skyColorToday;
Typedef
You can use typedef to create declaration shortcuts, as
the following example shows:
(source file)
// Define an enum type and give it the name skyColorType:
typedef enum skyColor {blue, orange, gray} skyColorType;
// Define a type called "doublePointer":
typedef double *doublePointer;
// Define a string type:
typedef char *forecastString;
// This function takes a skycolorType and a doublePointer type
// as parameters and returns a string type.
forecastString forecast (skyColorType c, doublePointer temperature)
{
forecastString str = "";
if (c == blue) {
str = "Sunny and warm!";
*temperature = 85.5; // Note: "temperature" is a pointer.
}
else if (c == orange) {
str = "Enjoy the sunset";
*temperature = 77.3;
}
else if (c == gray) {
str = "Stay inside";
*temperature = 64.7;
}
return str;
}
int main ()
{
double temp;
char *str;
// Pass in a color and a pointer-to-double (the address), and
// get back a string. The double "temp" gets modified in forecast.
str = forecast (blue, &temp);
printf ("Temperature = %lf: %s\n", temp, str);
}
Note:
- typedef's are really just syntactic shortcuts. The
compiler actually does a string replacement in a pre-compilation pass.
- The position of a type's name in a declaration makes it look
like a variable:
typedef double *doublePointer;
- The name skyColor is not used once the type name
skyColorType is defined, another example of strange C syntax.
- Once a typedef has been created, it can be used for any kind of
variable declaration (global, parameter, local, static).
- typedef's are more useful in naming structures,
as we will see.
Structures
About C's struct's:
- A struct is like an object without methods, or a
record in some languages (e.g., Pascal).
- A struct lets you group variables into a single unit.
- To access the individual members of a struct: the struct variable
is followed by either
- the -> operator, when the struct variable is a
pointer, or
- the . operator, when the struct variable is not a pointer.
Let's look at an example that covers all the basic ideas:
(source file)
// Define an enum type and give it the name skyColorType:
typedef enum skyColor {blue, orange, gray} skyColorType;
// Define a structure containing a double and a string. Note the
// use of typedef to name the structure.
typedef struct {
double temperature;
char *message;
} forecastInfo;
// Define a pointer type to the above structure.
typedef forecastInfo *forecastInfoPtrType;
// The function forecast returns a structure.
forecastInfo forecast (skyColorType c)
{
// Declare a pointer to the struct.
forecastInfoPtrType fInfoPtr;
// Make the pointer point to a block of memory for the struct.
fInfoPtr = (forecastInfoPtrType) malloc (sizeof (forecastInfo) * 1);
if (c == blue) {
fInfoPtr->message = "Sunny and warm!"; // Note the use of the "->" operator
fInfoPtr->temperature = 85.5; // because fInfoPtr is a pointer.
}
else if (c == orange) {
fInfoPtr->message = "Enjoy the sunset";
fInfoPtr->temperature = 77.3;
}
else if (c == gray) {
fInfoPtr->message = "Stay inside";
fInfoPtr->temperature = 64.7;
}
return *fInfoPtr; // Return the struct itself, not the pointer.
}
int main ()
{
// Example of struct variable declaration:
forecastInfo fInfo;
fInfo = forecast (blue);
// Note the use of the "." operator to access struct members.
printf ("Temperature = %lf: %s\n", fInfo.temperature, fInfo.message);
}
Note:
- We have combined a struct definition with
typedef because it makes the code appear cleaner.
- A struct can be defined without a
typedef. Here's the same example without typedef:
(source file)
enum skyColor {blue, orange, gray}; // Foregoes the use of typedef.
struct forecastStruct { // Now the struct has a name.
double temperature;
char *message;
};
// Return type needs both the "struct" keyword and the struct name.
// Parameter needs both the "enum" keyword and the enum name.
struct forecastStruct forecast (enum skyColor c)
{
// Pointer declaration without typedef.
struct forecastStruct *fInfoPtr;
// Note the full type specification for sizeof and the cast.
fInfoPtr = (struct forecastStruct *) malloc (sizeof (struct forecastStruct) * 1);
// ...
}
int main ()
{
// Example of struct variable declaration:
struct forecastStruct fInfo;
fInfo = forecast (blue);
// ...
}
- Because we used a typedef we did not need to name the
struct, although that would cause no harm:
typedef struct whatever {
double temperature;
char *message;
} forecastInfo;
- Recall that malloc takes the required space (in bytes)
as argument. Since we don't know how many bytes are needed, we use
the sizeof operator:
fInfoPtr = (forecastInfoPtrType) malloc (sizeof (forecastInfo) * 1);
where the struct name is passed to sizeof.
- Similarly the pointer returned by malloc points to
a block of bytes, and must be cast into the appropriate pointer
type, in this case: a pointer to the struct type.
- Given a pointer-to-struct variable like fInfoPtr,
an individual member field is accessed using the -> operator:
fInfoPtr->temperature = 77.3;
- On the other hand, a struct variable like fInfo must use the .
operator:
printf ("Temperature = %lf: %s\n", fInfo.temperature, fInfo.message);
Linked lists
Let's look at an example:
(source file)
// From the previous example:
typedef enum skyColor {blue, orange, gray} skyColorType;
typedef struct {
double temperature;
char *message;
char *city;
} forecastInfo;
// Structure of each list node:
typedef struct ListNode {
forecastInfo fInfo;
struct ListNode *next;
} ListNodeType;
// A list node pointer type:
typedef ListNodeType *ListNodePtrType;
// The list's front and rear pointers:
ListNodePtrType front = NULL, rear = NULL;
// Add a new node to the rear of the list.
void add (char *city, double temp, char *message)
{
// Allocate space for the node:
ListNodePtrType listPtr = (ListNodePtrType) malloc (sizeof (ListNodeType));
// Fill data.
listPtr->fInfo.city = city;
listPtr->fInfo.temperature = temp;
listPtr->fInfo.message = message;
listPtr->next = NULL;
if (front == NULL) {
// If list is empty, front and rear point to same node.
front = rear = listPtr;
return;
}
// Otherwise, we know there's at least one element, so
// add the new node past the current rear node.
rear->next = listPtr;
rear = rear->next;
}
void printList ()
{
ListNodePtrType listPtr;
printf ("List :\n");
// Start at the front and walk through the list.
listPtr = front;
while (listPtr != NULL) {
// Process current node.
printf (" Forecast for %s: temperature=%lf %s\n", listPtr->fInfo.city,
listPtr->fInfo.temperature, listPtr->fInfo.message);
// Move on to next node.
listPtr = listPtr->next;
}
}
int main ()
{
add ("DC", 85.6, "Hot");
printList ();
add ("LA", 72.6, "Warm");
printList ();
add ("NY", 64.6, "Cool");
printList ();
}
Note:
- A struct is used for each list node:
// Structure of each list node:
typedef struct ListNode {
forecastInfo fInfo;
struct ListNode *next; // The "next" pointer.
} ListNodeType;
Note how the next pointer is declared.
- We have deliberately incorporated a struct within a struct to
show what it looks like:
- Note that the inner struct is a proper struct and not a
pointer.
- Accordingly, assignments to the inner struct's members use
the . operator:
listPtr->fInfo.city = city;
- For comparison, here is the same program without
typedef's:
(source file)
// From the previous example:
enum skyColor {blue, orange, gray} skyColorType;
struct forecastInfo {
double temperature;
char *message;
char *city;
};
// Structure of each list node:
struct ListNode {
struct forecastInfo fInfo;
struct ListNode *next;
};
// The list's front and rear pointers:
struct ListNode *front = NULL;
struct ListNode *rear = NULL;
// Add a new node to the rear of the list.
void add (char *city, double temp, char *message)
{
struct ListNode *listPtr = (struct ListNode *) malloc (sizeof (struct ListNode));
// ... as before ...
}
void printList ()
{
struct ListNode *listPtr;
// ... as before ...
}
int main ()
{
// ... as before ...
}
In-Class Exercise 3.5:
Convert the above list into a doubly-linked list and add a
function to print in reverse order. Add code to
main() to
print both in forward and reverse order.