Skip to main content
@shmVirus

Program Basics

Toolchain stages, program anatomy, lexical elements, command-line arguments, and coding style.

A C program begins as text, but a computer does not execute that text directly. Several tools translate it, combine it with library code, and load it into a running process. Understanding that journey makes compiler and linker messages far less mysterious.

First Program

#include <stdio.h>

int main(void) {
    puts("Hello, C!");
    return 0;
}

Save the program as hello.c, then compile and run it:

cc -std=c17 -Wall -Wextra -Wpedantic hello.c -o hello
./hello

The output is:

Hello, C!

This tiny program already contains an included header, a function definition, a function call, a string literal, a statement terminator, and a status returned to the host environment.

Toolchain

The command cc hello.c -o hello hides a pipeline. A real implementation may combine stages internally, but the conceptual stages remain useful.

hello.c
   │ preprocessing

translation unit
   │ compilation

assembly code
   │ assembly

object file ──┐
              │ linking with libraries
library code ─┘

executable
   │ loading

running process

Preprocessing

The preprocessor handles directives whose first non-whitespace character is #. For example, #include <stdio.h> makes the declarations supplied by the standard I/O header available in the translation unit. Macro expansion and conditional compilation also happen here.

You can ask many compilers to show preprocessed output:

cc -std=c17 -E hello.c

An include directive does not normally copy a precompiled library implementation into the file. It provides declarations and definitions from a header so the compiler can check how names such as puts are used.

Compilation

The compiler parses the preprocessed C program, checks its grammar and constraints, performs translations and optimisations, and usually emits assembly language. A missing semicolon is diagnosed here. So is calling a declared function with incompatible argument types.

cc -std=c17 -S hello.c

The generated assembly is platform-specific even when the C source is portable.

Assembly

The assembler translates assembly instructions into relocatable machine code stored in an object file. Names or addresses supplied by other object files may still be unresolved.

cc -std=c17 -c hello.c

This commonly creates hello.o. It is not yet a complete executable.

Linking

The linker combines object files and required libraries. It resolves external references: the call to puts, for example, must be connected to an implementation in the C library.

These failures occur at different stages:

  • expected ';' is normally a compilation diagnostic.
  • undefined reference to 'total' is normally a linking diagnostic.
  • ./hello: No such file or directory is a shell or loading problem, not a C syntax error.

Distinguishing the stage narrows the investigation immediately.

Execution

The operating environment loads the executable, establishes a process, arranges the initial execution state, and transfers control to the program. In a hosted C implementation, program startup eventually calls main. When main returns, its value becomes a termination status visible to the host.

By convention, 0 means successful completion. EXIT_SUCCESS and EXIT_FAILURE from <stdlib.h> express the intent portably.

Program Anatomy

Consider a slightly larger example:

#include <stdio.h>

static int square(int value);

int main(void) {
    int number = 7;
    int result = square(number);

    printf("%d squared is %d\n", number, result);
    return 0;
}

static int square(int value) {
    return value * value;
}

Header Inclusion

<stdio.h> declares standard input/output facilities, including printf. A declaration tells the compiler a name’s type and how it may be used. Using a library function without the proper declaration is not a harmless shortcut.

Angle brackets request an implementation-provided header. Quotes, as in #include "report.h", first search according to rules intended for project headers. Exact search locations are implementation-defined.

Main Function

Two portable definitions for a hosted program are:

int main(void)

and

int main(int argc, char *argv[])

void main() is not a portable hosted-C definition. Writing main(void) explicitly says that the function accepts no arguments. Empty parentheses in an old-style C declaration do not communicate the same type information.

Declarations

A declaration introduces a name and its type. Here int number = 7; both declares and initializes number. The prototype static int square(int value); declares a function before its first call.

Declare a variable close to the first place it is needed. A short lifetime reduces the amount of program state a reader must track.

Statements

Statements perform actions. An expression followed by ; can form an expression statement. Braces group declarations and statements into a compound statement, also called a block.

Indentation is not part of C’s grammar, but it is part of communication. Braces determine nesting; indentation should reveal it accurately.

