📚 Chapter 7 🟡 Intermediate ⏱ 60–90 min

File Handling &
Preprocessor Directives

Make data survive past program exit, and control exactly what the compiler sees before it ever runs — from fopen() to include guards.

🎯 Learning Objectives
Open, read, write, and close files safely
Choose the correct file mode and I/O function
Work with binary files and file position
Understand the C preprocessor and macros
Use conditional compilation correctly
Protect headers with include guards
1File Handling Basics

Why File Handling Exists

Every variable you’ve used so far lives only in RAM. When your program exits, that memory is reclaimed and the data is gone — int age = 20; simply disappears the moment main() returns.

✗ Without files — data dies with the program
int age = 20; /* program ends → age is gone forever */
✓ With files — data survives
fprintf(fp, "age=%d\n", 20); /* program ends → still in age.txt next time */

Some data must survive program exit: a student database, bank records, a sensor log accumulated over days, configuration settings that should persist after a reboot. The solution is the file — data stored permanently on a storage device (SSD, HDD, SD card, flash memory).

📊

Student Database

Records that need to exist across every run of the program, not just the current session.

📊

Sensor Logs

Temperature readings accumulated over days or weeks for later analysis.

⚙️

Configuration

Settings that should persist even after the device is powered off and on again.

💾

Firmware Images

The actual binary that gets flashed onto a microcontroller — itself a file.

2File Handling Basics

What is a File? Streams

PropertyMemory (RAM)File
LifetimeTemporary — lost when program endsPermanent — survives program exit
SpeedVery fastSlower
Examplesint age;students.txt, firmware.bin

C accesses files through streams — think of a stream as a flow of data between your program and a source or destination, whether that’s a keyboard, monitor, or file on disk.

StreamPurposeExample function
stdinStandard inputscanf() reads from it
stdoutStandard outputprintf() writes to it
stderrStandard error outputUsed for error messages, kept separate from stdout
💡
You’ve Already Used Streams
Every printf() and scanf() call you’ve written since Chapter 1 has been using streams —stdout and stdin respectively. File handling simply extends this same stream-based model to disk files.
3File Handling Basics

FILE* & Opening Files

Every file operation goes through a FILE pointer — a handle that represents an open file and tracks its current position, mode, and error state internally.

C — Declaring a FILE pointer
FILE *fp;     /* fp is a "handle" to an open file */

To actually open a file, use fopen() — it returns a valid FILE* on success, or NULL on failure.

C — Opening a file and ALWAYS checking the result
FILE *fp = fopen("data.txt", "r");   /* filename, mode */

if(fp == NULL)
{
    printf("Unable to open file\n");
    return 1;          /* never assume the file opened successfully */
}
Never Skip the NULL Check
A file might not exist, you might lack permission, the disk might be full, or the path might be wrong. Dereferencing a NULL FILE* (e.g. calling fprintf(fp, ...) on it) is undefined behaviour and a guaranteed crash.
4File Handling Basics

Closing Files & File Modes

Always close a file when you’re done with it —fclose() flushes any buffered data to disk, releases the operating system resources tied to that handle, and prevents resource leaks.

C — fclose syntax
FILE *fp = fopen("data.txt", "r");
/* ... use fp ... */
fclose(fp);

The Six File Modes

ModeReadWriteCreates file?Erases existing?
"r"✗ (must exist)
"w"
"a"
"r+"✗ (must exist)
"w+"
"a+"
⚠️
"w" Mode Destroys Existing Data
Opening a file in "w" mode truncates it to zero length immediately —even before you write anything. If you meant to preserve existing content and only add new data, you wanted "a" (append), not "w".
5File Handling Basics

fprintf() & fscanf()

These are the file-aware counterparts of printf() and scanf() you already know —they take an extra FILE* argument telling them where to write or read.

C — Complete write-then-read example
#include <stdio.h>

int main()
{
    /* Writing */
    FILE *fp = fopen("marks.txt", "w");
    if(fp == NULL) return 1;

    fprintf(fp, "Alice %d\n", 95);
    fclose(fp);

    /* Reading it back */
    fp = fopen("marks.txt", "r");
    if(fp == NULL) return 1;

    char name[30];
    int  marks;
    fscanf(fp, "%s %d", name, &marks);
    printf("%s scored %d\n", name, marks);   /* Alice scored 95 */

    fclose(fp);
    return 0;
}
Output
Alice scored 95

