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:

Next, let's add a couple of functions:

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:
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:

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:
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().
  1. 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);
    }
      

  2. 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:

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.