Return Status

The final return 0; ends main successfully. Reaching the closing brace of main is defined to return zero in modern C, but writing the return explicitly can make the contract visible to beginners.

Lexical Elements

Before parsing expressions and statements, C recognises smaller elements called tokens.

Keywords

Keywords have reserved language meanings: int, return, if, while, struct, and sizeof are examples. They cannot be reused as identifiers.

Identifiers

Identifiers name functions, variables, types, labels, and other program entities. They may contain letters, digits, and underscores, but cannot begin with a digit. C is case-sensitive: total, Total, and TOTAL are different identifiers.

Prefer names that reveal roles:

double temperature_celsius;
int attempt_count;

Avoid identifiers reserved by the implementation. At file scope, names beginning with an underscore are reserved. Names beginning with two underscores or an underscore followed by an uppercase letter are reserved everywhere. A simple project rule is: do not begin your own identifiers with _.

Literals

Literals write values directly in source code:

42          /* integer literal */
3.5         /* floating literal */
'A'         /* character constant */
"Ada"       /* string literal */

Suffixes can influence type: 42U is unsigned, 3.5f is float, and 42L is long. Types and conversions receive a full treatment in the next chapter.

Comments

C supports block comments and line comments:

/* Explain why this threshold exists. */
if (load > 80) {
    warn_user(); // The immediate action is clear from its name.
}

Block comments do not nest. Comments should explain intent, constraints, units, or non-obvious decisions—not translate syntax into English.

Weak:

count++; /* increment count */

Useful:

count++; /* The rejected record still consumes an input position. */

Escape Sequences

Escape sequences represent characters that are awkward or impossible to type literally inside character and string literals.

EscapeMeaning
\nnewline
\thorizontal tab
\\backslash
\"double quote
\'single quote
\0null character

For example:

printf("Name:\t\"Grace\"\n");

produces a labelled, quoted name followed by a newline.

Command Arguments

The command line can supply text to a program:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("argument count: %d\n", argc);

    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = \"%s\"\n", i, argv[i]);
    }

    return 0;
}

Running ./show Ada 42 typically prints:

argument count: 3
argv[0] = "./show"
argv[1] = "Ada"
argv[2] = "42"

argc is non-negative and counts the argument strings. When argc is positive, argv[0] conventionally represents the program invocation. argv[argc] is guaranteed to be a null pointer, providing a sentinel after the final argument.

Arguments are strings. A numeric-looking argument does not become an integer automatically. Numeric conversion must validate syntax and range; Chapter 8 develops that process using strtol.

Always check the count before indexing:

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

int main(int argc, char *argv[]) {
    const char *program_name = "program";

    if (argc > 0 && argv[0] != NULL) {
        program_name = argv[0];
    }

    if (argc != 2) {
        fprintf(stderr, "usage: %s NAME\n", program_name);
        return EXIT_FAILURE;
    }

    printf("Hello, %s!\n", argv[1]);
    return EXIT_SUCCESS;
}

Without the guard, using argv[1] when no argument was supplied would access an element that is not an argument string.

Coding Style

Style is not decoration. It reduces ambiguity and makes defects easier to see.

Naming

Use a consistent convention. For example:

  • nouns for values: line_count, temperature;
  • verbs for actions: read_record, print_summary;
  • question-like names for truth values: is_valid, has_data;
  • uppercase names for simple symbolic constants: MAX_ATTEMPTS.

Avoid type-only names such as integer and unexplained abbreviations such as tmp2. A name should describe the value’s role, not merely its representation.

Indentation

Choose one brace and indentation style, then apply it mechanically. Use braces even around a one-statement body when a later edit could create a misleading indentation bug:

if (temperature < 0) {
    puts("freezing");
}

Keep lines and functions short enough to scan. Blank lines should separate conceptual steps, not every individual statement.

Documentation

A function comment is valuable when it states a contract:

/* Converts Celsius to Fahrenheit.
   The result uses the same physical value, expressed in °F. */
double to_fahrenheit(double celsius);

