Procedural Programming with C · Basic · Week 3

Program Structure, Style, and Basic Output

Every C program has the same skeleton. This week takes it apart piece by piece, introduces the output you will use for the rest of the course, and establishes two habits — readable formatting and version control — that separate people who finish projects from people who abandon them.

By the end of this week you can
  • Explain every line of a minimal C program, including why main returns a value and who reads it.
  • Use EXIT_SUCCESS and EXIT_FAILURE, and inspect a program's exit status from the shell.
  • Print text and values with printf.
  • Format and name code so that a stranger can read it, and run a formatter to enforce it.
  • Create a Git repository, commit your work, and ignore build products.

1The shape of a C program

Here is the smallest useful program, annotated:

#include <stdio.h>      /* 1. bring in declarations           */
#include <stdlib.h>     /*    for EXIT_SUCCESS                */

int main(void)          /* 2. where execution begins          */
{                       /* 3. the function body opens         */
    printf("ready\n");  /* 4. one statement, ends in ;        */
    return EXIT_SUCCESS;/* 5. hand a result back to the shell */
}                       /*    the body closes                 */

A C source file is a sequence of declarations and definitions at the outermost level. You cannot put a bare statement outside a function; there is no top-level script. Everything executable lives inside a function body.

#include and where names come from

C does not know what printf is. Nothing is built in except the language itself — the keywords, the operators, the control structures. Every function you call comes from a library, and before you can call it, the compiler needs its declaration: its name, its return type, and the types of its parameters.

A header file is a text file full of such declarations. #include <stdio.h> tells the preprocessor to paste that file in at this point, which is why removing the line produced the "implicit declaration" error in week 1.

The angle brackets mean "look in the system include directories". Quotation marks — #include "myheader.h" — mean "look next to this source file first". You will write your own headers in week 28.

HeaderWhat it declaresFirst needed
<stdio.h>printf, scanf, fgets, file handlingWeek 3
<stdlib.h>EXIT_SUCCESS, malloc, free, strtol, exitWeek 3
<string.h>strlen, strcpy, memcpyWeek 14
<stdbool.h>bool, true, falseWeek 5
<math.h>sqrt, pow, fabs (link with -lm)Week 24

2main and its return value

When the operating system starts your program, it calls main. When main returns, the program ends and the returned integer is handed back to whatever launched it — usually your shell. This value is the program's exit status, and it is not decoration: it is how programs tell scripts whether they succeeded.

./hello
echo $?

$? holds the exit status of the last command. Run it after a successful program and you get 0. Run it after a failing one and you get something else.

By convention, 0 means success and any non-zero value means failure. This is backwards from most people's intuition, and there is a reason: there is one way to succeed and many ways to fail, so the non-zero values are free to distinguish between them.

<stdlib.h> gives you two names for the common cases:

return EXIT_SUCCESS;   /* 0 on every platform */
return EXIT_FAILURE;   /* an implementation-defined non-zero value */

Use them. return 0; is correct and extremely common, but the named constants say what you mean, and EXIT_FAILURE has no fixed numeric spelling you could write by hand portably.

Why this matters early

Shell scripts, build systems, and continuous integration all branch on exit status:

./myprogram && echo "worked"
./myprogram || echo "failed"

A program that always returns 0, even when it could not open its input file, is a program that silently breaks every automation around it. Week 31 designs command-line tools properly; the habit starts here.

The two legal forms of main. int main(void) and int main(int argc, char *argv[]). The second receives the command-line arguments and appears in week 23. Anything else — notably void main(), which some old textbooks show — is not standard C, even where a compiler tolerates it.

3Statements, blocks, and punctuation

A statement is one step. It ends with a semicolon. The semicolon is a terminator, not a separator — every statement gets one, including the last one in a block.

A block is a sequence of statements wrapped in braces. A block can appear anywhere a statement can, and it creates a new scope, which week 17 examines in detail.

{
    int inner = 1;      /* visible only inside these braces */
    printf("%d\n", inner);
}
/* inner does not exist here */

C ignores whitespace almost everywhere. The following is legal and compiles to exactly the same program as the annotated version above:

