Module 5: Advanced C
Multiple files
Let's start with a simple two-file example:
- File 1, test1a.c:
(source file)
#include <stdio.h>
// A global variable in this file:
int i;
// A global variable from another file:
extern int j;
int main ()
{
i = 5; // Access ths file's variable, as usual.
printf ("i = %d\n", i);
j = 6; // Access the other file's variable.
printf ("j = %d\n", j);
}
- File 2, test1b.c:
(source file)
#include <stdio.h>
int j;
- To compile:
gcc -o test1 test1a.c test1b.c
or
gcc -o test1 test1b.c test1a.c
Note:
- The extern keyword is used to indicate a variable in
another file.
- There is no special declaration needed for j in the
other file.
- Local variables (declared inside a function) cannot be accessed
outside the function, let alone another file.
- If you want to prevent a variable from being accessed
elsewhere, use the static modifier for global variables,
e.g.,
- File 1, test2a.c
(source file)
#include <stdio.h>
// A global variable in this file:
int i;
// A global variable from another file:
extern int j;
// Attempt to access j2, declared as static in the other file:
extern int j2;
int main ()
{
i = 5; // Access the file variable, as usual.
printf ("i = %d\n", i);
j = 6; // Access the other file's variable.
printf ("j = %d\n", j);
j2 = 7; // Attempt to access j2.
printf ("j = %d\n", j2);
}
- File 2, test2b.c
(source file)
#include <stdio.h>
// Declaration of j.
int j;
// Static declaration, to prevent external access.
static int j2;
- The compilation command
gcc -o test2 test2a.c test2b.c
results in the error: undefined reference to j2.
How does compilation work with multiple files?
- The process varies with different operating systems.
- Compilation occurs in two phases:
- Compilation: the process of turning the C source into low-level
binary or "object" code.
- Linking: the process of combining multiple object files into
a single executable,
adjusting references so accessing variables or functions in other
files works correctly.
- In the static example above (test2a.c and
test2b.c) we could have simply compiled the files:
gcc -c test2a.c test2b.c
This gives no error. But linking them does give an error.
- We could also have compiled the first example separately:
gcc -c test1a.c test1b.c
and then linked to create a single executable:
gcc -o test1 test1a.o test1b.o
Function access works similarly:
- File 1, test3a.c:
(source file)
#include <stdio.h>
extern void funcB ();
void funcA ()
{
printf ("func A\n");
}
int main ()
{
funcA (); // Accessing a function in this file.
funcB (); // A function in another file.
}
- File 1, test3b.c:
(source file)
#include <stdio.h>
void funcB ()
{
printf ("func B\n");
}
- The compilation command
gcc -o test3 test3a.c test3b.c
or the compilation-followed-by-linking sequence
gcc -c test3a.c test3b.c
gcc -o test3 test3a.o test3b.o
both work.
- For extra clarity, we could add a function prototype
to the second file:
(source file)
#include <stdio.h>
void funcB ();
void funcB ()
{
printf ("func B\n");
}
- To prevent access, or to hide visibility a function prototype can
be declared static:
(source file)
#include <stdio.h>
// Declare the function prototype as static:
static void funcB ();
void funcB ()
{
printf ("func B\n");
}
This causes a compilation error when the first file tries to access funcB().
- You cannot define main() in more than one file.
The same function name can be used in two files, provided at least
one of them is static:
- File 1, test6a.c:
(source file)
#include <stdio.h>
static void funcA ();
void funcA ()
{
printf ("func A in file 1\n");
}
int main ()
{
funcA ();
}
- File 2, test6b.c:
(source file)
#include <stdio.h>
void funcA ()
{
printf ("func A in file 2\n");
}
Good practices:
- It is good practice to create function prototypes for all your
functions in each file as follows:
- Declare static prototypes for functions you don't want
to export to other files.
- Declare non-static prototypes for those that you wish
to export.
- Some compilers will pick up the error (name clash) here:
- File 1, test7a.c:
(source file)
#include <stdio.h>
int i = 5;
int main ()
{
printf ("i = %d\n", i);
}
- File 1, test7b.c:
#include <stdio.h>
int i = 6;
- Therefore, it's best to declare each variable local to a file as
static, unless it's explicitly for sharing:
- File 1, test7a.c:
(source file)
#include <stdio.h>
static int i = 5;
int main ()
{
printf ("i = %d\n", i);
}
- File 1, test7b.c:
#include <stdio.h>
static int i = 6;
For large applications with many files, the use of separate header
files is recommended
- Header file, test9.h:
(source file)
// File test9.h
int global_i; // Declare global variables
void funcA (); // and function prototypes.
void funcB ();
- File 1, test9a.c:
(source file)
#include <stdio.h>
#include "test9.h"
void funcA ()
{
global_i = 5;
printf ("func A: global_i=%d\n", global_i);
}
int main ()
{
funcA ();
funcB ();
}
- File 2, test9b.c:
(source file)
#include <stdio.h>
#include "test9.h"
void funcB ()
{
global_i = 6;
printf ("func B: global_i=%d\n", global_i);
}
- Large C projects use a makefile which facilitates
specification of compilation options, compilation order, and
other compilation-related features.
The C Preprocessor
About the preprocessor:
- Preprocessor commands are processed and translated before actual
compilation begins.
- Preprocessor commands are not strictly part of the C language
- they can be considered as "commands to the compiler".
- These are the preprocessor commands:
#define
#include
#ifdef
#ifndef
#else
#elif
#endif
#undef
- Each preprocessor command must be on a line of its own.
- The convention is to have each preprocessor command start at the very
beginning of the line.
Preprocessor examples:
- Constant definitions, e.g.
(source file)
#define ARRAY_SIZE 10
int A [ARRAY_SIZE];
int main ()
{
A[0] = 5;
}
- Conditional compilation for cross-platform compilation:
#define UNIX
int main ()
{
#ifdef UNIX
printf ("Unix\n");
#endif
#ifdef WINDOWS
printf ("Windows\n");
#endif
}
- Conditional compilation for debugging:
#define DEBUG
int main ()
{
#ifdef DEBUG
// ... debugging output
#endif
}
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)