Module 5: Advanced C


Multiple files

 
Let's start with a simple two-file example:

Note:
 

How does compilation work with multiple files?

 

Function access works similarly:

 

The same function name can be used in two files, provided at least one of them is static:

Good practices:

 

For large applications with many files, the use of separate header files is recommended


The C Preprocessor

 
About the preprocessor:

Preprocessor examples:


Pointers to functions

 
C provides the unusual feature of function pointers, or pointers to functions: (source file)

// Take in a pointer-to-a-function, two arguments for the function,
// and apply the function. Return the result.

double applyFunction (double (*func)(double, double),  double x, double y)
{
  double z;                 // For result.

  z = (*func) (x, y);       // Apply the function. Note the parentheses around "*func".
  return z;
}


// How to declare a function prototype when an argument is a pointer-to-function:
char * funcName ( double (*func)(double, double) );


//---------------------------------------------------------------------
// Test code.

// A test function. Take in a pointer-to-a-function, apply it,
// print its name and result of applying the function.

void test ( double (*func)(double, double) )
{
  double x, y, z;
  char *name;

  x = 1.0;  y = 2.0;                  // Test values.
  z = applyFunction (func, x, y);     // Send the function itself to "applyFunction".
  name = funcName (func);             // Likewise to "funcName".
  printf ("x = %lf  y = %lf  %s(x,y) = %lf\n", x, y, name, z);
}


// Some function prototypes that will be used in testing.

double max (double, double);
double min (double, double);
double absDiff (double, double);

int main () 
{
  test (max);
  test (min);
  test (absDiff);
}


//---------------------------------------------------------------------
// The function implementations:

double max (double x, double y)
{
  if (x > y)
    return x;
  else
    return y;
}

double min (double x, double y)
{
  if (x < y)
    return x;
  else
    return y;
}


double absDiff (double x, double y)
{
  if (x > y)
    return (x - y);
  else
    return (y - x);
}


// Take in a pointer-to-a-function and print its name.

char * funcName ( double (*func)(double, double) )
{
  if (func == max) {
    return "maximum";
  }
  else if (func == min) {
    return "minimum";
  }
  else if (func == absDiff) {
    return "absoluteDifference";
  }
}
Note:
  • The example shows how complicated declarations in C can appear.

  • First, let's focus applyFunction:

    • The first argument essentially says "a pointer to a function that itsef takes two double arguments".

    • Notice that func is a variable name - a function variable.
    • The function variable needs to be encased in parentheses and must have the * operator to indicate that it's really a pointer to a function that's being declared:
      double applyFunction (double (*func)(double, double),  double x, double y)
             
    • Next, note that the same function is declared to itself take two arguments
      double applyFunction (double (*func)(double, double),  double x, double y)
             
      and to return a double:
      double applyFunction (double (*func)(double, double),  double x, double y)
             
    • The result of applyFunction is standard fare: two other double arguments and a double return value:
      double applyFunction (double (*func)(double, double),  double x, double y)
             
    • Finally, notice how a function is invoked when given a pointer to it:
        z = (*func) (x, y);       // Apply the function. Note the parentheses around "*func".
             
      The * is used to dereference the function-pointer, and must be enclosed in parens.

  • Next, let's see how applyFunction is called:
    void test ( double (*func)(double, double) )
    {
      // ...
      z = applyFunction (func, x, y);     // Send the function itself to "applyFunction".
      // ...
    }
        
    • Here, func is itself a function-variable (as parameter to test).
    • The variable itself serves as the pointer.
    • This is the key to understanding function pointers: all function names in C are actually function pointers. A function name, when used as a variable, is a function pointer:
        test (max);
        test (min);
        test (absDiff);
            

  • Next, let's examine how a function prototype is declared when one of its arguments is a function-pointer:
    char * funcName ( double (*func)(double, double) );
        
    This is similar to the applyFunction declaration, just without a body (as expected for a function prototype).

  • Finally, note that since pointers can be compared, so can function pointers:
      if (func == max) {
        return "maximum";
      }
         

Can a return value be a function-pointer? Yes, with some stranger syntax:

// Take in a pointer-to-a-function, two arguments for the function,
// and apply the function. Return the result.

double applyFunction (double (*func)(double, double),  double x, double y)
{
  double z;                 // For result.

  z = (*func) (x, y);       // Apply the function. Note the parentheses around "*func".
  return z;
}


// A function prototype for a function that returns a function-pointer:

double (*getFunction(char* str))(double, double);


int main () 
{
  double x, y, z;
  double (*func)(double, double);

  x = 1.0;  y = 2.0;                  // Test values.
  func = getFunction ("max");         // Get back a function-pointer. 
  z = applyFunction (func, x, y);     // Send the function-pointer to another function.
  printf ("x = %lf  y = %lf  max(x,y) = %lf\n", x, y, z);
}


