Working with Larger Programs in C: Communication Between Modules

Several methods can be used so that the modules contained in separate files can effec- tively communicate. If a function from one file needs to call a function contained inside another file, the function call can be made in the normal fashion, and arguments can be passed and returned in the usual way. Of course, in the file that calls the function, you should always make  certain to include a prototype declaration so the compiler knows  the function’s argument types and the type of the return value. As noted in Chapter 14, “More on Data Types,” in the absence of any information about a function, the compiler assumes it returns an int and converts short or char arguments to ints and float arguments to doubles when the function is called.

It’s important to remember that even though more than one module might be speci-fied to the compiler at the same time on the command line, the compiler compiles each module independently. That means that no knowledge about structure definitions, function return types, or function argument types is shared across module compilations by the compiler. It’s totally up to you to ensure that the compiler has sufficient information about such things to correctly compile each module.

1. External Variables

Functions contained in separate files can communicate through external variables, which are effectively an extension to the concept of the global variable discussed in Chapter 8, “Working with Functions.”

An external variable is one whose value can be accessed and changed by another module. Inside the module that wants to access the external variable, the variable is declared in the normal fashion and the keyword extern is placed before the declaration. This signals to the system that a globally defined variable from another file is to be accessed.

Suppose you want to define an int variable called moveNumber, whose value you want to access and possibly modify from within a function contained in another file. In Chapter 8, you learned that if you wrote the statement

int moveNumber = 0;

at the beginning of your program, outside of any function, then its value could be refer- enced by any function within that program. In such a case, moveNumber was defined as a global variable.

Actually, this same definition of the variable moveNumber also makes its value accessi- ble by functions contained in other files. Specifically, the preceding statement defines the variable moveNumber not just as a global variable, but also as an external global variable. To reference the value of an external global variable from another module, you must declare the variable to be accessed, preceding the declaration with the keyword extern, as follows:

extern int moveNumber;

The value of moveNumber can now be accessed and modified by the module in which the preceding declaration appears. Other modules can also access the value of moveNumber by incorporating a similar extern declaration in the file.

You must obey an important rule when working with external variables. The variable has to be defined in some place among your source files. This is done in one of two ways. The first way is to declare the variable outside of any function, not preceded by the key- word extern, as follows:

int moveNumber;

Here, an initial value can be optionally assigned to the variable, as was shown previously.

The second way to define an external variable is to declare the variable outside of any function, placing the keyword extern in front of the declaration, and explicitly assigning an initial value to it, as follows:

extern int moveNumber = 0;

Note that these two ways are mutually exclusive.

When dealing with external variables, the keyword extern can only be omitted in one spot throughout your source files. If you don’t omit the keyword in any one spot, in exactly one place, you must assign the variable an initial value.

Take a look at a small program example to illustrate the use of external variables. Suppose you type the following code into a file called main.c:

#include <stdio.h>

int i = 5;

int main (void)

{

printf (“%i “, i);

foo ();

printf (“%i\n”, i);

return 0;

}

The definition of the global variable i in the preceding program makes its value accessi- ble by any module that uses an appropriate extern declaration. Suppose you now type the following statements into a file called foo.c:

extern int i;

void foo (void)

{

i = 100;

}

Compiling the two modules main.c and foo.c together with a command like

$ gcc main.c foo.c

and subsequently executing the program produces the following output at the terminal:

5 100

This output verifies that the function foo is able to access and change the value of the external variable i.

Because the value of the external variable i is referenced inside the function foo, you could have placed the extern declaration of i inside the function itself, as follows:

void foo (void)

{

extern int i;

i = 100;

}

If many functions in the file foo.c need to access the value of i, it is easier to make the extern declaration just once at the front of the file. However, if only one function or a small number of functions need to access this variable, there is something to be said for making separate extern declarations in each such function: It makes the program more organized and isolates the use of the particular variable to those functions that actually use it.

When declaring an external array, it is not necessary to give its size. Thus, the declaration

extern char text[];

enables you to reference a character array text that is defined elsewhere. As with formal parameter arrays, if the external array is multidimensional, all but the first dimension must be specified. Thus, the declaration

extern int matrix[][50];

suffices to declare a two-dimensional external array matrix that contains 50 columns.

2. Static Versus Extern Variables and Functions

You now know that any variable defined outside of a function is not only a global vari- able, but is also an external variable. Many situations arise in which you want to define a variable to be global but not external. In other words, you want to define a global vari- able to be local to a particular module (file). It makes sense to want to define a variable this way if no functions other than those contained inside a particular file need access to the particular variable. This can be accomplished in C by defining the variable to be static.

The statement

static int moveNumber = 0;

if made outside of any function, makes the value of moveNumber accessible from any sub- sequent point in the file in which the definition appears, but not from functions contained in other files.

If you need to define a global variable whose value does not have to be accessed from another file, declare the variable to be static. This is a cleaner approach to program- ming: The static declaration more accurately reflects the variable’s usage and no con- flicts can be created by two modules that unknowingly both use different external global variables of the same name.

