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
c.core.2— objects, values, the abstract machine.c.core.26— structs and alignment.
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
- Object representation: the actual byte contents of an object.
- Value representation: the bits that determine the value.
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
- Casting a
double *to anint *and reading, expecting a cheap bit
reinterpretation (strict aliasing violation).
- Using unions for type punning as if it were always portable (see
c.union.punning).
- Assuming object representation is the same across platforms (endianness,
padding).
Undefined Behavior
- Accessing an object through an lvalue of an incompatible type (other than the
allowed aliases) is UB. VERIFIED
- Writing to a
constobject through a non-const lvalue is UB. - Using an object outside its lifetime is UB.
Portability
- Byte order (endianness) is implementation-defined.
CHAR_BITis implementation-defined (but 8 on mainstream systems).- The presence of padding bits is implementation-defined (and removed for
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
- Use
memcpy(orunsigned charreads) to inspect or reinterpret object
representation portably.
- Treat allocated storage as acquiring the type you first write to it.
- Never cast a pointer to an unrelated type and dereference; use
memcpy.
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.
Related Concepts
c.obj.aliasing— strict aliasing.c.obj.representation— representation.c.union.punning— union type punning.c.mem.memcpy— representation copying.
References
- ISO/IEC 9899:2018 §6.2.6 (representations of types), §6.5 (effective type),
§6.3.2.3 (conversions).
Verification
- Effective type established by first write to allocated storage.
VERIFIED unsigned charmay inspect any object's representation.VERIFIED- Reading through incompatible lvalue is UB.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Bytes and CHAR_BIT
- [ ] Object vs. value representation
- [ ] Effective type
- [ ] memcpy representation reinterpretation
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.obj.bytes | 0 | 6 |
| c.obj.representation | 0 | 6 |
| c.obj.effective-type | 0 | 7 |