student@ubuntu:~$
c-foundations Lesson 3 12 min read

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.

Reading: Hanly & Koffman: §1.1–1.4 (pp. 14–35), §2.4 (pp. 69–71)

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 gcc and point at the output of each
  • Read a C90 main and 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)
  1. Preprocessing (gcc -E). Handles directives that start with #. #include <stdio.h> pastes the text of stdio.h into your file; #define PI 3.14 replaces every PI with 3.14. Pure text substitution. Run gcc -E hello.c to see hundreds of lines of stdio.h before your six lines.
  2. Compilation (gcc -S). Translates C into assembly. Try gcc -S hello.c and open hello.s. That is roughly what objdump -d shows when you disassemble a binary in a CTF challenge.
  3. Assembly (gcc -c). Converts assembly into machine code, producing an object file (.o). Binary, but not yet a complete program.
  4. Linking. Combines your .o file with library code (printf lives in libc) to produce the final executable. On Linux, printf is not physically copied into your binary; the binary records “I need printf from libc.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 $?.

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?

  1. int main(void)
  2. int main(int argc, char **argv)
  3. void main(void)
  4. int main(int argc, char *argv[])
  5. 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:

  1. The skeleton.
    #include <stdio.h>
    #include <string.h>
    
    int main(void)
    {
        /* declarations first (C90) */
        /* statements */
    
        return 0;
    }
    
  2. The compile command. Zero warnings is the goal.
    gcc lab1.c -Wall -Wextra -pedantic -std=c90 -o lab1
    
  3. The C90 discipline. All declarations at the top of main. /* ... */ comments only. Loop counter with the declarations, not inside the for initializer.
  4. 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.