C Mastery / Objects, Values, and the Abstract Machine
Part 1 — The Core Language

Objects, Values, and the Abstract Machine

This chapter introduces the two most important mental models in C: the *abstract machine* that defines what a program means, and the *object* that is the unit of storage.

Why This Matters

Almost every later concept — pointers, arrays, structs, lifetime, aliasing, undefined behavior — is defined in terms of objects. If your mental model of "object" is wrong, everything downstream is wrong. And the abstract machine is what separates "what the compiler must do" from "what the compiler may do."

Prerequisites

Core Concept

The abstract machine

The C standard defines program behavior in terms of an abstract machine: an idealized computer that executes your program statement by statement. A conforming compiler must produce an executable whose *observable behavior* matches that of the abstract machine.

Observable behavior consists of exactly:

1. reads and writes of volatile objects; 2. calls to library I/O functions; 3. the program's termination status.

Everything else is fair game for the compiler to reorder, eliminate, or transform, as long as the observable behavior is preserved. This is the foundation of all C optimization.

Crucially, the standard only promises this equivalence if your program has no undefined behavior. Once UB exists anywhere, the compiler is no longer obligated to preserve anything. Part 2 develops this fully.

Objects

In C, an object is:

> a region of data storage in the execution environment whose contents can > represent values.

This is *not* the object-oriented "object" from other languages. A C object is closer to "a place where a value lives." A int variable is an object. An element of an array is an object. A block of memory returned by malloc is an object.

Every object has:

Values

A value is the meaning of an object's contents. A value has a type. An object of type int can hold an int value. The same byte pattern can be a different value under a different type, which is why types and objects are separate concepts.

The object/value distinction, precisely

Not every value is an object. A literal like 42 is a value but not an object — it has no storage you can take the address of. A function is not an object. A type is not an object.

Syntax

Declaring an object

int x;        /* an object of type int, automatic storage duration */
static int y; /* an object of type int, static storage duration */

The declaration int x; reserves storage for an object of type int, gives it the name x, and — depending on where it appears — gives it automatic or static storage duration.

Taking an object's address

int x = 5;
int *p = &x;   /* &x yields the address of object x */

The unary & operator produces a pointer to its object operand. This is the bridge from objects to pointers (c.ptr.basics).

Examples

An object and its size

#include <stdio.h>

int main(void)
{
    int x = 5;
    printf("address: %p\n", (void *)&x);
    printf("size:    %zu\n", sizeof x);
    return 0;
}

Expected output (address varies; size is implementation-defined but typically 4 on modern systems):

address: 0x7ffd...
size:    4

%p prints a pointer; casting &x to void * is required because %p expects a void *. %zu prints a size_t value.

A value that is not an object

int main(void)
{
    /* 42 is a value with no storage; &42 is illegal */
    return 0;
}

You cannot write &42. An integer literal is not an lvalue (an expression that designates an object).

Objects with different storage durations

#include <stdio.h>

int global;          /* static storage duration, external linkage */

static int file_scope; /* static storage duration, internal linkage */

int main(void)
{
    int local = 1;   /* automatic storage duration */
    static int persistent = 2; /* static storage duration */
    printf("%d %d %d %d\n", global, file_scope, local, persistent);
    return 0;
}

global and file_scope are initialized to 0 (the default for static storage). local is initialized explicitly to 1. persistent keeps its value across calls to main, but main is called once, so that matters more for other functions.

How It Works

When a program runs, the abstract machine executes it in a sequence of *evaluations* of *expressions*. Evaluating an expression produces a value or a designation of an object. When an expression is an lvalue, it designates an object; otherwise it produces a value only.

At the hardware level, an object is a range of memory addresses. But the abstract machine is deliberately *above* the hardware: it does not require a specific register allocation, cache behavior, or instruction selection. The compiler is free to keep a variable in a register and never write it to memory at all — unless that would change observable behavior.

Variations

lvalues, rvalues, and the (informal) terminology

The C standard uses lvalue precisely: an expression that potentially designates an object. Historically, "rvalue" was used informally for "not an lvalue." The term "modifiable lvalue" means an lvalue that can be the target of an assignment (not const, not an array type, etc.).

In modern C (C11 and later), the standard sometimes uses "locator value" or just "value" in places, but "lvalue" remains the workhorse term.

Named vs. unnamed objects

(c.struct.compound-literal), or by a string literal.

Unnamed objects still have addresses and lifetimes; they just have no name bound to them directly.

Common Mistakes

evaluates to the value stored in that object.

Undefined Behavior

uninitialized automatic variable of a type with no guaranteed default) can be undefined behavior or an unspecified value, depending on the type and context. c.obj.indeterminate covers this precisely.

violates strict aliasing (c.obj.aliasing).

Portability

minimum range. c.types.int covers this.

Under the Hood

At runtime, a C object is a region of memory with a type interpreted by the generated code. The compiler tracks, at compile time, which objects exist and their types; the running program mostly just manipulates addresses and values. This compile-time/run-time split is why type information is generally *not* present at run time in C (no reflection), and why sizeof is a compile-time constant for non-VLA types.

Practical Usage

observable behavior?"

These three questions recur throughout systems and embedded programming.

Exercises

1. Write a program that prints the address and sizeof of an int, a char, and a double. Explain why the sizes differ and why the addresses differ. 2. Write a function that increments a static local and a function that increments an automatic local; call each several times and observe the difference. This demonstrates storage duration. 3. Determine, by consulting the standard or your compiler's docs, which expression in &42 is illegal and why.

Deep Challenge

Explain the observable behavior of this program under the abstract machine, and identify what a compiler may legally eliminate or reorder:

#include <stdio.h>

int main(void)
{
    int x = 1;
    x = 2;              /* is this a write to a volatile object? */
    x = 3;
    printf("%d\n", x);
    return 0;
}

Then modify it to use volatile int x; and explain what changes and why.

References

(program execution), §6.3.2.1 (lvalues and values).

Verification

lvalue). VERIFIED

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.lang.abstract-machine05
c.lang.obj05