Separate Compilation and Linking

Most C programs are too large for just one file.

A typical program consists of several source files and usually some header files.

We can create a project composed of several C files. All the files belonging to one project should reside in the same directory. The best practice is to create a new directory for each new project.

Example1: a program to process an array of numbers, using a separately compiled program composed of several files.

We have the following:

multifilearray.c This file contains the "main" function definition.

Execution will begin here.

array.h Contains prototypes needed for the array.c file.

array.c Contains the function definitions for the functions needed

by multifilearray.c to process a data array.

Source Files

By convention source files have the extension .c. Each source file contains part of the program, primarily definitions of functions and variables. One source file must contain a function named main, which serves as the starting point for the program.

Header Files

If the program is in several source files, how can a function in one file call a function that’s defined in another file? How can a function access an external variable in another file? How can two files share the same macro definition or type definition?

Answer - #include directive. – makes it possible to share function prototypes, macro definition, type definitions, etc among any number of source files.

#include directive tells the preprocessor to open a specified file and insert its contents into the current file. If we want several source files to have access to the same information, we’ll put that information in a file and then use #include to bring the file’s contents into each of the source files. Files included like this are called header files (or include files).

Header files have extension .h.

#include

2 forms: difference has to do with how compiler locates the header file.

#include <filename>

Search the directory (or directories) in which system header files reside (on UNIX, system header files are usually kept in the directory /usr/include).

#include “filename”

Search the current directory, then search the directory (or directories) in which system header files reside.

When we create header files, we shouldn’t use #include <myheader.h>, because the preprocessor will look for myheader.h where the system header files are kept and won’t find it.

#include may include information tat helps locate the file such as directory path or drive:

#include “C:\cprogs\utils.h” /*DOS path*/

#include “/cprogs/utils.h” /*UNIX path */

Macro definitions and type definitions that need to be shared by several source files should go into header files.

Ex. BOOL, TRUE, FALSE – why repeat in each source file.

Put in a header file with a name like boolean.h

And any source file that requires these macros:

#define BOOL int

#define TRUE 1

#define FALSE 0

would simply contain the line #include “Boolean.h”

Also type definitions go in header.

Instead of #define BOOL int

We can have Boolean.h look like this:

#define TRUE 1

#define FALSE 0

typedef int Bool;

Putting definitions of macros and types in header files has advantages. We save time not having to copy the definitions into the source files, and programs become easier to modify. We only need to change a single header file to change a definition of a macro or type. Also, we don’t have to worry about inconsistencies caused by some source files containing different definitions of the same macro or type.

Sharing Function Prototypes

If we have a function f defined in foo.c, calling f without declaring it is risky. We need the prototype to rely on with the return type of f and number of parameters with their types.

Don’t want to declare it in the file where it’s called, because how can we ensure that f’s prototypes are the same in all the files? If f should change later, how can we find all the files where it’s used?

We put f’s prototype in a header file, then include the header file in all the places where f is called. Since f is defined in foo.c we’ll name the header file foo.h. we’ll also need to include the header in foo.c as well as the source files where f is called, so the compiler can check f’s protype in foo.h matches its definition in fo.c.

If foo.c contains other functions, most of them should be declared in the same header file as f, since presumably it should be related to f, and the other files that contain a call of f probably will need some of the other functions in foo.c

Functions that are intended for use only within foo.c shouldn’t be declared in a header file.

Sharing Variable Declarations

Variables can be shared among files like functions. Functions – we put its definition in one source file and its declarations in other files. Sharing a variable is done the same way.

Until now we wrote:

Int I; /*declares I and fines it as well */

Declares I as variable of type int, and defines I by causing the compiler to set aside space for it.

To declare i without defining it, we must put the keyword extern at the beginning of its declaration:

Extern int I; /* declares I without defining it */

Extern informs the compiler that I is defined elsewhere in the program (most likely in a different source file), so there’s no need to allocate space for it.

Extern works with variables of all types. We can use it for array.

Extern int a[];

Compiler doesn’t allocate space for a at this time, so there’s no need for it to know a’s length.