The File Operation Lifecycle

fopen()
get a FILE*
Check NULL
verify success
Read / Write
fprintf, fscanf...
fclose()
always last
6Advanced File I/O

Character I/O: fgetc() & fputc()

Sometimes you need to process a file one character at a time. fgetc() reads a single character; fputc() writes one.

C — Reading an entire file character by character
int fgetc(FILE *fp);     /* returns the character read, or EOF */

FILE *fp = fopen("data.txt", "r");
int ch;

while((ch = fgetc(fp)) != EOF)
{
    printf("%c", ch);     /* prints every character until EOF */
}
fclose(fp);
⚠︙
Use int, Not char, to Store fgetc’s Return Value
EOF is typically -1, which doesn’t fit reliably in an unsigned/signed-ambiguous char on every platform. Always store the return value of fgetc() in an int to compare it against EOF correctly.
C — fputc writing characters one by one
int fputc(int ch, FILE *fp);

FILE *fp = fopen("letters.txt", "w");
fputc('A', fp);
fputc('B', fp);
fputc('C', fp);
fclose(fp);
/* letters.txt now contains: ABC */
7Advanced File I/O

String I/O: fgets() & fputs()

Reading character-by-character is slow for long text. fgets() reads an entire line at once; fputs() writes a whole string.

C — fgets and fputs
char *fgets(char *str, int size, FILE *fp);
int   fputs(const char *str, FILE *fp);

/* Reading one line, safely bounded by sizeof(line) */
char line[100];
FILE *fp = fopen("data.txt", "r");
fgets(line, sizeof(line), fp);
printf("%s", line);
fclose(fp);

/* Writing a string */
fp = fopen("output.txt", "w");
fputs("Embedded Systems", fp);
fclose(fp);
Character I/OString I/O
fgetc() — one characterfgets() — one line
fputc() — write one characterfputs() — write one string
8Advanced File I/O

Binary Files

Everything so far has stored text. Sometimes you need to store raw memory exactly as it exists —images, audio, firmware images, sensor logs, or entire structures.

PropertyText FileBinary File
Human readable?YesNo
StoresCharacters (text representation)Raw bytes (memory as-is)
Typical sizeLargerUsually smaller
Best forLogs, configuration, easy editingStructures, speed, exact memory layout
💡
The "b" in Mode Strings
Add b to a mode string ("wb", "rb") to open a file in binary mode. This matters most on Windows, where text mode performs newline translation that would corrupt raw binary data.
9Advanced File I/O

fwrite() & fread()

fwrite() writes raw binary data; fread() reads it back. Both work in terms of element size and count, making them perfect for writing entire structures or arrays in one call.

C — Writing and reading a structure in binary
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
size_t fread (      void *ptr, size_t size, size_t count, FILE *fp);

struct Student { int roll; char name[30]; float marks; };

/* Writing */
struct Student s = {101, "Alice", 92.5};
FILE *fp = fopen("student.dat", "wb");
fwrite(&s, sizeof(struct Student), 1, fp);   /* 1 record */
fclose(fp);

/* Reading it back */
struct Student loaded;
fp = fopen("student.dat", "rb");
fread(&loaded, sizeof(struct Student), 1, fp);
fclose(fp);
/* loaded now contains exactly what was saved */
⚠️
Always Check fwrite/fread’s Return Value
Both functions return the number of elements actually read or written —which can be less than requested if the disk is full or the file ended early. if(fread(&s, sizeof(s), 1, fp) != 1) { /* handle short read */ }
Never Mix Text and Binary Functions
Writing a structure with fprintf() produces a text representation, not the raw memory layout —and reading binary data back with fscanf() will misinterpret the bytes. Text functions for text files, binary functions (fwrite/fread) for binary files —never cross the streams.
10Advanced File I/O

File Position & fseek()

