c-foundations Lesson 4 20 min read

What Is the Structure of a C Program?

Preprocessor directives, data types, function prototypes — the anatomy of every C file

Reading: C Text: Ch. 2 §1 (pp. 66–74), Ch. 2 §4 (pp. 85–91)

After this lesson, you will be able to:

  • Organize a C source file into preprocessor directives, constants, prototypes, main, and definitions
  • Use #include to include standard library headers and explain its text substitution behavior
  • Define named constants with #define and const
  • Write function prototypes before main and explain why C requires them
  • Identify C’s primitive data types and their typical sizes
  • Explain why uninitialized local variables contain garbage values

Why Does C Need So Much Boilerplate?

In Java, you write System.out.println("Hello") and it works — the compiler knows what System.out is. In C, you write printf("Hello\n") and get an error: implicit declaration of function ‘printf’. Why?

Because C reads your file top to bottom, one pass. If it hasn’t seen printf yet, it doesn’t know it exists. That #include <stdio.h> at the top isn’t optional decoration — it’s literally pasting the declaration of printf into your file so the compiler can find it.

Understanding this top-to-bottom, one-pass rule explains everything about C program structure: why you need #include, why you need function prototypes, and why the order of your code matters.


The Five Sections of a C File

Every well-structured C file follows this pattern:

/* 1. Preprocessor directives */
#include <stdio.h>
#include <math.h>

/* 2. Constant definitions */
#define PI 3.14159265358979

/* 3. Function prototypes */
double calculate_area(double radius);
double calculate_circumference(double radius);

/* 4. Main function */
int main(void)
{
    double radius = 5.0;
    printf("Area: %.2f\n", calculate_area(radius));
    printf("Circumference: %.2f\n", calculate_circumference(radius));
    return 0;
}

/* 5. Function definitions */
double calculate_area(double radius)
{
    return PI * radius * radius;
}

double calculate_circumference(double radius)
{
    return 2.0 * PI * radius;
}

Let’s go through each section.

Section 1: Preprocessor Directives

Lines starting with # are processed before compilation. The preprocessor does text substitution — it’s not really “compiling,” just copying and replacing.

#include pastes the contents of a header file into your source:

#include <stdio.h>      // Standard I/O (printf, scanf)
#include <stdlib.h>     // Utilities (exit, atoi, calloc, free)
#include <string.h>     // String functions (strlen, strcmp, strcpy)
#include <math.h>       // Math functions (sqrt, pow, sin)
#include <ctype.h>      // Character functions (isalpha, toupper)
#include <stdbool.h>    // Boolean type (bool, true, false)

There are two forms of #include:

#include <stdio.h>      // Angle brackets: search system header directories
#include "myutils.h"    // Quotes: search current directory first, then system dirs

Use angle brackets for standard library headers and quotes for your own header files. You’ll create your own headers when we cover multi-file projects.

From Java: #include is NOT the same as Java’s import. Java’s import tells the compiler where to find a class by name. C’s #include literally copy-pastes the entire header file into your source code. That’s why it must come first — the compiler needs those declarations before it sees your code.

You can see exactly what the preprocessor does by running gcc -E:

gcc -E circle.c | head -20

This shows the file after preprocessing — all #include contents expanded, all #define substitutions made. It’s a useful debugging tool when macros behave unexpectedly.

Section 2: Constants

#define creates named constants via text substitution:

#define PI 3.14159265358979
#define MAX_SIZE 100
#define COURSE "CSCD 240"

Watch out: #define does NOT end with a semicolon or an =! #define PI 3.14; would replace PI with 3.14; everywhere — including inside expressions like PI * r * r, which becomes 3.14; * r * r (syntax error). And #define PI = 3.14 would replace PI with = 3.14.

#define can also create macro “functions,” but they have a nasty trap:

#define SQUARE(x) x * x          // Looks right, but it's broken

SQUARE(1+2) expands to 1+2 * 1+2 = 1 + 2 + 2 = 5, not 9. Operator precedence strikes. Always parenthesize both the parameter and the whole expression:

#define SQUARE(x) ((x) * (x))    // Correct: SQUARE(1+2) → ((1+2) * (1+2)) = 9

From Java: Java uses final static int MAX = 100; for constants. C’s #define MAX 100 does pure text substitution with no type checking. C also has const int MAX = 100; which is closer to Java’s approach and generally preferred when you need type safety.

Check Your Understanding
What does #include <stdio.h> actually do during compilation?
A Links the standard I/O library into your executable
B Copy-pastes the entire contents of the header file into your source code before compilation
C Tells the compiler to import the stdio module, like Java's import
D Downloads the standard I/O library from the system package manager
Answer: B. The preprocessor literally copies the contents of stdio.h into your file before the compiler sees it. This is text substitution, not module loading. That's why it must come first — the compiler reads top-to-bottom and needs to see printf's declaration before your code uses it. Linking (option A) happens later, in stage 4 of the pipeline.

You can also use const for typed constants:

const double PI = 3.14159265358979;
const int MAX_SIZE = 100;

const is generally preferred because it has a type and the compiler can catch type mismatches.