The comment adds units and meaning. It does not repeat the parameter list.

Build Autopsy

Consider a small program whose visible job is only to calculate a difference:

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

#define JOURNEY_NAME "morning route"

int main(void) {
    int start_km = 7;
    int finish_km = 19;
    int travelled_km = finish_km - start_km;

    printf("%s: %d km\n", JOURNEY_NAME, travelled_km);
    return EXIT_SUCCESS;
}

Save it as journey.c and build it with:

cc -std=c17 -Wall -Wextra -Wpedantic journey.c -o journey
./journey

The output is:

morning route: 12 km

That simple result crosses several boundaries. It helps to know what exists at each one:

BoundaryInputImportant result
Preprocessingsource plus included headersexpanded translation unit
Compilationtranslation unitchecked and translated program logic
Assemblygenerated assemblyrelocatable machine instructions
Linkingobject files and librariesexecutable with references connected
Loadingexecutable fileprocess image prepared by the environment
Executionprocess stateoutput and an exit status

JOURNEY_NAME is replaced during preprocessing. The declaration for printf arrives through <stdio.h>, but the function’s compiled definition normally comes from the C library during linking. The variables begin to exist as objects only while main executes. Mixing these stages leads to confused diagnoses: adding another header cannot supply a missing function definition, and changing linker options cannot repair a missing semicolon.

The intermediate forms can be inspected separately:

cc -std=c17 -E journey.c -o journey.i
cc -std=c17 -S journey.c -o journey.s
cc -std=c17 -c journey.c -o journey.o
cc journey.o -o journey

The preprocessed file is usually large because standard headers contribute many declarations. Read it to answer a focused question—such as “what did this macro become?”—not as if it were the source humans should maintain. Assembly output is implementation- and optimisation-dependent. It can explain what one build produced, but it is not a portable definition of what C means.

Statement State

The declarations in main create a short state history:

Point reachedstart_kmfinish_kmtravelled_km
after first declaration7not yet in scopenot yet in scope
after second declaration719not yet in scope
after subtraction71912
after printf71912

“Not yet in scope” is different from “uninitialized.” Before a declaration, the name cannot be used there at all. An uninitialized automatic object has been declared but has not received a value; reading it is undefined behaviour. This distinction becomes increasingly important as blocks and functions introduce more scopes.

Diagnostic Triage

A diagnostic is most useful when classified by the earliest stage that could have produced it.

source cannot be tokenized or parsed       -> syntax/translation problem
name or call has no valid declaration      -> declaration/type problem
external name has no supplied definition   -> link problem
program cannot be started                  -> loading/environment problem
program starts and violates a language rule-> runtime undefined behaviour
program runs safely but computes the wrong result -> logic problem

Suppose printf is misspelled as printff. A modern compiler should warn that no declaration is visible. If compilation is forced onward, the linker may later report an undefined reference. The link message is real, but the first compiler diagnostic identifies the earlier cause. Fixing only the last message encourages random library flags instead of correcting the spelling.

When a build reports many errors, start at the first one in the first file. One missing quote can make later lines look like part of a string; one missing brace can make later functions appear illegally nested. Rebuild after correcting a small coherent group. A clean rebuild is evidence that the current diagnostics describe the current source rather than stale objects.

Process Contract

A command-line program communicates through more than printed text:

  • its standard output carries ordinary results;
  • its standard error carries diagnostics;
  • its exit status reports success or failure to a parent process or script;
  • command arguments and environment state influence its initial data;
  • files and other external resources may be changed as side effects.

For the journey program, the contract is deterministic: it reads no external input, prints one line, and returns success. If printf itself fails, this tiny example does not detect it; a program writing important data should check output and close errors. return EXIT_SUCCESS; describes meaning better than relying on a memorized numeric status. EXIT_FAILURE similarly supplies a portable failure value, although operating systems may support many additional conventions.

A useful source audit asks four questions in order:

  1. What objects exist at this point, and which have initialized values?
  2. Which statement changes state or performs input/output?
  3. Which declarations promise the types of the operations being called?
  4. What observable result and exit status follow on each path?

