Introduction to C
Your first C program, what gcc actually does, and why we start in C90
Based on content from Dr. Stu Steiner, Eastern Washington University.
In a nutshell
In CSCD 210 you wrote Java and handed it to the JVM, which handled memory, bounds checking, and linking for you. C does none of that. You write source, run gcc, and the output is a native executable the operating system loads and runs directly. No runtime. No garbage collector. No bounds checking. This lesson covers a working hello-world, the four stages gcc runs, the legal shapes of main, the two forms of #include, and why the course opens in C90.
Practice this topic: C Basics drill, or browse the practice gallery.
After this lesson, you will be able to:
- Write, compile, and run a minimal C program with the exact flags Lab 1 uses
- Explain the four stages of
gccand point at the output of each - Read a C90
mainand identify the three legal signatures - Say what
#include <...>and#include "..."each tell the preprocessor - Say why this course starts in C90
Quick reference
| Thing | C syntax | Used in Lab 1? |
|---|---|---|
| Bring in standard I/O | #include <stdio.h> |
Yes |
| Bring in string functions | #include <string.h> |
Yes |
| Entry point | int main(void) { ... return 0; } |
Yes |
| Print with newline | printf("Hi\n"); |
Yes |
| Block comment | /* ... */ |
Yes |
| Line comment | // ... (C99 only; fails under -pedantic -std=c90) |
No |
| Compile command | gcc lab1.c -Wall -Wextra -pedantic -std=c90 -o lab1 |
Yes |
| Run the executable | ./lab1 |
Yes |
Coming from CSCD 210
CSCD 240 builds on what you did in CSCD 210. Primitive types, variables, if/else, loops, methods, arrays, basic file I/O, a first look at classes and objects. Most of you wrote Java; a few transfers used Kotlin or Python. This course is not CSCD 210 again. It is what C does differently from the language you just finished using. Every concept below has a 210 analogue; the names change and the rules change, but the ideas do not start from scratch.
Why C is different
C is a 50-year-old language and still one of the most-used languages on the planet. Operating systems are written in C. Database engines, network stacks, device drivers, and the interpreters for every language you have touched (Java’s JVM, Python, Ruby, Node) are written in C.
For this course specifically:
- Classes ahead of you that require CSCD 240. CSCD 260 (Computer Architecture and Organization), CSCD 330 (Computer Networks), and CSCD 340 (Operating Systems) all assume you can read and write C.
- Reverse engineering in CTF. When a challenge hands you a binary and asks what it does, reading the disassembly is uncomfortable without a mental model of C.
- Security, concretely. Most historic memory-safety CVEs are C bugs: CVE-2014-0160 Heartbleed, CVE-2021-3156 Baron Samedit, CVE-2019-11931 WhatsApp GIF, CVE-2001-0144 OpenSSH CRC-32. The compile flags on this page and the habits in Lab 1 are the professional defense against writing the next one.
The paradigm shift in one sentence
Java abstracted the machine. C exposes it.
In Java, an array knows its own length. A variable is either a primitive or a reference to a garbage-collected object. You never think about memory. In C, an array is a bare memory address with no length attached. Every variable sits at a specific byte offset you can ask for. Every byte of storage is your responsibility from the moment you request it until the moment you give it back, because there is no garbage collector.
Most of what you learn this quarter is what Java was hiding from you. For the machine-level picture of exactly what lives where and why &x resolves to an address, see the deep dive on how a C program becomes a running process.
What Java had that C does not
| Java | C |
|---|---|
| Classes, objects, methods | Only functions that take arguments and return values |
public / private visibility |
Visibility controlled by which header declares what |
try / catch / exception objects |
Errors are return values; the caller checks |
Packages, import |
You #include header files, every time |
| Garbage collector | You call free yourself, or it leaks |
String class |
A char array with a null terminator at the end |
arr.length |
You track the length yourself |
| Bounds-checked indexing | Out-of-bounds writes silently corrupt adjacent memory |
Inside a function, C behaves the way every language you have seen behaves: enter at the top, run line by line, return at return. The difference is that this is all C has.
Your first program
Write, compile, run
Create a file called hello.c:
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
Compile and run:
gcc hello.c -Wall -Wextra -pedantic -std=c90 -o hello
./hello
# Hello, world!
Breaking down the command:
| Part | Meaning |
|---|---|
gcc |
The GNU C Compiler |
-Wall |
Enable a broad set of warnings. Always on. |
-Wextra |
Additional warnings on top of -Wall. Also always on. |
-pedantic |
Refuse non-standard C. |
-std=c90 |
Use the C90 language rules. |
-o hello |
Name the output file hello instead of the default a.out. |
hello.c |
The source file. |
./hello instead of just hello because the current directory is not in your $PATH by default.
The four stages of gcc
gcc does not go straight from source to binary. It runs four stages:
hello.c → Preprocessor → Compiler → Assembler → Linker → hello
(.c → .i) (.i → .s) (.s → .o) (.o → executable)
- Preprocessing (
gcc -E). Handles directives that start with#.#include <stdio.h>pastes the text ofstdio.hinto your file;#define PI 3.14replaces everyPIwith3.14. Pure text substitution. Rungcc -E hello.cto see hundreds of lines ofstdio.hbefore your six lines. - Compilation (
gcc -S). Translates C into assembly. Trygcc -S hello.cand openhello.s. That is roughly whatobjdump -dshows when you disassemble a binary in a CTF challenge. - Assembly (
gcc -c). Converts assembly into machine code, producing an object file (.o). Binary, but not yet a complete program. - Linking. Combines your
.ofile with library code (printflives inlibc) to produce the final executable. On Linux,printfis not physically copied into your binary; the binary records “I needprintffromlibc.so.6” and the loader wires it up at program start.
In practice, one gcc hello.c -Wall -Wextra -pedantic -std=c90 -o hello runs all four.
Check your understanding (predict the output)
#include <stdio.h>
int main(void)
{
printf("A");
printf("B\n");
printf("C");
return 0;
}
Reveal answer
AB on one line, then C on the next line with no newline after it. The shell prompt usually appears right after the C, on the same line, because printf never adds a newline for you. Only the \n in the middle printf forces a line break.
Rules that matter this week
Program structure
/* 1. Preprocessor directives */
#include <stdio.h>
/* 2. Function prototypes */
int add(int a, int b);
/* 3. Main function */
int main(void)
{
int result; /* declarations at the top (C90) */
result = add(3, 4);
printf("Sum: %d\n", result);
return 0;
}
/* 4. Function definitions */
int add(int a, int b)
{
return a + b;
}
Prototypes: C reads top to bottom in one pass. If main calls add but add is defined below, the compiler has not seen add yet. A prototype declares the signature ahead of time. Without one, -Wall warns about implicit declaration.
return 0: main returns an int, the program’s exit status. return 0; means success; the shell reads this value via $?.
The three legal main signatures
| Signature | When you write it |
|---|---|
int main(void) |
Program takes no command-line arguments. Lab 1. |
int main(int argc, char **argv) |
Program takes arguments. argc counts them; argv holds them. |
int main(int argc, char *argv[]) |
Equivalent syntax for the second form. |
For Lab 1, int main(void) is what the grader expects. The standard also guarantees that argv[argc] is NULL in the second and third forms: a sentinel you can loop against.
Check your understanding (multi-select). Which of the following are legal C90 main signatures?
int main(void)int main(int argc, char **argv)void main(void)int main(int argc, char *argv[])public static void main(String[] args)
Reveal answer
1, 2, and 4. void main is accepted by some compilers but is not standard; the last line is Java.
#include <...> vs #include "..."
Two forms, different search rules.
| Form | Where the preprocessor searches | When to use |
|---|---|---|
#include <name.h> |
Compiler’s system directories (/usr/include, plus GCC’s own) |
Standard-library headers |
#include "name.h" |
Current directory first, then system | Your own headers |
Related pitfall: <string.h> is the C90 header for strlen, strcmp, strcpy. <strings.h> (plural) is a POSIX extension for bcmp, bzero, strcasecmp. Use <string.h>.
C90 vs C99
C has been standardized several times (C89/C90, C99, C11, C17, C23). The differences that matter this week:
| Feature | C90 | C99 and later |
|---|---|---|
| Variable declarations | Only at the top of a block | Anywhere |
for (int i = 0; ...) |
Not allowed | Allowed |
// line comments |
Not in the standard | Standard |
bool, true, false |
Define them yourself | <stdbool.h> |
Kernels, embedded systems, automotive, and avionics code often still target C90 rules. This course starts in C90 so you learn the discipline, then switches to C99 in week 5. For the fuller C89 through C23 timeline and why C90 still matters, see the deep dive on C standards history.
In practice this week: declare variables at the top of each function, not scattered through the body.
/* C90 style, what we use first */
int sum_first_n(int n)
{
int i;
int total;
total = 0;
for (i = 1; i <= n; i++) {
total = total + i;
}
return total;
}
Check your understanding (what’s wrong?). This code errors on line 8 under gcc -Wall -Wextra -pedantic -std=c90. Why?
#include <stdio.h>
int main(void)
{
int a;
int b;
a = 5;
int c = 7; /* line 8 */
b = a + c;
printf("%d\n", b);
return 0;
}
Reveal answer
Line 8 mixes a declaration with statements. Under C90, every declaration in a block must precede the first statement of that block. Statements began at a = 5;. Move int c; up with the other declarations, then assign c = 7; among the statements.
Preview: the three-file model
In Java a small program is often two files: Main.java with main, and Car.java with a class. C forces a three-file minimum for any program with more than one source file:
| File | Role | Java analogue |
|---|---|---|
main.c |
Contains int main, drives the program |
The class with main |
lab.c |
Function definitions (the bodies) | Other classes’ method bodies |
lab.h |
Function declarations (prototypes) | The public section of an interface |
Headers also carry an include guard at the top:
#ifndef LAB1_H
#define LAB1_H
/* declarations go here */
#endif /* LAB1_H */
This prevents the same header from being pasted in twice in the same translation unit. You will meet the full three-file model, include guards, and extern in Headers & Makefiles. Lab 1’s starter is a single .c file; you do not need your own header.
Into Lab 1
The pieces from this page you will reach for during Lab 1:
- The skeleton.
#include <stdio.h> #include <string.h> int main(void) { /* declarations first (C90) */ /* statements */ return 0; } - The compile command. Zero warnings is the goal.
gcc lab1.c -Wall -Wextra -pedantic -std=c90 -o lab1 - The C90 discipline. All declarations at the top of
main./* ... */comments only. Loop counter with the declarations, not inside theforinitializer. - The mindset. You are not writing Java anymore. When something feels more primitive than you expect, it probably is.
Lab 1 also needs the next lesson: types, printf, scanf, strings, strcmp. Keep going with Variables & I/O. Want to drill this page before moving on? C Basics drill or the full practice gallery.