Every open file maintains a current position —an internal cursor that advances automatically as you read or write. Three functions let you inspect and control this cursor: ftell(), rewind(), and fseek().

A
B
C
D
E

After reading “ABC”, the position cursor sits at index 3 (pointing at ‘D’) —exactly what ftell() would report.

C — ftell, rewind, and fseek
long ftell(FILE *fp);                     /* current position    */
void rewind(FILE *fp);                    /* jump to position 0  */
int  fseek(FILE *fp, long offset, int origin);

/* Example: file contains "ABCDE" */
fgetc(fp); fgetc(fp); fgetc(fp);   /* read "ABC" */
printf("%ld\n", ftell(fp));         /* 3 — position after 3 reads */

rewind(fp);                          /* back to the very start */

fseek(fp, 5, SEEK_SET);             /* jump to byte 5 from start */
fseek(fp, -2, SEEK_END);            /* 2 bytes before the end    */
fseek(fp, 3, SEEK_CUR);             /* 3 bytes forward from here */
Origin constantMeaning
SEEK_SETOffset measured from the beginning of the file
SEEK_CUROffset measured from the current position
SEEK_ENDOffset measured from the end of the file (usually negative)
💡
A Common Trick: Finding File Size
fseek(fp, 0, SEEK_END); long size = ftell(fp); rewind(fp); — jump to the end, read the position (which equals the total size), then rewind back to the start before reading.
11Advanced File I/O

EOF Handling

Every file has an end, and your program must detect it correctly. EOF (End Of File) is a special value most input functions return once there’s nothing left to read.

✗ Common but WRONG pattern
while(!feof(fp)) { fgets(line, 100, fp); printf("%s", line); } /* feof() is set only AFTER a failed read — this can print one extra garbage iteration */
✓ Correct pattern
while(fgets(line, 100, fp)) { printf("%s", line); } /* loop based on the input function's OWN return value — no extra iteration */
C — The correct fgetc EOF loop
int ch;
while((ch = fgetc(fp)) != EOF)
{
    putchar(ch);
}
⚠️
Why while(!feof(fp)) Is a Trap
feof() only becomes true after a read operation has already attempted to go past the end of the file —not before. Checking it as your loop condition before attempting the read can cause one extra (invalid) iteration. Always loop on the return value of the read function itself.
12File Handling Review

Common Mistakes

✗ Forgetting fclose()
FILE *fp = fopen("data.txt", "w"); fprintf(fp, "Hello"); /* fclose missing — resources leak, data may not even be flushed! */
✓ Always close what you open
FILE *fp = fopen("data.txt", "w"); fprintf(fp, "Hello"); fclose(fp);
✗ Using fp before checking NULL
FILE *fp = fopen("x.txt", "r"); fprintf(fp, "Hello"); /* crash if fp is NULL */
✓ Check first, use second
FILE *fp = fopen("x.txt", "r"); if(fp == NULL) return 1; fprintf(fp, "Hello");
✗ Wrong mode destroys existing data
FILE *fp = fopen("log.txt", "w"); /* meant to ADD to the log — "w" truncates it to empty first! */
✓ Use append mode to preserve data
FILE *fp = fopen("log.txt", "a"); /* existing log entries preserved */
✗ Reading from a write-only file
FILE *fp = fopen("x.txt", "w"); fscanf(fp, "%d", &n); /* "w" mode is write-only — fails */
✓ Open with the mode you intend to use
FILE *fp = fopen("x.txt", "r+"); fscanf(fp, "%d", &n); /* read+write mode */
13File Handling Review

Best Practices & Memory Tricks

Always check fopen’s return

Never assume a file opened successfully —test against NULL every single time before using the handle.

Close every file you open

Pair every fopen() with exactly one fclose(), even on early-return error paths.

Choose the right mode

Use "a" to preserve existing content; use "w" only when you intend to start fresh.

Match function type to file type

Text functions (fprintf/fscanf) for text files; binary functions (fwrite/fread) for binary files.

open → use → close
fopenhandle
fprintf/fscanftext I/O
fcloserelease

“Every fopen needs exactly one fclose”