Those questions scale from a six-line program to a multi-file system. They also separate language reasoning from guesses based on one successful run.

Language Boundaries

Not every questionable C program fails in the same way. The standard uses several categories that determine what evidence is meaningful.

Diagnosed Violations

Some source violates a syntax rule or a constraint for which a conforming implementation must issue at least one diagnostic. An undeclared identifier, incompatible assignment, or malformed declaration belongs here. The implementation may continue after warning, but “an executable was produced” does not make the program valid.

Treat diagnostics as contract failures between the source and translator. Correct the source or intentionally choose a documented extension; do not infer that a warning is harmless because one run looked right.

Undefined Behaviour

Undefined behaviour occurs when the C standard imposes no requirements after a prohibited operation, such as reading outside an array, dividing an integer by zero, or reading an uninitialized automatic object. The program is not promised a crash, wraparound, or stable wrong answer. It may appear correct until optimization, input, or surrounding code changes.

A debugger observation shows what one build happened to do. It cannot turn undefined behaviour into a portable rule. The useful response is to prove that the prohibited operation cannot occur on any allowed path.

Unspecified Choice

For some valid code, C allows one of several behaviours without requiring the implementation to document which is selected on each occasion. Operand evaluation order is a common example. Correct code must accept every permitted choice rather than testing one compiler and depending on its current order.

Implementation-Defined Choice

An implementation-defined choice must be documented by the implementation. The signedness of plain char is an example. Depending on such a choice can be reasonable inside a declared platform contract, but the dependency should be visible and tested. It is not portable merely because two popular compilers make the same choice.

These categories lead to different questions:

diagnostic present?       -> which language constraint was violated?
undefined behaviour?      -> how can the invalid path be prevented?
unspecified choice?       -> does every permitted choice work?
implementation-defined?   -> where is the chosen behaviour documented?

The distinction is a practical debugging tool. It prevents searches for “what value does out-of-bounds access return?” when the language supplies no such value.

Preprocessor Boundary

Preprocessing manipulates tokens before C types exist. An object-like macro:

#define TAX_RATE 0.15

does not create a typed object. It replaces later matching tokens. A const double object, by contrast, participates in C scope, type checking, and debugger inspection:

const double tax_rate = 0.15;

Use macros where preprocessing is actually required—conditional inclusion, include guards, and carefully controlled token generation. Prefer language objects and functions when types, scopes, and single evaluation matter.

Header inclusion is token inclusion, not library linking. This explains two otherwise surprising facts:

  • including a header in several source files repeats its declarations independently;
  • a function can be correctly declared by a header yet still fail to link when its definition or required library is absent.

To inspect one macro expansion without reading an entire preprocessed standard header, create a tiny isolated source or ask the compiler for its macro diagnostics where supported. Keep experiments small enough that the changed token sequence is visible.

Reproducible Build Record

When a result matters, preserve the recipe that produced it:

compiler and version
language mode and all warning/optimization options
source revision
preprocessor definitions and include paths
object/library inputs and their order where relevant
target architecture and relevant runtime environment

cc file.c records too little for another person to reproduce an optimization-sensitive failure. A build script or build system turns the command into versioned project knowledge.

Compile generated or third-party code under a deliberate warning policy rather than weakening diagnostics globally. Keep application code at the strongest practical setting and document each unavoidable platform extension. Warnings differ across compilers, so using more than one implementation can expose assumptions without replacing language reasoning.

Program Review Walkthrough

Review the journey program from the outside inward:

  1. Observable contract: one line on standard output and success status.
  2. Entry contract: hosted execution calls main with no program arguments requested by this signature.
  3. Declarations: every automatic object is initialized before reading.
  4. Calculation: subtraction is representable for the chosen constants.
  5. Output contract: %s receives a null-terminated string and %d receives int.
  6. Dependencies: <stdio.h> declares printf; the link supplies its definition.
  7. Portability: no result depends on an implementation-specific integer width for these small values.