#include <stdio.h>
int main(void){printf("ready\n");return 0;}

The compiler does not care. Everyone who ever reads your code does. Which brings us to the real subject of this week.

The stray semicolon. if (x > 0); is legal C. The semicolon is an empty statement, so the if controls nothing and the block after it always runs. The compiler will not stop you, though -Wextra may warn. This bug has cost people entire afternoons.

4Printing with printf

printf takes a format string and then whatever values that string refers to. Inside the format string, a % introduces a conversion specifier — a placeholder that says "insert the next argument here, formatted this way".

int   count = 42;
double ratio = 0.5;
char  grade = 'A';

printf("count = %d\n", count);
printf("ratio = %f\n", ratio);
printf("grade = %c\n", grade);
printf("text  = %s\n", "literal");
printf("%d items at %f each\n", count, ratio);
SpecifierArgument type
%dint
%uunsigned int
%fdouble (and float, which is promoted)
%ca single character
%sa string
%zusize_t — you will need this from week 12
%%a literal percent sign

Escape sequences let you write characters you cannot type directly. \n is a newline, \t a tab, \\ a backslash, \" a double quote. Each is one character, not two.

Mismatched specifiers are not caught by the language. printf("%d\n", 3.5) is undefined behavior: printf will read an int-sized piece of whatever was passed and print nonsense, or worse. GCC with -Wall catches the common cases — another reason that flag is not optional. Week 8 covers the full mechanism and week 30 explains why printf cannot check for itself.

5Style: writing for the next reader

You will spend far more time reading code than writing it, and most of what you read will have been written by someone else — including yourself six months ago. Style is not decoration; it is the difference between code you can change safely and code you are afraid of.

Indentation

Indent one level for each nested block. Four spaces is the most common choice; the Linux kernel uses tabs of width eight. Pick one and never mix them within a file — mixed indentation looks correct in your editor and wrong in everyone else's.

Naming

GuidelineGoodPoor
Say what it holdsstudent_countn2
Length matches scopei as a loop indexi as a global
Consistent word separationread_line, line_lengthread_line, lineLength
Constants stand outMAX_STUDENTSmaxstudents
No abbreviation gamesaverageavrg

C traditionally uses snake_case for functions and variables and UPPER_CASE for macros and constants. The standard library follows it; so should you.

Comments

Comments should explain why, not what. The code already says what it does.

x = x + 1;              /* add one to x          — worthless */
retries = retries + 1;  /* the modem drops the first byte
                           after power-on, so we retry once */

A comment that restates the code is worse than none: it doubles the maintenance burden and eventually contradicts the code it describes.

Let a tool do it

Arguing about brace placement is a waste of a life. Install clang-format and let it decide:

clang-format -style=llvm -i hello.c

Put a .clang-format file at the root of a project and every file in it is formatted identically, forever, with no discussion.

6Worked example: from unreadable to readable

Here is a working program. It compiles cleanly and produces correct output.

#include <stdio.h>
int main(void){int a=5,b=3,c;c=a*b;printf("%d\n",c);
int d=0;for(int i=1;i<=c;i++){d+=i;}printf("%d\n",d);return 0;}

Read it and answer: what does it print, and what are the two numbers for? The program works and tells you nothing.

Now the same computation, rewritten. Nothing about the behavior changed.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const int rows = 5;
    const int columns = 3;

    const int cell_count = rows * columns;
    printf("cells in the grid: %d\n", cell_count);

    /* Sum 1..cell_count, used later to size the report buffer. */
    int running_total = 0;
    for (int i = 1; i <= cell_count; i++) {
        running_total += i;
    }
    printf("triangular number for %d: %d\n", cell_count, running_total);

    return EXIT_SUCCESS;
}

What changed, concretely:

  • a, b, c, d became names that say what the value is.
  • Values that never change are marked const, so the compiler enforces that.
  • Each statement is on its own line, indented consistently.
  • The output labels itself, so the program is diagnosable without reading its source.
  • The one comment explains a purpose that is not visible from the code.
  • EXIT_SUCCESS states the intent of the return value.

