# C fundamentals

Source: https://codewiki.com/cpp/c-basics/

> - **what**: A C program consists of typed objects, expressions, control flow, and functions. An implementation translates source files and links the required definitions into a program.
> - **trap**: C's type rules do not automatically protect bounds or lifetimes. Uninitialized reads, signed overflow, and wrong format specifiers can all cause undefined behavior.
> - **fix**: Make types and ranges explicit, check library results, and verify generated code with strict warnings and sanitizers.

## What it is and why it exists

C is a statically typed, compiled language. Declarations describe the types of objects and functions, expressions compute values, and statements control execution. The language offers a small set of mechanisms while letting code express memory layouts, bit operations, and external interfaces directly, so it remains common in operating systems, embedded software, runtimes, and foreign-function interfaces.

“Close to the hardware” does not mean that every statement maps to one machine instruction. The C standard describes an abstract machine and its observable behavior; an implementation maps a valid program to a target platform. A compiler may reorder or remove work that cannot change observable results, provided the program does not invoke undefined behavior.

Basic C code answers four questions: what type the data has, how an expression is evaluated, which statement executes next, and where a name is visible. Pointers, arrays, dynamic memory, and file I/O all build on these rules. This topic covers only the boundaries you need before moving to those subjects.

You encounter these concepts in `.c` source files, header declarations, build logs, and compiler diagnostics. A small runnable program usually includes a header, a `main` function, declarations and expressions, and a return value that reports success or failure.

| Need | C mechanism | Contract you maintain |
|---|---|---|
| Represent data | Objects and types | Range and initialization state |
| Compute a result | Operators and expressions | Conversions, overflow, evaluation order |
| Select a path | `if`, `switch`, and loops | Bounds and termination conditions |
| Reuse behavior | Functions and declarations | Parameter meaning, result, and error convention |

## How it works

### From source file to program

After preprocessing, one source file becomes a translation unit. Preprocessing handles `#include`, conditional compilation, and macro replacement; the compiler then analyzes declarations, types, and statements and produces information needed for object code. A linker combines object files with required library definitions into an executable program. Exact commands and intermediate files belong to the toolchain, not to a fixed C language requirement.

A declaration tells the compiler what a name denotes. A definition also supplies a function body or reserves storage for an object. Headers usually contain shared declarations and source files contain definitions. If several translation units need one function, they must see compatible declarations, and the complete program must provide the required definition.

With GCC, `gcc -std=c2x -Wall -Wextra -Wconversion -Wpedantic program.c -o program` requests a language mode close to C23 and enables a useful set of warnings. GCC 13 calls that mode `c2x`, so this article uses that spelling to verify its examples. Warning options are not language semantics, and other compilers may use different names.

A typical build path can be described in four steps:

1. Preprocessing expands headers, macros, and conditional branches.
2. Compilation checks syntax and types and produces information needed for object code.
3. Assembly creates an object file containing machine code and relocation information.
4. Linking resolves cross-file names and library references and creates the final program.

### Objects, types, and initialization

An object is data storage that holds a value during execution. The declaration `int retries = 3;` creates and initializes an `int` object named `retries`. A later assignment changes its value, not its type. The `const` qualifier prevents modification through that lvalue; it does not require the compiler to place the object in physically read-only storage.

C specifies type capabilities and minimum ranges, but an implementation chooses many exact widths. `sizeof(char)` is always `1`, and that unit is a byte; one C byte has at least 8 bits but need not have exactly 8. Portable code must not assume fixed sizes for `short`, `int`, `long`, or pointers. Use `sizeof`, `limits.h`, or an exact-width integer type when the requirement is real.

The basic arithmetic types include integer and floating types. Integer types have signed and unsigned variants, while an implementation chooses the signedness of plain `char`. `void` represents no value or an incomplete object type. Arrays, pointers, functions, structures, unions, and enumerations are developed in their own topics.