FunctionMemory hook
FILE*A handle — not the file itself, just a reference to it
fopen()Open — returns the handle, or NULL on failure
fprintf() / fscanf()Text write / text read
fgetc() / fputc()One character at a time
fgets() / fputs()One line / one string at a time
fwrite() / fread()Binary write / binary read
fseek() / ftell() / rewind()Move position / report position / jump to start
fclose()Close — flush and release
14File Handling Review

Interview Questions

Q1
What is the difference between fgetc()/fgets() and fread()?
fgetc() and fgets() are text-oriented —they read characters or lines and are typically used with text files. fread() is binary-oriented —it reads a specified number of raw bytes (often a whole structure or array) exactly as they're stored in memory, regardless of what those bytes represent.
Q2
What happens if fopen() fails, and how should you handle it?
fopen() returns NULL on failure (file not found, no permission, disk error, invalid path). You must always check if(fp == NULL) immediately after calling fopen() and handle the error (print a message, return an error code) before attempting any read/write operation on that handle.
Q3
Why is while(!feof(fp)) considered a bug pattern?
feof() only becomes true after a read operation has already tried and failed to read past the end of the file —not proactively before each iteration. Using it as a loop condition can cause one extra, invalid iteration with stale or garbage data. The correct pattern loops directly on the return value of the read function itself, e.g. while(fgets(line, size, fp)).
Q4
What's the difference between "w" and "a" mode?
"w" truncates the file to zero length immediately on open (destroying any existing content) before allowing writes. "a" preserves all existing content and positions every write at the end of the file. Both create the file if it doesn't already exist.
Q5
How would you find the size of a file in C?
Use fseek(fp, 0, SEEK_END) to move the position to the end of the file, then call ftell(fp) —which now returns the total byte count (since the position equals the size when at the end). Follow with rewind(fp) to reset the position back to the beginning before reading.

Frequently Asked Questions

Why must I check the return value of fread() and fwrite()?
Both functions return the number of complete elements actually transferred, which can legitimately be less than the count you requested —the disk might be full, the file might end early, or an I/O error might occur partway through. Silently assuming the full count succeeded can lead to your program processing incomplete or corrupted data without ever knowing something went wrong.
Should I use text files or binary files for storing structures?
Binary files (via fwrite/fread) are generally faster and more compact for storing structures, since the raw memory is written and read back directly with no text conversion overhead. Text files (via fprintf/fscanf) are human-readable and easier to inspect, debug, or edit by hand, but are slower and less compact. For configuration files and logs that humans need to read, prefer text. For large datasets or speed-critical applications, prefer binary.
15The Preprocessor

What is the Preprocessor?

Before your C source code is actually compiled, it passes through a separate program called the preprocessor. The preprocessor does not compile anything —it transforms your source text, preparing it for the compiler that runs afterward.

Source (.c)
your code
Preprocessor
expands directives
Compiler
C → assembly
Assembler
assembly → object
Linker
→ executable

The preprocessor handles tasks like inserting header file contents, replacing macros with their definitions, conditionally including or excluding code, and stripping out comments. It recognizes any line beginning with # as a preprocessor directive.

DirectivePurpose
#includeInsert the contents of a header file
#defineDefine a macro
#undefRemove a macro definition
#ifConditional compilation based on an expression
#ifdefConditional compilation: only if a macro exists
#ifndefConditional compilation: only if a macro doesn’t exist
#else / #elifAlternative branches for conditional compilation
#endifEnds a conditional compilation block
16The Preprocessor

#include & Header Files

printf() is declared in stdio.h. To use it, you tell the preprocessor to insert that header’s contents directly into your source file before compilation begins.

System Header (angle brackets)
#include <stdio.h> /* searches the compiler's system include directories */
User Header (quotes)
#include "myheader.h" /* searches the project directory first, then system directories */
C — A complete header/source/main split
/* math_utils.h — declarations only */
int add(int a, int b);
int subtract(int a, int b);

/* math_utils.c — implementation */
#include "math_utils.h"
int add(int a, int b)      { return a + b; }
int subtract(int a, int b) { return a - b; }

/* main.c — usage */
#include <stdio.h>
#include "math_utils.h"

