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
c.core.1— program structure and translation units.
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:
- an address (where it is);
- a size (how many bytes it occupies, from
sizeof); - an effective type (what type is used to interpret it —
c.obj.effective-type); - a storage duration (how long it lives —
c.lang.storage-duration); - a lifetime (the period during which it exists —
c.lang.lifetime); - an alignment (constraints on its address —
c.obj.alignment); - a value (what it currently represents).
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
- An object is *storage*.
- A value is *meaning*.
- A variable is a *named* object (plus possibly other properties).
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
- Named object: declared with an identifier (a variable).
- Unnamed object: created by
malloc, by a compound literal
(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
- Confusing a variable with its value.
xis an object;xin an expression
evaluates to the value stored in that object.
- Assuming every value has an address. Literals and function names do not.
- Assuming the abstract machine requires a specific memory layout. It does not.
- Assuming that "object" means "object-oriented object." It does not.
Undefined Behavior
- Reading an object whose value is *indeterminate* (for example, an
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.
- Using an object outside its lifetime is undefined behavior.
- Accessing an object through an lvalue of an incompatible type generally
violates strict aliasing (c.obj.aliasing).
Portability
- The size of
intis implementation-defined; the standard only guarantees a
minimum range. c.types.int covers this.
%pformatting and the exact address values are implementation-defined.
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
- When you reason about a pointer, ask: "which object does it point into?"
- When you reason about a bug, ask: "is this object still alive?"
- When you reason about an optimization, ask: "could this reorder change
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.
Related Concepts
c.types.int— integer types and representations.c.lang.storage-duration/c.lang.lifetime— how long objects live.c.ptr.basics— pointers to objects.c.obj.effective-type— how a type is attached to storage.c.ub.definedness— the abstract machine's boundary.
References
- ISO/IEC 9899:2018 §3.15 (object), §3.6 (behavior/observable), §5.1.2.3
(program execution), §6.3.2.1 (lvalues and values).
Verification
- The definition of object is quoted from the standard.
VERIFIED - The three components of observable behavior are standard.
VERIFIED - The claim that
&42is illegal is standard (an integer literal is not an
lvalue). VERIFIED
- Size of
intis implementation-defined.VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] c.lang.abstract-machine — The abstract machine
- [ ] c.lang.obj — Objects
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.lang.abstract-machine | 0 | 5 |
| c.lang.obj | 0 | 5 |