An object needs a value appropriate for its type before its first read. An ordinary uninitialized local integer with automatic storage duration does not have a value you may simply read. File-scope objects and `static` local objects, by contrast, undergo zero initialization during program startup. The reliable habit is to give a local object a meaningful initializer at its declaration.

| Spelling | Meaning | Watch for |
|---|---|---|
| `int count = 0;` | Create and initialize a modifiable integer | The implementation determines the `int` range |
| `const double rate = 0.2;` | Prevent modification through `rate` | Floating representation is usually not decimal-exact |
| `unsigned mask = 1u;` | Create an unsigned integer | Mixing it with signed values triggers conversions |
| `int pending;` | Create without explicit initialization | Do not read it before assignment |

### Expressions and conversions

An expression combines operands and operators to produce a value or a side effect. Multiplication, division, and remainder bind more tightly than addition and subtraction; comparisons bind more tightly than logical AND and OR. Precedence determines grouping, not the evaluation order of every subexpression. Split the work into statements when order changes the result.

Dividing two integers still produces an integer, with any fractional part truncated toward zero. Thus, `7 / 2` is `3`, while `7 / 2.0` converts the integer to `double` first and produces `3.5`. A cast can request a conversion explicitly, but it cannot recover precision already lost or automatically make an out-of-range value safe.

Narrow integer types undergo integer promotion before most arithmetic. The usual arithmetic conversions then find a common type for the two operands of a binary arithmetic operation. Mixing signed and unsigned integers is especially risky: a negative value may first become a large unsigned value and only then participate in the comparison or calculation.

Signed integer overflow is undefined behavior. Unsigned arithmetic wraps modulo one more than the type's maximum value, which is defined but may still violate the application's intent. For counts, sizes, and money, determine the allowed range and check the boundary before performing the operation.

A condition treats scalar zero as false and nonzero as true. `&&` and `||` evaluate left to right and short-circuit, so they can check a precondition before performing a potentially unsafe operation. Short-circuiting cannot repair an out-of-bounds access, invalid read, or overflow that already occurred on the left.

### Control flow, functions, and scope

Use `if` to choose by a Boolean condition and `switch` to choose among discrete integer or enumeration values. A `switch` starts at the matching `case` and continues into later cases unless it reaches `break`, `return`, or another jump. Comment deliberate fallthrough; otherwise, write the `break`.

A `for` statement groups initialization, continuation test, and per-iteration update, making it suitable for a clear iteration variable. `while` tests before each iteration, while `do while` executes at least once. Loop bounds are often easiest to maintain as half-open ranges, starting at `0` and continuing while `index < count`.

A function declaration gives a name, return type, and parameter types; a function definition adds the body. Writing `int main(void)` explicitly says that `main` takes no arguments, and ordinary no-argument functions should also use a `void` parameter list. C always passes arguments by value. When an argument is a pointer, the copied value is still the pointer itself.

A name declared inside a block is visible from its declaration through the end of that block. An inner block may shadow an outer name. A file-scope name can be referred to by later declarations in the same translation unit, while linkage determines whether names across translation units correspond to one entity. Scope says where a name is visible; storage duration says how long an object's storage exists.

### Program entry and exit status

In a hosted environment, execution starts at `main`. Use `int main(void)` when the program does not read command-line arguments; `int main(int argc, char *argv[])` receives an argument count and argument strings. Reaching the closing brace of `main` is equivalent to returning `0`, but an explicit `return 0;` makes the successful path in an example easy to see.

A zero status returned to the host reports success, while a nonzero status reports some failure; `<stdlib.h>` also provides `EXIT_SUCCESS` and `EXIT_FAILURE`. The meaning of a particular nonzero value belongs to the interface between the program and its caller. Do not assume that every platform preserves any large status unchanged.

## Examples

These four programs progressively add types and arithmetic, functions and branches, loops, and `switch`. Each is an independent file. Every output shown here came from compiling locally with GCC 13.3.0 using `-std=c2x -Wall -Wextra -Wconversion -Wpedantic -Werror` and then running the result.