As mentioned earlier in this chapter, you can directly call a function defined in another file. Unlike variables, no special mechanisms are required; that is, to call a func- tion contained in another file, you don’t need an extern declaration for that function.

When a function is defined, it can be declared to be extern or static, the former case being the default. A static function can be called only from within the same file as the function appears. So, if you have a function called squareRoot, placing the keyword static before the function header declaration for this function makes it callable only from within the file in which it is defined:

static double squareRoot (double x)

{

}

The definition of the squareRoot function effectively becomes local to the file in which it is defined. It cannot be called from outside the file.

The same motivations previously cited for using static variables also apply to the case of static functions.

Figure 15.1 summarizes communication between different modules. Here two mod- ules are depicted, mod1.c and mod2.c.mod1.c defines two functions: doSquare and main. The way things are set up here, main calls doSquare, which in turn calls square. This last function is defined in the module mod2.c.

Because doSquare is declared static, it can only be called from within mod1.c, and by no other module.

mod1.c defines two global variables: x and result, both of type double. x can be accessed by any module that is linked together with mod1.c. On the other hand, the keyword static in front of the definition of result means that it can only be accessed by functions defined inside mod1.c (namely main and doSquare).

When execution begins, the main routine calls doSquare. This function assigns the value 2.0 to the global variable x and then calls the function square. Because square is defined in another source file (inside mod2.c), and because it doesn’t return an int, doSquare properly includes an appropriate declaration at the beginning of the function.

The square function returns as its value the square of the value of the global variable x. Because square wants to access the value of this variable, which is defined in another source file (in mod1.c), an appropriate extern declaration appears in mod2.c (and, in this case, it makes no difference whether the declaration occurs inside or outside the square function).

The value that is returned by square is assigned to the global variable result inside doSquare, which then returns back to main. Inside main, the value of the global variable result is displayed. This example, when run, produces a result of 4.0 at the terminal (because that’s obviously  the square of 2.0).

Study the example until you feel comfortable with it. This small—albeit impractical— example illustrates very important concepts about communicating between modules, and it’s necessary  that you understand these concepts to work effectively with larger programs.

3. Using Header Files Effectively

In Chapter 13, “The Preprocessor,” you were introduced to the concept of the include file. As stated there, you can group all your commonly used definitions inside such a file and then simply include the file in any program that needs to use those definitions. Nowhere is the usefulness of the #include facility greater than in developing programs that have been divided into separate program modules.

If more than one programmer is working on developing a particular program, include files provide a means of standardization: Each programmer is using the same definitions, which have the same values. Furthermore, each programmer is thus spared the time- consuming and error-prone task of typing these definitions into each file that must use them. These last two points are made even stronger when you start placing common structure definitions, external variable declarations, typedef definitions, and function prototype declarations into include files.Various modules of a large programming system invariably deal with common data structures. By centralizing the definition of these data structures into one or more include files, you eliminate the error that is caused by two modules that use different definitions for the same data structure. Furthermore, if a change has to be made to the definition of a particular data structure, it can be done in one place only—inside the include file.

Recall your date structure from Chapter 9, “Working with Structures”; following is an include file that might be similar to one you would set up if you have to work with a lot of dates within different modules. It is also a good example of how to tie together many of the concepts you’ve learned up to this point.

// Header file for working with dates

#include <stdbool.h>

// Enumerated types

enum kMonth { January=1, February, March, April, May,

June, July, August, September, October, November, December };

enum kDay { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday };

struct date

{

enum kMonth month;

enum kDay   day;

int  year;

};

// Date type

typedef struct date Date;

// Functions that work with dates

Date dateUpdate (Date today);

int  numberOfDays (Date d);

bool isLeapYear (Date d);

// Macro to set a date in a structure

#define setDate(s,mm,dd,yy)  s = (Date) {mm, dd, yy}

// External variable reference

extern Date todaysDate;

The header file defines two enumerated data types, kMonth and kDay, and the date struc- ture (and note the use of the enumerated data types); uses typedef to create a type called Date; and declares functions that use this type, a macro to set a date to specific val- ues (using compound literals), and an external variable called todaysDate, that will pre- sumably be set to today’s date (and is defined in one of the source files).

As an example using this header file, the following is a rewritten version of the dateUpdate function from Chapter 9.

#include “date.h”

// Function to calculate tomorrow’s date

Date dateUpdate (Date today)

{

Date tomorrow;

if ( today.day != numberOfDays (today) )

setDate (tomorrow, today.month, today.day + 1, today.year);

else if ( today.month == December )  // end of year

setDate (tomorrow, January, 1, today.year + 1);

else                        // end of month

setDate (tomorrow, today.month + 1, 1, today.year);

return tomorrow;

} .

Source: Kochan Stephen G. (2004), Programming in C: A Complete Introduction to the C Programming Language, Sams; Subsequent edition.

Leave a Reply

Your email address will not be published. Required fields are marked *