Section 3: Function Prototypes

Prototypes tell the compiler “this function exists, here’s its signature, I’ll define it later”:

double calculate_area(double radius);
double calculate_circumference(double radius);

Key Insight: C reads top-to-bottom. If you call a function before defining it, the compiler hasn’t seen it yet and will give a warning or error. Function prototypes solve this: they promise the compiler “this function exists, here’s its signature, I’ll define it later.”

The Trick: The pattern is: prototypes at top, main in the middle, function definitions at bottom. This lets you read main first to understand the program’s flow, then look at the function implementations if you need details.

Section 4: The main Function

Every C program starts at main:

int main(void)
{
    // Your program goes here
    return 0;
}
  • int — returns an exit status to the OS (0 = success)
  • void — takes no command-line arguments (for now)

Common Pitfall: In C, void greet() and void greet(void) are different! greet() means “takes an unspecified number of arguments” (an old C89 feature). greet(void) means “takes NO arguments.” Always use (void) when you mean no parameters.

Why does this matter?

The five-section file structure isn’t busywork — it’s dictated by how the compiler works. Once you internalize this pattern, you’ll never get “implicit declaration” warnings again, and your code will be readable by anyone who knows C. Every professional C codebase follows this same layout.

Check Your Understanding
You write a C program that calls calculate_area(5.0) in main, but define the function below main without a prototype. What happens when you compile with -Wall?
A Compiler warning about implicit declaration of calculate_area
B Linker error: "undefined reference to calculate_area"
C It compiles and runs correctly — the compiler finds the function below main
D Runtime error when the function is called
Answer: A. C reads top-to-bottom. When the compiler reaches the call to calculate_area in main, it hasn't seen the function yet, so it warns about an "implicit declaration." The function is defined later in the same file, so the linker finds it (no linker error). The fix: add a function prototype above main.

Section 5: Function Definitions

After main, define the functions you prototyped:

double calculate_area(double radius)
{
    return PI * radius * radius;
}

C Data Types

Type Size (typical) Range Java Equivalent
char 1 byte -128 to 127 (or ASCII) char (but C’s is a number)
int 4 bytes ±2 billion int
long 8 bytes ±9 quintillion long
float 4 bytes ~7 digits precision float
double 8 bytes ~15 digits precision double
_Bool / bool 1 byte 0 or 1 boolean

Key Insight: char in C is a number — an 8-bit integer that represents an ASCII character. 'A' is 65, 'a' is 97, '0' is 48. You can do arithmetic on characters: 'A' + 1 is 'B'. Java’s char is 16-bit Unicode; C’s is 8-bit ASCII.

Check Your Understanding
What value does an uninitialized local variable int x; contain in C?
A 0 — C initializes integers to zero like Java does
B -1 — the standard uninitialized sentinel value
C NULL — the default for all unset variables
D Garbage — whatever bits happened to be at that memory location
Answer: D. C does not initialize local variables. The memory allocated for x still contains whatever was stored there previously by another function or process. This could be 0, 42, or -7583921 — it's unpredictable. Java zeroes out instance variables automatically, but C gives you raw memory. Always initialize: int x = 0;.

To use bool, true, and false, include <stdbool.h>. Without it, C uses 0 for false and any non-zero value for true.

Variable Declarations

int count = 0;              // Initialize immediately (recommended)
double temperature;         // Uninitialized — contains garbage!
char grade = 'A';           // Single character in single quotes

Common Pitfall: Uninitialized local variables in C contain whatever garbage data happened to be in that memory location. Java initializes variables for you (to 0, false, null). C does not. Always initialize your variables.

Comments

/* C89 style — works everywhere */
/* Can span
   multiple lines */

// C99 style — single line (same as Java)
Quick Check: Why does #include <stdio.h> need to be at the top of the file?

Because C reads top-to-bottom. The compiler needs to see the declaration of printf (and other functions) before your code uses them. #include pastes those declarations from the header file into your source.

Quick Check: What's wrong with #define MAX 100;?

The semicolon is included in the substitution. Everywhere MAX appears, it’s replaced with 100;. In an expression like int arr[MAX];, this becomes int arr[100;]; — a syntax error.

Quick Check: What value does an uninitialized int x; contain?

Garbage — whatever bits happened to be at that memory location from a previous use. Unlike Java, C does not zero-initialize local variables. Always initialize: int x = 0;


Structure Is Your Safety Net

C gives you more freedom than Java — and less protection. There’s no compiler checking that you initialized your variables, no runtime checking that your array access is in bounds, no garbage collector cleaning up your memory. The structure you impose on your code — consistent file layout, named constants, explicit prototypes — is your defense against bugs.

Next lesson: variables and I/O. You’ll learn printf format specifiers, scanf for input, and the critical difference between Java’s automatic string conversion and C’s explicit format strings.

Big Picture: The five-section structure (includes, constants, prototypes, main, definitions) isn’t just convention — it reflects how the C compiler works. The preprocessor needs includes first. The compiler needs prototypes before function calls. And main goes in the middle so you can read the program’s logic without scrolling past helper functions.