### Money with an explicit unit

The calculation stores money as integer cents until the final conversion to euros. This keeps quantities and units clear and avoids repeated binary floating-point rounding in intermediate steps.

<!-- quick -->

```c
// file: order_total.c
#include <stdio.h>

int main(void) {
    const int unit_price_cents = 725;
    const int quantity = 3;
    const int subtotal_cents = unit_price_cents * quantity;
    const double tax_rate = 0.20;
    const double tax_euros = subtotal_cents * tax_rate / 100.0;
    const double total_euros = subtotal_cents / 100.0 + tax_euros;

    printf("items: %d\n", quantity);
    printf("subtotal: %.2f EUR\n", subtotal_cents / 100.0);
    printf("tax: %.2f EUR\n", tax_euros);
    printf("total: %.2f EUR\n", total_euros);
    return 0;
}
```

```text
items: 3
subtotal: 21.75 EUR
tax: 4.35 EUR
total: 26.10 EUR
```

<!-- /quick -->

The `100.0` makes the division use a floating type. With `subtotal_cents / 100`, integer division would produce `21` first. Assigning that result to `double` later could not restore the lost 75 cents.

### Put a branch behind a function

The shipping function accepts grams and returns cents; `-1` represents invalid input. The caller must check that convention before treating the result as a price.

```c
// file: shipping.c
#include <stdio.h>

static int shipping_cents(int weight_grams) {
    if (weight_grams <= 0) {
        return -1;
    }
    if (weight_grams <= 500) {
        return 499;
    }
    if (weight_grams <= 2000) {
        return 799;
    }
    return 1299;
}

int main(void) {
    const int weight_grams = 1200;
    const int price = shipping_cents(weight_grams);

    if (price < 0) {
        puts("invalid weight");
        return 1;
    }

    printf("weight: %d g\n", weight_grams);
    printf("shipping: %.2f EUR\n", price / 100.0);
    return 0;
}
```

```text
weight: 1200 g
shipping: 7.99 EUR
```

Placing `static` on a file-scope function declaration limits that function's linkage to the current translation unit. The two consecutive `if` statements already cover lighter ranges. Each later condition handles only inputs that have not returned, so the branch boundaries do not overlap.

### Accumulate a range with a loop

The loop uses the closed interval `[first, last]`, so its condition is `current <= last`. The function handles a reversed range first instead of carrying invalid bounds into the loop.

```c
// file: range_total.c
#include <stdio.h>

static long long range_total(int first, int last) {
    long long total = 0;

    if (first > last) {
        return 0;
    }

    for (int current = first; current <= last; ++current) {
        total += current;
    }
    return total;
}

int main(void) {
    for (int end = 3; end <= 6; ++end) {
        printf("1..%d = %lld\n", end, range_total(1, end));
    }

    printf("empty range = %lld\n", range_total(5, 2));
    return 0;
}
```

```text
1..3 = 6
1..4 = 10
1..5 = 15
1..6 = 21
empty range = 0
```

The sample inputs are small, so `long long` holds each total. The function itself has not proved that arbitrary `int` boundaries cannot overflow. A production interface should also constrain the range or check against `LLONG_MAX - current` before adding.

### Handle discrete states with `switch`

A `switch` displays the mapping from status code to handling path. Every known branch ends with `break`, and `default` provides a defensive path for an unknown value.

```c
// file: job_state.c
#include <stdio.h>

static void print_state(int state) {
    printf("state %d: ", state);

    switch (state) {
        case 0:
            puts("queued");
            break;
        case 1:
            puts("running");
            break;
        case 2:
            puts("complete");
            break;
        default:
            puts("unknown");
            break;
    }
}

int main(void) {
    print_state(0);
    print_state(2);
    print_state(9);
    return 0;
}
```