int main()
{
    printf("%d\n", add(5, 3));   /* 8 */
    return 0;
}

A Header File Typically Contains

  • Function declarations (prototypes)
  • Macros and constants
  • Type definitions (typedef)
  • Structure and enumeration declarations
💡
Why Header Files Matter
Without headers, every source file that wants to use add() would need to duplicate its declaration manually —a maintenance nightmare across a multi-file project. Headers centralize the “public interface” of each module, improving organization, reusability, and maintainability.
17The Preprocessor

#define & Object-like Macros

#define creates a macro —a pure text substitution performed by the preprocessor before the compiler ever sees your code.

C — Object-like macros: literal text replacement
#define PI 3.14159
#define MAX 100

float area = PI * r * r;
int   arr[MAX];

/* The compiler never sees PI or MAX — after preprocessing it sees: */
float area = 3.14159 * r * r;
int   arr[100];
💡
Macros Replace Magic Numbers
Instead of scattering the literal 120 through your code, write #define MAX_SPEED 120 once and use if(speed > MAX_SPEED) everywhere. If the limit ever changes, you update one line instead of hunting through the entire codebase.
18The Preprocessor

Function-like Macros

Macros can accept parameters too, making them look like function calls —but remember, they’re still pure text substitution, not real function calls.

C — SQUARE and MAX macros
#define SQUARE(x) ((x) * (x))
#define MAX(a,b)  ((a) > (b) ? (a) : (b))

int a = SQUARE(5);          /* expands to: ((5) * (5))  → 25 */
int m = MAX(10, 20);        /* expands to: ((10)>(20)?(10):(20)) → 20 */

Why Parentheses Are Non-Negotiable

✗ Missing parentheses — wrong answer
#define SQUARE(x) x*x SQUARE(2+3) /* expands to: 2+3*2+3 → 11 (!!) */ /* operator precedence breaks everything */
✓ Fully parenthesized — correct answer
#define SQUARE(x) ((x)*(x)) SQUARE(2+3) /* expands to: ((2+3)*(2+3)) → 25 */ /* correct, regardless of caller's expression */
Always Parenthesize Every Parameter AND the Whole Expression
Macros are textual substitution —the preprocessor has no understanding of operator precedence beyond what your parentheses enforce. Without them, a caller passing a multi-term expression (like 2+3) silently corrupts the result.

A Subtler Trap: Side Effects

C — A macro argument can be evaluated more than once
#define MAX(a,b) ((a) > (b) ? (a) : (b))

int x = 5;
int y = MAX(x++, 10);
/* x++ may be expanded (and evaluated) TWICE in the comparison —
   the macro's textual nature means side-effecting arguments
   behave unpredictably depending on which branch is taken */
⚠️
Prefer Functions When Arguments Have Side Effects
A real function call evaluates each argument exactly once before the call happens, no matter what the function does internally. A macro expands its arguments wherever they appear in the definition —potentially multiple times. Never pass an expression with side effects (like x++ or a function call with state) into a macro whose definition might reference that parameter more than once.
19The Preprocessor

Macro vs Function

AspectMacroFunction
Processed byPreprocessor (text substitution)Compiler, then executed at runtime
Call overheadNoneFunction call overhead
Type checkingNoneYes
Argument evaluationMay happen multiple times (or zero)Always exactly once, before the call
Debugger visibilitySees only the expanded codeSees the actual function and its name
Modern C Often Prefers Alternatives
For simple constants, const or enum give the compiler type information that #define never does. For small reusable expressions, an inline function (where supported) gets the safety of a real function with comparable performance. Macros remain valuable for things only the preprocessor can do —conditional compilation, stringification, and code that must work across multiple types without true generics.
20Conditional Compilation

#if / #ifdef / #ifndef

Sometimes different parts of a program should be compiled under different conditions —different operating systems, debug vs release builds, or different hardware targets. This is conditional compilation: code is included or excluded entirely before compilation even happens, unlike a runtime if statement which always gets compiled.