To share a variable I among several source files, we first put a definition of I in one file:

Int I;

If i needs to be initialized, the initializer would go here. When this file is compiled, the compiler will allocate storage for i. The other files will contain declarations of I:

extern int i;

By declaring I in each file, it becomes possible to access and/or modify I within those files. Because of extern, the compiler doesn’t allocate additional storage for I each time one of the files is compiled.

Could have problem ensuring that all declarations of a variable agree with the definition.

When declarations of the same variable appear in different files, the compiler can’t check that the declarations match the varaibles’s definition.

Ex. One file may contain:

Int I;

While another contains the declaration:

Extern long int I;

To avoid this, declarations of shared variables are usually put in header files. A source file that needs access to a particular variable can include the header file. Each header file that contains a variable declaration is included in the source file that contains the variables definition enabling the compiler to check that the 2 match.

Nested Includes

A header file may itself contain #include directives.

Ex. If in a header file stack.h, etc you have

Bool is_empty(void)

You will need to include the file Boolean.h in stack.h sot that the definition of Bool is available when stack.h is compiled.

//array.h

void readarray (float[], int);

void printarray (const float[], int);

float sumarray (const float[], int);

//array.c

#include <stdio.h>

#include "array.h"

void readarray(float x[], int n){

int i;

printf("\nEnter five numbers: ");

for(i=0;i<n;i++)

scanf("%f", &x[i]);

}

//***************************************************************

float sumarray (const float x[], int n){

float sum = 0;

int i;

for (i=0; i<n; i++) sum += x[i];

return sum;

}

//***************************************************************

void printarray (const float x[], int n){

printf("\nThe data values are:\n");

int i;

for (i=0; i<n; i++)

printf (" %f\n", x[i]);

}

//arraydriver.c

#include "array.h"

#include <stdio.h>

int main(){

int n=5;

float data [5] = {0.0};

readarray(data, n);

printarray(data, n);

printf("\nThe sum is %f\n", sumarray(data, n));

return 0;

}

Protecting Header Files

If a source file includes the same header file twice, compilation errors may result. Common problem when header files include other header files.

Ex.

File1.h includes file 3.h

File2.h includes file 3.h

Prog.c includes file1.h and file2.h

When prog.c is compiled file3.h will be compiled twice.

Doesn’t always cause difficulty – only if it contains a type definition. (macro definitions, function prototypes, and/or variable declarations are fine).

To be safe – good idea to protect all header files, so we don’t have to worry later if add typedef. It also saves some time by avoiding unnecessary recompilation of the same header file.

Protecting boolean.h:

#ifndef BOOLEAN_H

#define BOOLEAN_H

#define TRUE 1

#define FALSE 0

typedef int Bool;

#endif

The first time the file is included, the BOOLEAN_H macro won’t be defined, so the preprocessor will allow the lines between #ifndef and #endif to stay. If the file is included a second time, the preprocessor will remove the lines between #ifndef and #endif.

The name of the macro BOOLEAN_H doesn’t really matter. However, making it resemble the name of the header file is a good way to avoid conflicts with other macros. Since we can’t name the macro BOOLEAN.H (identifiers can’t contain periods), a name such as BOOLEAN_H is a good alternative.

Example: RPN – Reverse Polish notation

Operators follow operands. Let’s write a simple calculator.

30 5 – 7 *

(answer = 175)

Need to use a stack. Did Stacks by external variables chapter 10.

If program reads a number, we’ll have it push the number onto the stack. If it reads an operator, we’ll have it pop two numbers from the stack, perform the operation, then push the result back onto the stack. When the program reaches the end of the user’s input, the value of the expression will be on the stack.

Ex. 30 5 – 7 *

1. Push 30 onto the stack.

2. Push 5 onto the stack.

3. Pop the top two numbers from the stack, subtract 5 from 30, giving 25, and then push the result back onto the stack.

4. Push 7 onto the stack.

5. Pop the top two numbers from the stack, multiply them, and then push the result back onto the stack.

After these steps the stack will contain the value of the expression (175).

Main will contain the loop to

Read a “token” (a number or an operator).

