What Is the Structure of a C Program?
Preprocessor directives, data types, function prototypes — the anatomy of every C file
After this lesson, you will be able to:
- Organize a C source file into preprocessor directives, constants, prototypes,
main, and definitions - Use
#includeto include standard library headers and explain its text substitution behavior - Define named constants with
#defineandconst - Write function prototypes before
mainand 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:
#includeis NOT the same as Java’simport. Java’simporttells the compiler where to find a class by name. C’s#includeliterally 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:
#definedoes NOT end with a semicolon or an=!#define PI 3.14;would replacePIwith3.14;everywhere — including inside expressions likePI * r * r, which becomes3.14; * r * r(syntax error). And#define PI = 3.14would replacePIwith= 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 100does pure text substitution with no type checking. C also hasconst int MAX = 100;which is closer to Java’s approach and generally preferred when you need type safety.
#include <stdio.h> actually do during compilation?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,
mainin the middle, function definitions at bottom. This lets you readmainfirst 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()andvoid 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.
calculate_area(5.0) in main, but define the function below main without a prototype. What happens when you compile with -Wall?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:
charin 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' + 1is'B'. Java’scharis 16-bit Unicode; C’s is 8-bit ASCII.
int x; contain in C?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
maingoes in the middle so you can read the program’s logic without scrolling past helper functions.