Week 11
This week covers the C preprocessor, conditional compilation, and the basics of multi-file programs.
The C Preprocessor
The C Preprocessor is a program that processes the source code before it is passed to the compiler. Its main functions are:
- Inclusion of other files.
- Definition of symbolic constants and macros.
- Conditional compilation of program code.
- Conditional execution of preprocessor directives.
Preprocessor directives always begin with a hash sign (#) and can only have
whitespace characters before them on a line.
The Build Process
The preprocessor is the first step in converting a source file to an executable.
graph TD
%% Define Styles - Transparent fill, thick colored strokes
%% The text color will inherit from your site theme (Light/Dark)
classDef file fill:transparent,stroke:#2196F3,stroke-width:3px;
classDef process fill:transparent,stroke:#FF9800,stroke-width:3px,rx:10,ry:10;
subgraph Preprocessing [Preprocessing Phase]
direction TB
A[Source File]:::file --> B(Preprocessor):::process
B --> C[Preprocessed File]:::file
end
C --> D(Compiler):::process
D --> E[Object File]:::file
subgraph Linking [Linking Phase]
direction TB
E --> F(Linker):::process
G[Library Code]:::file --> F
end
F --> H[Executable File]:::file
#include Directive
The #include directive causes a copy of the specified file to be included in
place of the directive itself. This is typically used to incorporate header
files that contain common declarations like function prototypes and struct
definitions.
| Format | Search Location | Usage |
|---|---|---|
#include <filename> |
Searches standard library for the file. | Use for standard library files. |
#include "filename" |
Searches the current directory, then the standard library. | Use for user-defined files. |
#define and Macros
The #define directive is used to create symbolic constants and
macros.
Symbolic Constants
When the program is compiled, all occurrences of the symbolic constant are replaced with the replacement text. Once a symbolic constant is created, it cannot be redefined.
// Format: #define identifier replacement-text
#define PI 3.14159
Macros with Arguments
A macro is an operation defined in a #define directive. A macro with
arguments has its arguments substituted for the replacement text when the macro
is expanded. Macros perform a text substitution, meaning no data type
checking is performed. A macro without arguments is treated like a symbolic
constant.
Macros vs. Functions
A macro performs a text substitution and does no data type checking, unlike functions.
Area Macro and the Importance of Parentheses
Always use parentheses around arguments and the entire macro body to avoid potential precedence issues after substitution.
#define PI 3.14159
int c = 2;
double area;
// --- FAILURE: Missing Parentheses ---
#define CIRCLE_AREA(x) PI * x * x
area = CIRCLE_AREA(c + 2);
// Expansion: area = 3.14159 * c + 2 * c + 2;
// Logic: (3.14159 * 2) + (2 * 2) + 2
// Result: 12.28318 (Incorrect)
#undef CIRCLE_AREA // Undefine the macro so we can redefine it
// --- SUCCESS: Correct Parentheses ---
#define CIRCLE_AREA(x) (PI * (x) * (x))
area = CIRCLE_AREA(c + 2);
// Expansion: area = (3.14159 * (c + 2) * (c + 2));
// Logic: 3.14159 * 4 * 4
// Result: 50.26544 (Correct)
#undef
#undef: Undefines a symbolic constant or macro. If undefined, it can later be redefined.
Conditional Compilation
Conditional compilation is used to control which parts of the program code
or which preprocessor directives are included or excluded before compilation.
The structure is similar to the if statement in C.
Directives
Every starting #if, #ifdef, or #ifndef must end with an #endif.
| Directive | Equivalent | Purpose |
|---|---|---|
**#if** constant-expression |
Compiles the block if the expression is non-zero. | |
**#ifdef** name |
#if defined(name) |
Compiles the block if the name is defined. |
**#ifndef** name |
#if !defined(name) |
Compiles the block if the name is not defined. |
#else |
Alternative block if the preceding condition is false. | |
#elif |
Else-if condition for checking multiple conditions. |
Note on Evaluation
Preprocessor directives cannot evaluate complex expressions like sizeof,
cast expressions, or enumeration constants.
Header Guards (Preventing Multiple Inclusion)
This pattern ensures a header file's contents are only processed once, preventing redefinition errors when the header is included in multiple source files.
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... contents of the header file
#endif
Debugging with Conditional Compilation
A frequent use case is enabling or disabling debug code.
#define DEBUG // Define DEBUG to enable the code block below.
// ... some program logic
#ifdef DEBUG
// This code only compiles if DEBUG is defined
printf("DEBUG: Variable x = %d\n", x);
#endif
// ... more program logic
To turn off the debug statements, simply remove or comment out the initial
#define DEBUG. The debugging statements are then ignored by the compiler.
Multi-Source File Programs
Large programs are often split across multiple source files. A function definition must exist entirely within one file (it cannot be split across files), but it can be called from others.
Sharing Global Variables: extern
Global variables are accessible to functions within the same file by default.
To use a global variable defined in one file in a different file, you must
use the extern keyword.
extern: Declares that a variable is defined in another file (External Linkage).- Function Prototypes: Can be used in other files without an
externstatement; you simply need a prototype in each file that uses the function (usually via a header file).
Using extern vs static
File 1 (one.c)
extern int value; // DECLARATION: Refers to 'value' defined in two.c
int val2; // DEFINITION: Global variable with external linkage
int main() {
// ...
}
File 2 (two.c)
int value; // DEFINITION: The actual storage for 'value'
static int val2; // DEFINITION: Global variable with INTERNAL linkage
// (only visible in two.c)
Limiting Scope: static Keyword
When the static keyword is used outside of all functions (at file scope),
it specifies that the variable or function has internal linkage. This means
it can only be used in the file in which it is defined, effectively hiding it
from other source files.
Compiling Multi-File Programs
Compiling every file in a large project is tedious if small changes have been made to only one file.
- Make Utility: On UNIX systems, the
makeutility is used to manage multi-file projects. It checks timestamps and recompiles only the files that have changed, linking them with the existing object files.
Storage Classes
The storage class of a variable determines three key properties:
- Duration (Lifetime): When the variable is created and destroyed.
- Scope: Where the variable can be referenced.
- Linkage: Whether the variable is accessible across multiple files.
| Storage Class | Duration | Scope | Linkage | Declaration Location |
|---|---|---|---|---|
automatic |
Automatic | Block | None | In a block (keyword auto is optional). |
register |
Automatic | Block | None | In a block with keyword register. |
static (Block) |
Static | Block | None | In a block with keyword static. |
static (File) |
Static | File | Internal | Outside all functions with keyword static. |
extern |
Static | File | External | Outside all functions (implied or explicit). |