```text
state 0: queued
state 2: complete
state 9: unknown
```

When several `case` labels genuinely share behavior, place them consecutively and write one `break` after the shared code. Do not remove `default` merely because current callers make it look unreachable; external input and future states may still reach it.

## Pitfalls

### Reading an uninitialized automatic object

> **Pitfall:** `int result;` declares an object but does not guarantee a zero value. If a control-flow path reads `result` before assigning it, the program can invoke undefined behavior.

**Fix:** initialize at the declaration, or prove that every path reaching the read performs an assignment first. Enable `-Wuninitialized` and test error branches, empty inputs, and early returns. Never treat the zero observed in one debug run as a guarantee.

### Assuming integer widths and signedness

> **Pitfall:** Writing an `int` into a fixed four-byte protocol or assuming plain `char` is signed turns a property of one platform into a supposed C contract.

**Fix:** use a type from `<stdint.h>` that meets a protocol field's requirement, with the matching `<inttypes.h>` formatting macro. Ordinary counts can still use `int` or `size_t`, but design their bounds from the type's real range rather than a familiar platform.

### Mixing signed and unsigned values

> **Pitfall:** When a `-1` error sentinel is compared with `size_t`, the negative value may convert to a large unsigned value and reverse an apparently obvious ordering.

**Fix:** use compatible types in one calculation domain. If an API returns a signed error code, check the error before converting a valid result. When conversion is necessary, prove that the source is nonnegative and representable in the destination, and let `-Wconversion` locate implicit cases.

### Mismatching a `printf` format

> **Pitfall:** Variadic arguments do not carry enough information for `printf` to correct a type. Reading a `long` with `%d`, an `int` with `%f`, or a `size_t` without `%zu` can all produce undefined behavior.

**Fix:** match every specifier to the promoted argument type and keep `-Wformat=2` enabled. For fixed-width integers, use the `PRI...` macros from `<inttypes.h>` instead of guessing between `%ld` and `%lld` from a byte count.

### Modifying one object repeatedly in an expression

> **Pitfall:** `values[index] = index++;` leaves the read and modification of `index` without a required ordering. Precedence can explain how the expression groups, but it does not sequence these side effects.

**Fix:** perform one visible state change per statement: store the element, then increment the index separately. Review compound expressions containing `++`, `--`, assignment, and function calls. Do not ask a model to predict a result for an expression that has no guaranteed C meaning.

<!-- deep -->

## The C abstract machine and portability boundaries

### Four different kinds of behavior

C portability depends on describing specification boundaries precisely. The language fully specifies some results and leaves other choices to an implementation or a particular execution. Only undefined behavior means that the standard imposes no requirements on the result. Calling all these categories “random” hides which choices can be documented, tested, or constrained.

| Category | Meaning | Response |
|---|---|---|
| Defined behavior | The standard requires a result | Depend on it directly |
| Implementation-defined behavior | The implementation chooses and documents one behavior | Read compiler and target documentation |
| Unspecified behavior | The implementation chooses from an allowed set without documenting the choice | Keep correctness independent of the selection |
| Undefined behavior | The standard imposes no requirements | Exclude it through design, diagnostics, and tests |

The signedness of plain `char` is an implementation choice, function-argument evaluation order often involves an unspecified choice, and out-of-bounds access and signed overflow invoke undefined behavior. Their consequences differ: the first two still have a language-defined set of candidates, while the last has no promise to crash or to continue.

An optimizer may assume that an executing program has no undefined behavior. For example, because signed addition cannot overflow in a valid execution, an optimizer can derive conditions from that assumption. Sanitizers and warnings find many violations, but cannot prove that an arbitrary program is free of undefined behavior. Interface constraints and code review remain the first defense.

### Scope, storage duration, and linkage

Scope controls where a name is visible in source code. Block scope applies to names in function bodies and nested blocks, file scope applies to declarations outside all functions, and parameter names in a function prototype matter only inside that prototype. When an inner declaration shadows an outer name, both objects may still exist; the outer name is merely hidden.