If the token is a number, push it on a stack.

If the token is an operator, pop its operands from the stack, perform the operation, and push the result back onto the stack.

When dividing a program like this one into files, it makes sense to put related function sand variables into the same file. The function that reads tokens could go into one source file (token.c), together with any functions that have to do with tokens. Stack-related functions such as push, pop, make_empty, is_empty, and is_full could go into a different file, stack.c. The variable that represents the stack would also go into stack.c. The main function would go into yet another file calc.c.

Advantages to splitting a program into multiple sources files:

· Grouping related functions and variables into a single file helps clarify the structure of the program.

· Each source file can be compiled separately – a great time-saver if the program is large and must be changed frequently.

· Functions are more easily reused in other programs when grouped in separate source files. Ex. By us separating stack.c and token.c from main makes it simpler to reuse the stack functions and token functions in the future.

Prototypes for stack.c functions should go into the header file stack.h

Void make_empty(void);

Int is_empty(void);

Int Is_full(void);

Void push(int i);

Int pop(void);

(could also have used Bool)

include stack.h in calc.c so that the compiler will know each function’s return type and the number and type of its parameters. We’ll also include stack.h in stack.c so the compiler can check that the prototypes in stack.h match the definitions in stack.c.

Note STACK_H macro protects STACK.h from being included more than once.

Contents of stack.h file:

#ifndef STACK_H

#define STACK_H

void make_empty(void);

int is_empty(void);

int is_full(void);

void push(int i);

int pop(void);

#endif

Contents of calc.c file:

#include <ctype.h>

#include <stdio.h>

#include "stack.h"

#define MAX_LEN 20 /* limits length of a token */

char current_char = ' '; /* retains character between calls of read_token */

int read_token(char *token, int n);

int convert_to_number(const char *token, int *value);

void error(const char *msg);

main()

{

int op1, op2, result;

char token[MAX_LEN+1];

make_empty();

for (;;) {

switch (read_token(token, MAX_LEN)) {

case -1: /* token too long */

error("Token too long");

continue;

case 0: /* end of input line */

if (is_empty())

return 0; /* entering an empty line terminates program */

current_char = ' '; /* discard new-line character at end of line */

result = pop();

if (!is_empty()) {

printf("*** Malformed expression (not enough operators) ***\n\n");

make_empty();

continue;

}

printf("Value: %d\n\n", result);

break;

case 1:

if (!isdigit(token[0])) { /* must be an operator */

if (is_empty()) {

error("Malformed expression (not enough operands)");

continue;

}

op1 = pop();

if (is_empty()) {

error("Malformed expression (not enough operands)");

continue;

}

op2 = pop();

switch (token[0]) {

case '+': push(op2 + op1);

break;

case '-': push(op2 - op1);

break;

case '*': push(op2 * op1);

break;

case '/': push(op2 / op1);

break;

default: error("Illegal operator");

continue;

}

break;

}

/* FALL THROUGH */

default: /* operand is a number */

if (is_full()) {

error("Expression too complicated");

continue;

}

if (!convert_to_number(token, &op1)) {

error("Illegal operand");

continue;

}

push(op1);

break;

}

}

}

int read_token(char *token, int n)

{

/* Assumptions: Tokens are separated by one or more blanks.

* Operands may begin with + or -.

* Returns length of token (-1 indicates token too long, 0 indicates end of line)

*/

int i = 0;

while (current_char == ' ')

current_char = getchar(); /* skip blanks */

while (current_char != ' ' && current_char != '\n') {

if (i == n)

return -1; /* token too long */

token[i++] = current_char;

current_char = getchar();

}

token[i] = '\0';

return i;

}

int convert_to_number(const char *token, int *value)

{

/* No check for numeric overflow. Returns 1 if number is valid; 0 otherwise. */

int i = 0, sign = 1;

/* see if there's a sign */

if (token[0] == '+')

i++;

else if (token[0] == '-') {

sign = -1;

i++;

}

*value = 0;

do {

if (!isdigit(token[i]))

return 0; /* not a number */

*value = *value * 10 + token[i] - '0';