//---------------------------------------------------------------------
// The function implementations:

double max (double x, double y)
{
  if (x > y)
    return x;
  else
    return y;
}

double min (double x, double y)
{
  if (x < y)
    return x;
  else
    return y;
}


double absDiff (double x, double y)
{
  if (x > y)
    return (x - y);
  else
    return (y - x);
}


// Take in a string and return a function-pointer.

double (*getFunction(char* str))(double, double) 
{
  if (strcmp (str, "max") == 0) {
    return max;
  }
  else if (strcmp (str, "min") == 0) {
    return min;
  }
  else if (strcmp (str, "absDiff") == 0) {
    return absDiff;
  }
}
Note:
  • The syntax of either the function prototype of getFunction or the function itself is complicated:
    double (*getFunction(char* str))(double, double) 
    {
      // ...
    }    
    • First, observe that getFunction is the name of the function.
    • This function takes in ONE parameter, a char* variable:
      double ( *getFunction(char* str) )(double, double) 
          
    • Now, we are used to having the return value type reside entirely to the left of the function name.
    • Unfortunately, it's a little more complicated in C.
    • The first * operator indicates that getFunction returns a pointer.
      double ( *getFunction(char* str) )(double, double) 
          
    • Since it's a pointer to a function, the return type and arguments need to be someplace.
    • The return value precedes the whole definition:
      double ( *getFunction(char* str) )(double, double) 
          
    • The parameters follow:
      double ( *getFunction(char* str) )(double, double)
          

  • The declarations can be somewhat simplified using typedef: (source file)
    // ...
    
    // A typedef for a function-pointer:
    typedef double (*funcPointerType)(double, double);
    
    // getFunction is declared as a function that returns a funcPointerType, 
    // and is declared to itself take a char* parameter.
    funcPointerType getFunction(char *str);
    
    // ...
    
    // Function that returns a function-pointer
    
    funcPointerType getFunction (char *str)
    {
      // ...
    }
        
    • The typedef declaration is like the first example, a declaration of a function-pointer.
    • Now, the function-prototype declaration
      // getFunction is declared as a function that returns a funcPointerType, 
      // and is declared to itself take a char* parameter.
      funcPointerType getFunction(char *str);
            
      can be read as simply a function that returns a function-pointer.


Functions with variable number of arguments

 

C let's you define functions to take a variable number of arguments. For example: (source file)

#include <stdio.h>
#include <string.h>
#include <stdarg.h>    // Required include for variable number of arguments.

// Function prototype for a function with variable number of arguments:
// This function will take in an integer and any number of strings.
char * concatStrings (int numStrings, ...);


int main ()
{
  char *str;

  // Example with 2 strings.
  str = concatStrings (2, "Hello", " World!");
  printf ("%s\n", str);

  // Example with 6 strings.
  str = concatStrings (6, "So,", " how're", " we", " doing", " today,", " eh?");
  printf ("%s\n", str);
}


// The function with definition and body.

char * concatStrings (int numStrings, ...)
{
  va_list argList;                // The var-arg package requires this variable
                                  // to be defined.

  // Variables for our use:
  char *resultString = "";        // Resulting string after concatenation.
  char *nextString;               // A variable to hold the next argument.
  int numStringsExtracted = 0;    

  // This call initializes the var-arg package:
  va_start (argList, numStrings);

  // Extract arguments one by one:
  while (numStringsExtracted < numStrings) {

    // Note: first argument is the required argList variable, and
    // the second argument is simply the type of the next argument:
    nextString = va_arg (argList, char*);

    numStringsExtracted ++;

    // Process argument: append the string to resultString

    // ...
  }

  // Required call to signal end of var-args:
  va_end (argList);

  // Return result.
  return resultString;
}
Note:
  • The package stdarg.h needs to be included.

  • The first argument cannot be an "unknown" argument:
    char * concatStrings (int numStrings, ...);
        

  • The three periods act as an operator, indicating a variable number of arguments.

  • The stdarg package requires you to define a variable to hold the unknown arguments:
    char * concatStrings (int numStrings, ...)
    {
      va_list argList;                // The var-arg package requires this variable
    
      // ...
    }  
        

  • The unknown arguments themselves are extracted in a loop:
      va_start (argList, numStrings);
      while (numStringsExtracted < numStrings) {
        nextString = va_arg (argList, char*);
        numStringsExtracted ++;
        // ...  
      }
      
        
 
Exercise 5.1: Complete the program above (in vararg.c) by adding code to concatenate strings.


Advanced topics not covered here

 
Some topics we haven't covered:

  • void pointers and their uses.
  • How to load external libraries.
  • How to write a library.
  • size_t and its uses.
  • wchar_t and extended character sets.
  • Including assembly in a C program.
  • Getting C to work with other languages.
  • Full coverage of standard C libraries.
  • Compiler optimizations.



© 2003, Rahul Simha (revised 2017)