Runtime if (always compiled)
if(debug) { printf("Debug"); } /* This code exists in the binary either way — just may not run */
Conditional compilation (removed entirely)
#ifdef DEBUG printf("Debug"); #endif /* If DEBUG isn't defined, this code never even reaches the compiler — zero binary cost */
C — #if, #ifdef, #ifndef, #else, #elif, #undef
/* #if — evaluate a constant expression */
#define VERSION 2
#if VERSION == 2
    printf("Version 2");
#endif

/* #ifdef — compile only if macro IS defined */
#define DEBUG
#ifdef DEBUG
    printf("Debug Enabled");
#endif

/* #ifndef — compile only if macro is NOT defined */
#ifndef DEBUG
    printf("Release Mode");
#endif

/* #else and #elif — like else / else-if, but for the preprocessor */
#if VERSION == 1
    printf("V1");
#elif VERSION == 2
    printf("V2");           /* this branch compiles */
#else
    printf("Unknown");
#endif

/* #undef — remove a macro definition */
#define SIZE 100
#undef SIZE
/* SIZE is now undefined again */
DirectiveChecks
#ifdef MACROIs MACRO currently defined?
#ifndef MACROIs MACRO currently not defined?
#if expressionDoes a constant expression evaluate to non-zero?
21Conditional Compilation

Include Guards

One of the most important patterns in all of C. Imagine main.c includes student.h, which itself includes config.h —and some other file also includes config.h directly. Without protection, the compiler processes config.h’s declarations multiple times, causing redefinition errors.

C — The include guard pattern
#ifndef STUDENT_H      /* has STUDENT_H been defined yet? */
#define STUDENT_H       /* if not, define it now           */

struct Student
{
    int roll;
};

#endif                  /* end of the guarded block         */

How It Plays Out Across Multiple Includes

1st #include
STUDENT_H undefined → define it, compile contents
2nd #include
STUDENT_H already defined → skip entirely
💡
Modern Alternative: #pragma once
Many compilers support #pragma once as a one-line shortcut for the same effect. It’s widely supported but not part of the ISO C standard —traditional include guards (#ifndef/#define/#endif) remain fully portable across every standard-conforming compiler.
Every Header File Needs a Guard
Skipping the guard in even one header can cause “multiple definition” compile errors the moment your project grows past a single source file that includes it more than once (directly or indirectly).
22Conditional Compilation

Project Organization & Compiler Workflow

Large C programs split across multiple files —each module gets its own .c (implementation) and .h (declarations) pair.

A typical embedded project layout
Project/
├── main.c
├── uart.c
├── uart.h
├── gpio.c
├── gpio.h
├── adc.c
├── adc.h
└── Makefile
📝

Easier Maintenance

Each module is small and focused; bugs are easier to isolate and fix.

Code Reuse

A well-organized uart.c/uart.h pair can be dropped into a new project as-is.

Faster Compilation

Only the files that changed need to be recompiled, not the entire project.

🤝

Team Development

Different engineers can work on gpio.c and adc.c simultaneously without conflicts.

Complete Compiler Workflow

main.c, math.c
source files
Preprocessor
expand each separately
Compiler
→ assembly each
main.o, math.o
object files
Linker
combine → executable
StagePurpose
PreprocessorExpands #include, #define, and conditional directives
CompilerTranslates expanded C source into assembly code
AssemblerConverts assembly into machine-readable object code
LinkerCombines all object files and libraries into one executable
LoaderLoads the finished executable into memory to run it
🔌
Embedded Firmware Example
A firmware project might have main.c, gpio.c, uart.c, timer.c, and adc.c — each peripheral driver compiled separately, then linked together into the final firmware image. Only the modules the application actually uses get linked in, keeping the binary lean.
23Review

Common Mistakes

✗ Forgetting include guards
/* config.h, no guard */ struct Config { int x; }; /* included twice → "multiple definition" compile error */
✓ Guard every header
#ifndef CONFIG_H #define CONFIG_H struct Config { int x; }; #endif
✗ Including a .c file
#include "math.c" /* pulls in implementation — duplicate symbols if math.c is also compiled separately */
✓ Always include the .h
#include "math.h" /* declarations only — implementation compiled and linked separately */
✗ Forgetting macro parentheses
#define SQUARE(x) x*x SQUARE(2+3) /* → 2+3*2+3 = 11 */
✓ Parenthesize everything
#define SQUARE(x) ((x)*(x)) SQUARE(2+3) /* → ((2+3)*(2+3)) = 25 */
✗ Confusing #if with runtime if
#if debug /* WRONG — debug is a runtime variable, not a preprocessor constant; this is a compile error */ printf("Debug"); #endif
✓ #ifdef checks a macro, not a variable
#ifdef DEBUG /* DEBUG must be #defined, not a runtime variable */ printf("Debug"); #endif
24Review

