Module 4: 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
(on the stack) 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)
{
// ...
}
Exercise 4.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, download
uniform.c
and
ex4_1.c
and write your code in
ex4_1.c.
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);
Exercise 4.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().
Write your code in
swap.c
and
swap2.c.
-
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);
}
Exercise 4.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.
Exercise 4.4:
Explain why
argv is declared as
char **argv (pointer to
a pointer to char).
Then, fill in the code
(in commandline.c)
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);
}
Exercise 4.5:
Download and edit struct.c
to add the the lines
dPtr = (double*) fInfoPtr;
printf ("Pointer=%p temp=%lf\n", fInfoPtr, *dPtr);
just before the
return
statement of the
forecast()
method. Declare the variable
dPtr
as a pointer to a
double.
Explain the printed result and why it makes sense. Draw
a memory picture as part of your explanation.
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 ...
}
Exercise 4.6:
In
doublylinked.c,
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.
© 2003, Rahul Simha (revised 2017)