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:
 

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

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