Verify that the rewrite really is equivalent — the discipline of checking matters more than the belief:

gcc -std=c17 -Wall -Wextra -g -o ugly ugly.c
gcc -std=c17 -Wall -Wextra -g -o clean clean.c
./ugly > a.txt
./clean | grep -o '[0-9]*$' > b.txt
diff a.txt b.txt && echo "same numbers"

7Putting it under version control

From this week on, keep your work in Git. Not because the course requires it, but because you will want to know what you changed when something that worked yesterday does not work today — and because week 43's exercise in finding a regression with git bisect only makes sense if you have a history to bisect.

mkdir c-course && cd c-course
git init
git config user.name  "Your Name"
git config user.email "you@example.com"

Create a .gitignore before your first commit. Compiled output does not belong in a repository: it is large, it changes on every build, and it is reproducible from the source.

# Build products
*.o
*.obj
*.a
*.so
*.exe
a.out

# Binaries without an extension are hard to match by pattern,
# so keep them in one directory and ignore that.
/build/
/bin/

# Editor and OS noise
*.swp
.DS_Store
.vscode/

Then the loop you will repeat for the next fifty-three weeks:

git status                       # what changed
git add week03/clean.c           # stage this file
git commit -m "Week 3: readable rewrite of the grid example"
git log --oneline                # what has happened so far

Write commit messages in the imperative mood, describing the change rather than the file: Fix off-by-one in row count, not updated main.c. Your future self is the primary audience, and your future self will be in a hurry.

Commit small and often. A commit should be one idea. That makes history readable, makes a bad change easy to isolate, and makes git bisect actually useful. Branching, merging, and reviewing arrive in week 44; the single-user habits are enough for now.

8Common mistakes

MistakeWhat happensFix
Writing void main()Works on some compilers, rejected by othersint main(void). It is the only portable spelling for the no-argument form.
Forgetting to return a status on the failure pathThe shell sees success even though the program failedReturn EXIT_FAILURE from every path that did not do its job.
printf(count) instead of printf("%d\n", count)Compiler error, or a format-string vulnerability if count were a stringThe first argument is always a format string. Week 36 explains why this is a security issue.
Mixing tabs and spacesCode looks aligned for you, ragged for everyone elseConfigure your editor to insert spaces, and run clang-format.
Committing a.out and .o filesRepository bloats; every build shows as a changeWrite .gitignore before the first commit.
One enormous commit at the end of the weekHistory says nothing; a bad change cannot be isolatedCommit each working step.

9Check yourself

Why does main return a value, and who reads it?

The value is the program's exit status, returned to whatever started the program — normally the shell. Zero conventionally means success and non-zero means failure, which lets scripts and build systems branch on whether your program worked. Inspect it with echo $?.

Your program prints the right answer but returns EXIT_FAILURE. Does it matter?

To a human watching the terminal, no. To anything automated, yes — a build script, a test harness, or a shell pipeline will treat the run as failed and may stop. Exit status is part of your program's interface, not an afterthought.

What does #include <stdio.h> actually do?

It instructs the preprocessor to insert the contents of that header at that point in the file. The header contains declarations — names, return types, parameter types — not the function bodies. The bodies live in the C library and are attached later by the linker.

If the compiler ignores whitespace, why bother indenting?

Because the compiler is not the audience. Code is read many more times than it is written, and badly formatted code hides bugs — a misplaced brace or an accidental empty statement is invisible in a dense line and obvious in a formatted one. Delegate the decision to clang-format and stop thinking about it.

Why should compiled output be kept out of version control?

It is derived data: fully reproducible from the source by running the build. Storing it makes the repository large, makes every build appear as a change, and creates merge conflicts in binary files that cannot be merged. Track the inputs, not the outputs.

10Where this leads

You can write, build, format, and version a program. Week 4 goes underneath it: how the machine actually stores the numbers and characters you have been printing. That is the foundation for types in week 5 and for the conversion pitfalls in week 7, and it is what makes the bit-level work of week 25 feel obvious rather than arbitrary.