Storage duration controls the minimum lifetime of an object's storage. Ordinary local objects usually have automatic storage duration: their storage begins on entry to the corresponding block and ends on exit. File-scope objects and `static` local objects have static storage duration and last for the program's execution. Allocation and deallocation functions control dynamically allocated storage; its ownership rules belong in the memory-management topic.

Linkage answers whether declarations in different scopes or translation units denote the same entity. A file-scope `static` name has internal linkage and corresponds only within that translation unit. An ordinary external function definition usually has external linkage. Local variables generally have no linkage. Keeping these dimensions separate clarifies “visible here,” “still alive,” and “the same definition.”

### Evaluation order and side effects

Operator precedence and associativity determine syntactic grouping during translation. For example, `a + b * c` groups as `a + (b * c)`. They usually do not determine which operand runs first. Nor may `consume(first(), second())` assume that `first()` always runs before `second()`.

The language uses sequencing relationships to constrain selected evaluations. `&&`, `||`, the comma operator, and the chosen path of the conditional operator establish ordering at important points, but function arguments do not become ordered as a result. If side effects on one scalar object are unsequenced relative to each other, or relative to a value computation that does not determine the value being stored, behavior can be undefined.

The safest code separates stateful calls and increment operations. A temporary variable is not inherently a performance cost. An optimizer can usually remove storage used only to express ordering, while a reviewer can see exactly where each read and modification occurs in a full expression.

### Integer models and boundary checks

Integer conversion considers conversion rank, signedness, and representable range, not merely which type name looks “larger” in source. If one type represents every value of the other, the value can be preserved; otherwise, the rules may take both operands to an unsigned type. This explains why `-1 < sizeof object` can be false: the right side is unsigned `size_t`, and the left side may be converted.

A boundary check must run before the operation that might overflow. Testing `a + b <= LIMIT` may already evaluate an overflowing `a + b`. For known nonnegative signed integers, test `a <= LIMIT - b` after establishing that the subtraction is valid. Multiplication checks must also handle zero, signs, and the minimum negative value; complex cases merit a reviewed helper or a compiler overflow intrinsic.

An exact-width type such as `uint32_t` exists only when the implementation supplies an integer type with exactly 32 bits and no padding. When the requirement is “at least 32 bits,” `uint_least32_t` expresses a looser constraint. Standard-library APIs normally use `size_t` for object sizes and indices. Choose a type from the data contract, not merely to silence a warning.

### Diagnostics and dynamic checking

Strict warnings catch suspicious conversions, missing prototypes, unreachable branches, and format-string defects. Treating warnings as errors keeps new diagnostics from disappearing in logs, although third-party headers or multiple compilers may require layered settings. When suppressing a warning, record the proved precondition instead of hiding it with a cast.

AddressSanitizer mainly helps find out-of-bounds and use-after-free memory defects. UndefinedBehaviorSanitizer detects some overflow, invalid shifts, and alignment problems. They check only paths that actually execute and change program layout and timing, so they neither replace test design nor prove the absence of defects.

A reliable baseline includes a strict-warning build, a test build with sanitizers, and a build near the release optimization level. For code that depends on implementation choices, record the compiler, version, target architecture, and relevant options so a later verification can tell whether the environment changed.

<!-- /deep -->

[Checkpoint: cpp/c-basics](https://codewiki.com/cpp/c-basics/#checkpoint)

## Further reading

Use the GCC manuals for toolchain diagnostics and the language references for types, conversions, and expression rules.

- [GCC warning options](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html)
- [GCC program instrumentation options](https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html)
- [cppreference: C language](https://en.cppreference.com/w/c/language)
- [cppreference: implicit conversions](https://en.cppreference.com/w/c/language/conversion)
- [cppreference: operator precedence](https://en.cppreference.com/w/c/language/operator_precedence)
