C Mastery / The Object Model: Bytes, Representation, Effective Type
Part 2 — The Object Model and Undefined Behavior

The Object Model: Bytes, Representation, Effective Type

This chapter defines the low-level model of what an object *is*: a region of bytes, a value representation, and an effective type that determines how those bytes may be accessed. This is the conceptual core of memory safety in C.

Why This Matters

Everything the compiler does — aliasing analysis, optimization, code generation — depends on the object model. The effective type rules are what make it legal to write to a malloced region as int and read it back as int, and illegal to reinterpret it arbitrarily. Violating these rules is the source of some of the most confusing UB in C.

Prerequisites

Core Concept

Bytes

The C memory model is byte-oriented. An object occupies a contiguous sequence of bytes. A byte is the smallest addressable unit of storage, at least 8 bits (CHAR_BIT >= 8, and exactly 8 on all mainstream platforms). char, signed char, and unsigned char are one byte wide; unsigned char is the type used to inspect raw object representation.

Object representation vs. value representation

For most integer types on real hardware, these coincide. But the C standard allows padding bits (object representation) that do not contribute to the value (value representation). C23 removed padding bits for standard integer types; C17 allowed them in principle. STANDARD-VERSION-DEPENDENT

You can read an object's object representation by copying it into an unsigned char array with memcpy — this is always well-defined.

Effective type

The effective type of an object is the type used to access it. For a declared object, the effective type is simply its declared type. For allocated storage (from malloc/calloc/realloc) with no declared type, the effective type is established by the first write:

int *p = malloc(sizeof *p);
*p = 5;          /* effective type of the storage becomes int */

Thereafter, the storage must be accessed only through lvalues of type int (or a few compatible/aliasing-permitted types), not arbitrarily as, say, float.

Syntax

Copying object representation (well-defined)

#include <string.h>

unsigned char bytes[sizeof(double)];
double d = 3.14;
memcpy(bytes, &d, sizeof d);   /* now bytes holds d's representation */

Copying representation back (well-defined)

double d2;
memcpy(&d2, bytes, sizeof d2);  /* reinterprets the bytes as double */

Examples

Reading representation with unsigned char

#include <stdio.h>
#include <string.h>

int main(void)
{
    unsigned int x = 0x01020304u;
    unsigned char b[sizeof x];
    memcpy(b, &x, sizeof x);
    for (size_t i = 0; i < sizeof x; i++)
        printf("%02x ", b[i]);
    printf("\n");
    return 0;
}

Expected output depends on endianness. On a little-endian machine: 04 03 02 01. On a big-endian machine: 01 02 03 04. This is implementation-defined (byte order), not UB.

Establishing effective type

#include <stdlib.h>

int main(void)
{
    int *p = malloc(sizeof *p);
    if (p == NULL) return 1;
    *p = 42;         /* effective type: int */
    /* reading as float would violate aliasing */
    free(p);
    return 0;
}

How It Works

The compiler uses the effective type to reason about aliasing and to generate loads/stores of the right width and interpretation. The standard's aliasing rules (c.obj.aliasing) say which types are allowed to access an object of a given effective type. The unsigned char exception permits inspecting raw bytes regardless of the effective type.

Variations

Declared objects have fixed effective type

A variable int x; always has effective type int. You cannot legally read it as a float through a pointer cast. Use memcpy to reinterpret its representation.

Allocated storage is "untyped" until written

calloc zeroes the storage, but the effective type is still not established until a (non-character) write. Reading the zeroed bytes through unsigned char is fine.

Common Mistakes

reinterpretation (strict aliasing violation).

c.union.punning).

padding).

Undefined Behavior

allowed aliases) is UB. VERIFIED

Portability

standard integer types in C23).

Under the Hood

The compiler's alias analysis uses effective type to prove that two pointers do not alias, enabling reordering and vectorization. When you violate effective type, you break the compiler's assumptions, which is why the behavior is undefined rather than "just read the bits."

Practical Usage

representation portably.

Exercises

1. Write a function that prints the byte representation of an int and a double using unsigned char. 2. Demonstrate that memcpy round-trips a double through bytes correctly. 3. Explain what effective type is established by each write in a sequence of malloc + writes. 4. Attempt to read a float as an int via pointer cast and run under UBSan.

Deep Challenge

Using the C standard's definition of effective type and the aliasing rules, explain precisely why this is UB and what could go wrong at the optimizer level:

float f = 1.0f;
int *p = (int *)&f;
int x = *p;

Then show the well-defined memcpy-based alternative and explain why it is correct.

References

§6.3.2.3 (conversions).

Verification

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.obj.bytes06
c.obj.representation06
c.obj.effective-type07