For a larger program, apply the same layers to each function and module. Review is easier when code exposes boundaries explicitly: named intermediate values, narrow functions, checked operations, and one clear owner for each resource.

Lexical Debugging

Before the compiler can understand declarations or statements, source characters become preprocessing tokens. A missing delimiter can therefore distort everything that follows.

puts("first line
puts("second line");

The first string has no closing quote. Diagnostics on the second line may be consequences, not independent defects. Repair the earliest broken token boundary and compile again.

Adjacent string literals are concatenated during translation:

puts("a long message "
     "split across source lines");

This produces one string. It differs from placing a raw newline inside a quoted literal, which is invalid. Escape sequences such as \n encode characters in the literal; source formatting alone does not create output line breaks.

Comments separate tokens rather than behaving as arbitrary removable characters. Do not use them to splice pieces of one token. More importantly, commenting out a large region with /* ... */ fails if that region already contains a block comment because block comments do not nest. Conditional preprocessing with #if 0 is often safer for temporary experiments, while version control is the right place for deleted code.

Identifiers also have ownership rules. Standard headers and the implementation reserve many names, including broad underscore patterns. Application code should use descriptive project-specific names rather than guessing which short internal-looking name is safe. A name collision introduced through a header macro can change tokens before compilation; inspecting preprocessed output can confirm that hypothesis.

Minimal Experiments

When uncertain about one language rule, construct the smallest well-diagnosed program that isolates it:

#include <stdio.h>

int main(void) {
    printf("sizeof(int) = %zu\n", sizeof(int));
    return 0;
}

Record compiler options and distinguish what the run proves:

  • it can show one implementation’s sizeof(int);
  • it cannot prove every C implementation uses that size;
  • it cannot validate undefined code by producing a stable result;
  • it can compare warnings across compilers;
  • it can test a hypothesis before changing a large program.

Experiments complement documentation. First identify whether the question asks about a language guarantee, an implementation choice, or current generated behaviour; then use the appropriate evidence.

Build Failures

  • Compiling without warnings: defects remain hidden until runtime or another platform.
  • Ignoring the first diagnostic: later messages often cascade from the first syntax error.
  • Calling every failure a compiler error: preprocessing, compilation, linking, loading, and runtime failures require different investigations.
  • Using undeclared library functions: include the standard header that declares the function.
  • Assuming command arguments exist: validate argc before accessing argv[n].
  • Commenting syntax instead of intent: let readable code explain mechanics.
  • Using non-portable main signatures: prefer a standard hosted form.

Program Reasoning

  • C source passes conceptually through preprocessing, compilation, assembly, linking, loading, and execution.
  • Headers supply declarations; object files supply compiled definitions; the linker connects external references.
  • A hosted program begins at main and reports success or failure to its environment.
  • Keywords, identifiers, literals, comments, and escape sequences are fundamental lexical elements.
  • Command-line arguments arrive as strings and must be counted and validated.
  • Consistent names, indentation, contracts, and diagnostics are correctness tools.

Build Practice

Read the Source

  1. Explain the difference between a compiler error and an undefined-reference linker error.
  2. Why is main(void) more informative than main() in a declaration?
  3. Which identifiers beginning with underscores should application code avoid?

Follow the Build

  1. Predict argc and every accessible argument string for ./convert 32 C.
  2. Mark the declarations, statements, literals, identifiers, and keywords in the square program.

Repair the Program

  1. Compile a program that calls printf without including <stdio.h>. Record the diagnostic, then correct the cause rather than suppressing the warning.
  2. Correct a greeting program that always uses argv[1], even when the user supplies no name.

Create a Program

  1. Write repeat, invoked as ./repeat WORD, that prints the word on three separate lines and rejects any other argument count.
  2. Create two source files: one defines double cube(double), and the other calls it. Compile them separately into object files, then link them. Chapter 14 will explain this workflow fully.

Judge the Style

  1. Take a short program you previously wrote. Rename vague identifiers, format its blocks consistently, and replace syntax-narrating comments with explanations of intent. Describe which version is easier to audit and why.