Best Practices & Memory Tricks

Uppercase macro names

#define BUFFER_SIZE 256 —visually distinguishes macros from regular variables and function names at a glance.

Parenthesize macro parameters

Wrap every parameter and the entire replacement expression in parentheses to protect against operator precedence surprises.

Keep macros simple

If a macro is getting complex or has side-effect risks, it’s usually a sign you want a real (possibly inline) function instead.

Guard every header file

No exceptions —every .h file needs an #ifndef/#define/#endif guard or #pragma once.

Declarations in .h, code in .c

Headers expose the public interface; source files contain the actual implementation that gets compiled once.

Use conditional compilation deliberately

Reach for #ifdef for platform-specific code and debug builds —not as a substitute for ordinary runtime logic.

#ifdef vs #ifndef
#ifdefif defined
#ifndefif NOT defined

“The extra n means NOT”

ConceptMemory hook
#includeInsert file — literally pastes the header’s text in
#defineText replacement — before compilation, not at runtime
MacroExpanded before the compiler ever sees it
CompilerOnly sees code after the preprocessor has run
Header (.h)Declarations — the “menu”
Source (.c)Implementation — the “kitchen”
25Review

Interview Questions

Q1
What is the C preprocessor, and when does it run?
The preprocessor is a program that processes all lines beginning with # (preprocessor directives) before actual compilation begins. It expands #include files, substitutes #define macros, evaluates conditional compilation blocks, and strips comments —producing an "expanded" source file that the compiler then translates into assembly.
Q2
What is the difference between a macro and a function?
A macro is expanded by the preprocessor as pure text substitution before compilation —it has no function-call overhead and no type checking, and its arguments may be evaluated multiple times (or zero times) depending on how they're used in the definition. A function is compiled once and executed at runtime, with each argument evaluated exactly once before the call, and full compiler type checking on its parameters and return value.
Q3
Why must function-like macro parameters be fully parenthesized?
Because macro expansion is purely textual, the preprocessor doesn't understand operator precedence the way the compiler does for real expressions. #define SQUARE(x) x*x expands SQUARE(2+3) into 2+3*2+3 (= 11, due to multiplication's precedence), not the intended 25. Wrapping every parameter and the whole expression in parentheses —((x)*(x)) —forces the correct grouping regardless of what expression the caller passes in.
Q4
Why are include guards necessary? How do they work?
Without an include guard, a header file included (directly or transitively) more than once in the same translation unit causes the compiler to see its declarations multiple times, producing "redefinition" errors. The guard pattern (#ifndef NAME / #define NAME / ... / #endif) checks whether a unique macro has already been defined; on the first inclusion it defines the macro and compiles the contents, and on every subsequent inclusion the #ifndef check fails and the entire body is skipped.
Q5
What is the difference between conditional compilation and a runtime if statement?
Conditional compilation (#if/#ifdef/#ifndef) is resolved entirely by the preprocessor before compilation —code in a "false" branch is removed from the source entirely and never reaches the compiler, contributing zero size or overhead to the final binary. A runtime if statement is always compiled into the executable; the condition is evaluated while the program is actually running, and the unused branch still exists in the binary even if it never executes.

Frequently Asked Questions

Why do header files contain declarations but not implementations?
If a function's full implementation lived in a header and that header were included by multiple .c files, each translation unit would get its own copy of the function body, typically causing "multiple definition" linker errors. Keeping only declarations (prototypes) in headers lets many files share the interface, while the single implementation in one .c file gets compiled exactly once and linked in wherever needed.
Is #pragma once a safe replacement for traditional include guards?
It's supported by virtually every major compiler (GCC, Clang, MSVC) and is simpler to write, but it is not part of the ISO C standard —so it is technically a non-portable compiler extension. For maximum portability across any standard-conforming compiler, traditional #ifndef/#define/#endif guards remain the safer choice, though #pragma once is extremely common and reliable in practice.
Why does modern C code sometimes avoid macros in favor of const, enum, or inline functions?
Macros have no type information (the compiler doesn't know PI is meant to be a float), no scoping (a macro defined anywhere applies everywhere after that point, regardless of braces), and can silently misbehave with side-effecting arguments. const variables and enum constants give the compiler real type checking and proper scope; inline functions (where supported) get the performance benefits of a macro with the safety guarantees of a real function call. Macros remain essential for things only the preprocessor can do, like conditional compilation.
26Review

Practice Programs & Chapter Summary

🟢 Easy
  • Create a file and write your name to it using fprintf(), then read it back and print it.
  • Read a text file character by character with fgetc() and count how many characters it contains.
  • Copy one line from a file to the screen using fgets().
  • Define a macro for PI and a macro for the maximum array size; use both in a small program.
  • Create a header file with proper include guards, containing one struct declaration.
🔵 Medium
  • Store five student records (roll, name, marks) in a text file using fprintf(), then read and display them in a formatted table.
  • Count the number of words in a text file by reading it line by line.
  • Store an array of structures in a binary file with fwrite(), then read them back with fread() and verify the values match.
  • Use fseek() and ftell() to determine and print the size of a file in bytes.
  • Split a simple calculator program into calc.h and calc.c files, called from main.c.
🔴 Challenge
  • Build a student database program: add records, save them to a binary file, read all records back, search by roll number, and update an existing record.
  • Write a program using function-like macros for MAX, MIN, CUBE, and ABS of a number; compare the macro versions against equivalent functions and explain any differences in behavior with side-effecting arguments.
  • Organize a small embedded project with main.c, uart.c/uart.h, gpio.c/gpio.h, and timer.c/timer.h, with proper include guards in every header.
  • Write a program that uses conditional compilation (#ifdef DEBUG) to print extra diagnostic messages only when a DEBUG macro is defined, and verify the binary differs in size with and without it defined.
✅ What you mastered in Chapter 7
  • Files provide permanent storage that survives program exit; FILE* is a handle representing an open file.
  • fopen() returns NULL on failure —always check before using the handle. Always pair every fopen() with exactly one fclose().
  • Six file modes (r, w, a, r+, w+, a+) control whether a file is read, written, created, or truncated.
  • fprintf()/fscanf() handle text I/O; fgetc()/fputc() work character-by-character; fgets()/fputs() work line-by-line.
  • fwrite()/fread() handle raw binary data —never mix text and binary I/O functions on the same data.
  • ftell(), rewind(), and fseek() inspect and control a file's current position; SEEK_SET/SEEK_CUR/SEEK_END anchor the offset.
  • Loop on the return value of the read function itself —never on while(!feof(fp)), which can cause an extra invalid iteration.
  • The preprocessor runs before compilation, expanding #include files and #define macros, and resolving conditional compilation blocks.
  • Macros are pure text substitution —always parenthesize every parameter and the entire replacement expression to avoid precedence bugs.
  • Macros differ from functions: no call overhead or type checking, but arguments can be evaluated multiple times —avoid passing expressions with side effects.
  • #ifdef/#ifndef/#if/#elif/#else/#endif remove code entirely before compilation, unlike a runtime if which always compiles both branches.
  • Every header file needs an include guard (#ifndef/#define/#endif, or #pragma once) to prevent multiple-definition errors.
  • Large projects split into .h (declarations) and .c (implementation) file pairs, compiled separately and combined by the linker.
  • The full build pipeline: Preprocessor → Compiler → Assembler → Linker → Executable.
📚 Chapter 8 — Embedded C Programming
Connecting everything you've learned to real embedded systems
Embedded C vs Standard C
Memory-Mapped I/O
volatile Keyword
const in Embedded
Register Access
GPIO Programming
Interrupt Basics
Coding Standards
Real Embedded Examples