Physics and Astronomy |
Back to top
On this page
Contents Sharing and the pre-processorThis final lecture deals primarily with sharing variables between functions and sharing both variables and functions between source code in different files. This this makes use of the pre-processor so we take the opportunity to cover the preprocesor in a little more detail. Sharing variables between functions Relying too heavily on external
variables is fraught with peril since it leads to programs
whose data connections are not at all obvious - variables can
be changed in unexpected and even inadvertent ways and the
program is hard to modify.
Kernighan & Ritchie. GLOBAL variables (to be used only if you really need them).. Sharing variables between functions in the same source fileVariables defined outside of a function are called external variables and can be used by any function in the file, provided only that the function definition follows the variable definition: Variables declared outside of a function are called external, or global,variables and can be accessed by any function. int somenumber = 7; // myfun() can now use the variable somenumber: void myfun(int i) { somenumber = i *i; } // so can main(), if it is inside the same file: int main() { int k; myfun(3); k = 6 + somenumber; return 0; } Initialising external variablesIn the above example somenumber, this happens before the program begins. Just like in-function static variables, by default external variables are initialised to zero (or NULL for pointers). External variables are initialised to zero by default.
External variables should be used very sparinglyLook at the comment by Kernighan & Ritchie at the top of the section ("variables can be changed in unexpected and even inadvertent ways and the program is hard to modify"). Contrast that with a "normal" function call with arguments: x = fun1(arg1, arg2); Here when we call function fun1 we can be confident that fun1 isn't going to change the values of its arguments. if instead of using arguments we had made arg1 and arg2 external so the function call was just x = fun1(); we wouldn't know if arg1 and arg2 had the same values when fun1 returned. If overused external variables tie different routines up in knots.
External variables should be used very sparingly
Use global variables for situations when
all of the following criteria apply:
Good uses include:
Never use global variables just to avoid function parameters. Splitting source code between filesAs programs get larger it makes sense to split the code between different files, with related functions all going into the same file. These are referred to as source files and have the file-name suffix ".c".
Header files: "mydefs.h"
|
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#ifdef M_PI | ||||
// Using system value of pi | // Using system value of pi | |||
#define PI M_PI | ||||
#else | ||||
// Using our value of pi | ||||
#define PI 3.14159265358979 | ||||
#endif | ||||
int main() { | int main() { | |||
double twopi = 2 * PI; | double twopi = 2 * 3.14159265358979323846; | |||
return 0; | return 0; | |||
} | } |
If M_PI is not defined then the second part of the #if is activated and PI is defined as 3.14159265358979, which again is used inside main():
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#ifdef M_PI | ||||
// Using system value of pi | ||||
#define PI M_PI | ||||
#else | ||||
// Using our value of pi | // Using our value of pi | |||
#define PI 3.14159265358979 | ||||
#endif | ||||
int main() { | int main() { | |||
double twopi = 2 * PI; | double twopi = 2 * 3.14159265358979; | |||
return 0; | return 0; | |||
} | } |
NB: for clarity we have used an option to the C preprocessor that preserves comments, when compiling they are replaced by spaces.
C also defines #ifndef M_PI for "if M_PI has not been #defined".
Using the the preprocessor to handle differences between operating systems or compilers, is very common. Note that #ifdef doesn't care what value M_PI has been defined with, just that it has been defined.
#if #elseif #else #endif cause the preprocessor to exclude parts of the source file
#if and #ifdef operate before the code ever gets to the compiler, and the "false" sections are completely removed. Consider the following rather extreme example:
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#define ABC "Alpha Bravo Charlie\n" | ||||
#ifndef ABC | ||||
Chim chiminey, chim chiminey, | ||||
chim chim cher-oo. | ||||
My dad is a dalek and I'm Dr Who! | ||||
#else | ||||
int main() { | int main() { | |||
printf(ABC); | printf("Alpha Bravo Charlie\n"); | |||
return 0; | return 0; | |||
} | } | |||
#endif |
Strange as it looks this is perfectly legal C as the "Chim chimineny.." text is removed before the code is compiled.
Before we go on to some examples of using #ifdef, a couple of points are worth mentioning.
#undef FOO
removes any definition of FOO. It does not matter if FOO had not previously been defined.
We've already met one use of an empty #define, with assert.h:
#define NDEBUG #include <assert.h>
which turns off the abort() macro. In this case any subsequent occurance of NDEBUG in the code would simply be removed.
"Empty" #defines like this are often used when their job is simply to turn on or off sections of code later on the the file(s) They have another use when defining debugging macros as we shall see below.
Chopping out whole sections of the program before they even get to the compiler is a pretty extreme measure and it tends to get used in a few specific circumstances:
We don't normally want to include a header file twice by mistake. We can guard against this by using the preprocessor. For example our previous header file alloc.h might actually look like this:
#ifndef _ALLOC_H #define _ALLOC_H void *xmalloc(size_t n); void *xrealloc(void *old, size_t n); #endif
Should this accidentally be #included twice into a source file then the second time around the preprocessor will strip it out. This is an almost-universal convention, for example on my system the file math.h begins with a comment followed by:
#ifndef _MATH_H #define _MATH_H 1
Protect header files with #ifndef _FILENAME_H
When developing code it's quite common to want to have debugging statements but to turn them off when the code goes out to other people. One way to do this is to have a line at the top of our include file like this:
#define DEBUG
This can be removed when the code goes "live". Later on in the include file, and sometimes in the .c files, there will be sections of the form:
#ifdef DEBUG ... debugging code #else ... normal code #endif
This allows us to define an optional "I'm here" macro to help us with cases where we are not quite sure the order in which functions are being called, how many times loops are running, etc:
#ifdef DEBUG #define IAMHERE fprintf(stderr, "%s() line %d\n", __func__, __LINE__) #else #define IAMHERE #endif
The second half of this looks strange but without it, if a function contained the statement:
IMHERE;
and DEBUG had not been defined then the compiler would not know what IMHERE meant an the compilation would fail. So we put in an empty #define which is replaced by an empty string.
As it stands we have to declare global variables twice: first as extern in the include file, and then once without the extern in a .c file. But what if we are too laz busy to do this twice?
One of my header files looks like this (with several hundred lines missed out):
#ifndef GLOBALS_H #define GLOBALS_H #ifndef EXTERN #define EXTERN extern #endif EXTERN int mouseaction, popup, msize; #endif
There are many other lines starting with EXTERN. Every file except one (main.c) has #include "globals.h" as its very first line:
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#include "globals.h" | extern int mouseaction, popup, msize; | |||
void somefunction(void) { | void somefunction(void) { | |||
// More code here.. | // More code here.. | |||
} | } |
This has the effect of declaring mouseaction, etc. as extern (ie it does not create these variables, just says they have been declared properly somewhere else).
But main.c looks like this:
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#define EXTERN | int mouseaction, popup, msize; | |||
#include "globals.h" | ||||
int main() { | int main() { | |||
// ... | // ... | |||
return 0; | return 0; | |||
} | } |
(note the missing "extern"). Now mouseaction, etc. have been properly declared and we have achieved our goal of declaring our global variables just once.
In extreme circumstances we can use expressions in #if lines:
#if ! defined(M_PI) && ! defined(PI) I don't know what pi is! #endif
If neither M_PI or PI has been #defined then the line "I don't know what pi is!" will get passed to the compiler with the inevitable consequence.
#defines can also take arguments, although these are less used than they used to be. For example;
#define myisdigit(c) (((c) >= '0' && (c) <= '9'))
notice that the "expansion" contains the arguments (in this case just one, "c"); this is nearly always the case. Now if our code contains the line:
if (myisdigit(str[i]))
It gets preprocessed to the macro definition with the"c"s replaced by "str[i]":
if ( (((str[i]) >= '0' && (str[i]) <= '9'))
Using this instead of isdigit() is very much faster on my machine. (It's also safer than it looks: although computers are not obliged to use ASCII to represent digits they are obliged to use a scheme where '1' == '0' + 1, etc.)
There are a few other dangers however, one of which is hinted at by the large number of brackets we chose to use.
Traditionally macros have been used for two main purposes:
Modern compilers will usually deal with problem 1 (by removing the function call altogether and inserting the instructions directly into the calling function) so "save time" macros are usually better written as single-line functions, particularly for macros that expand an argument twice, as in the above example:
// Single-line function replacing a macro
int myisdigit(char c) {
return c >= '0' && c <= '9';
}
Macros that expand an argument twice are best replaced by single-line functions.
#define BADSQ(foo) foo * foo
Then if the preprocessor later encounters:
z = BADSQ(x);
it replaces it with:
z = x * x;
This looks like a function but isn't, if the preprocessor later encounters:
z = BADSQ(x + y);
it replaces it with:
z = x + y * x + y;
which is not what we want! Our first attempt at a fix is to add brackets around the "x":
#define BADSQ(foo) (foo) * (foo)
Now
z = BADSQ(x + y);
correctly becomes:
z = (x + y) * (x + y);
But we still have a problem with the line:
z = 1.0/BADSQ(x);
which expands to:
z = 1.0 /(x) * (x);Instead we should use:
#define SQ(foo) ((foo) * (foo))
Even this isn't entirely safe however, consider:
y = SQ(++x);
which becomes:
y = ((++x) * (++x));
So x gets incremented twice. We show this below:
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#define BADSQ(foo) foo * foo | ||||
#define BADSQ2(foo) (foo) * (foo) | ||||
#define SQ(foo) ((foo) * (foo)) | ||||
int main() { | int main() { | |||
double x = 1.2, y = 2.1; | double x = 1.2, y = 2.1; | |||
printf("%g squared is not %g\n", x+y, BADSQ(x+y) ); | printf("%g squared is not %g\n", x+y, x+y * x+y ); | |||
printf("1/%g squared is also not %g\n", x+y, 1.0/BADSQ2(x+y) ); | printf("1/%g squared is also not %g\n", x+y, 1.0/(x+y) * (x+y) ); | |||
printf("1.0/%g squared is %g\n", x+y, 1.0/SQ(x+y) ); | printf("1.0/%g squared is %g\n", x+y, 1.0/((x+y) * (x+y)) ); | |||
printf("But %g squared ", x + 1); | printf("But %g squared ", x + 1); | |||
printf("is not %g\n", SQ(++x) ); | printf("is not %g\n", ((++x) * (++x)) ); | |||
printf("x is noq %g\n", x); | printf("x is noq %g\n", x); | |||
return 0; | return 0; | |||
} | } |
What this tells us is that using macros for mathematical shortcuts is very dubious: it's better to use a function instead.
As mentioned above, macros that use their arguments twice are best avoided and so is changing the value of variables in calls to functions or macro evaluations.
Macros with at least one argument may be specified to have a variable number of arguments: any "extra" arguments are just pasted in. These arguments are indicated by ...:
#define mymacro(arg1, arg2, ...)
We then need to specify where the arguments appear in the expansion. This is done by placing the word __VA_ARGS__ where the extra arguments should appear.
#ifdef DEBUG #define debug(format, ... ) fprintf(stderr, format, __VA_ARGS__) #else #define debug(format, ... ) #endif
We would normally put the above in a header file or at the very top of a source file. Inside a function we can then write:
debug("x = %g\n", x);
And it will only print out the message when DEBUG has been #defined.
We have already mentioned how C99 defines __func__, the name of the current function, and __LINE__ the line number. This enables to create a slightly better debug macro:
#ifdef DEBUG #define debug(format, ... ) fprintf(stderr, "%s() line %d: " format, __func__, __LINE__, __VA_ARGS__) #else #define debug(format, ... ) #endif
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#define DEBUG | ||||
#include "debug.h" | ||||
int main() { | int main() { | |||
int x = 1; | int x = 1; | |||
debug("x is %d\n", x); | fprintf(stderr, "%s() line %d: " "x is %d\n", __func__, 6, x | |||
return 0; | return 0; | |||
} | } |
This works because the complier joins the first two strings together to create a single format string. The output is:
main() line 6: x is 1
If we remove the #define of debug then the macro expands to a blank:
Before preprocessing | After preprocessing | |||
---|---|---|---|---|
#include "debug.h" | ||||
int main() { | int main() { | |||
int x = 1; | int x = 1; | |||
debug("x is %d\n", x); | ; | |||
return 0; | return 0; | |||
} | } |
Preprocessor tricks can sometimes be useful but are always ugly. Use them sparingly: a few can be useful but like all bodges once we have too many they can be confusing and interact quite badly.
The text of each key point is a link to the place in the web page.