---
title: Portability
source: 'https://academia.sh/en/courses/c-memory/portability'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Distinguishes why an address, a type's size, and a zero-size allocation were never printed"
    - 'Isolates target-dependent behavior by printing a comparison the standard guarantees instead of a value'
    - "Gathers the course's fifteen lessons into a single table by ownership class"
---

# Portability

Three values the shared core deliberately never printed — addresses, type sizes other than char, malloc(0)'s return — are target-dependent; this lesson isolates target-dependent behavior by printing a comparison the standard guarantees instead of a value, and gathers the course's fifteen lessons into a closing table.

The previous lesson measured that the `limit` parameter is written in the caller, not the
source. This lesson closes the course itself and leaves behind a single question: what the
measurement core never **printed** from the start. `ownership.c` printed thirteen
observations, but it never printed three things — addresses, type sizes other than
`sizeof(char)`, and whether `malloc(0)` returns null. All three were left out for the same
reason: all three are **target-dependent**, meaning the same source text can produce two
different numbers on two different targets.

This lesson does not try to print those three values. Instead it prints a comparison the
standard **guarantees**. The difference is this: printing `sizeof(int)` gives a number, and
that number changes by target; asking the question `sizeof(char) == 1` also gives a result,
but this result is the **same** on every target, because the standard itself guarantees it.
What is measured is not two approaches to the same question — one exposes a target-dependent
value, the other exposes the target-independent logical result the standard leaves fixed.
This lesson's ownership class is the **language**: the party giving the guarantee is not a
caller, a callee, or a block left unowned, it is the language's standard itself.

## Three Comparisons, One Isolation Technique

The measurement core's counter unit is also used in this lesson; its excerpt is below,
unchanged.

```c
// counter.h — a wrapper that counts every allocation and every free
#ifndef COUNTER_H
#define COUNTER_H

#include <stddef.h>

void *count_alloc(size_t size);
void *count_grow(void *old, size_t size);
void count_free(void *block);
int allocated_count(void);
int freed_count(void);

#endif
```

```c
// counter.c — the counters live only in this unit
#include <stdlib.h>
#include "counter.h"

static int allocated = 0;
static int freed = 0;

void *count_alloc(size_t size) {
    void *block = malloc(size);
    if (block != NULL) {
        allocated++;
    }
    return block;
}

void *count_grow(void *old, size_t size) {
    int new_block = (old == NULL);
    void *grown = realloc(old, size);
    if (grown != NULL && new_block) {
        allocated++;
    }
    return grown;
}

void count_free(void *block) {
    if (block != NULL) {
        freed++;
    }
    free(block);
}

int allocated_count(void) {
    return allocated;
}

int freed_count(void) {
    return freed;
}
```

**RO10.** `counter.c` keeps the same behavior in this lesson too: `count_alloc` increments its
counter only if the return is not `NULL`, `count_free` increments its counter only if the
argument is not `NULL` and still calls `free` regardless. These two lines are set up in
advance for this lesson's last observation — even though what `malloc` returns when `size` is
zero is target-dependent, the behavior of these two lines in `counter.c` **does not change**.

The new file measures the three comparisons and a zero-size allocation together. No line
prints a size, a limit value, or an address; every line only prints a comparison's result.

```c
// portability.c — prints the standard's guarantee instead of a value
#include <stdint.h>
#include <limits.h>
#include <stdio.h>
#include "counter.h"

static void observe(const char *name, const char *result) {
    printf("%-34s%s\n", name, result);
}

int main(void) {
    printf("%-34s%s\n", "observation", "result");

    observe("fixed-width integer", sizeof(int32_t) * CHAR_BIT == 32 ? "guarantee holds" : "guarantee does not hold");
    observe("char size", sizeof(char) == 1 ? "guarantee holds" : "guarantee does not hold");
    observe("int lower bound", INT_MAX >= 32767 ? "guarantee holds" : "guarantee does not hold");

    int first = 7;
    int second = 7;
    int *same = &first;
    int *again = &first;
    int *different = &second;
    observe("two addresses, one variable", same == again ? "equal" : "not equal");
    observe("two addresses, two variables", same == different ? "equal" : "not equal");

    int before = allocated_count() - freed_count();
    void *zero_size = count_alloc(0);
    count_free(zero_size);
    int after = allocated_count() - freed_count();
    observe("zero-size allocation contract", after - before == 0 ? "contract held" : "contract broken");

    return 0;
}
```

```
observation                       result
fixed-width integer               guarantee holds
char size                         guarantee holds
int lower bound                   guarantee holds
two addresses, one variable       equal
two addresses, two variables      not equal
zero-size allocation contract     contract held
```

**RO11.** The first three lines follow the same pattern: instead of directly printing a
type's size or a limit value, each compares it against a lower bound the standard already
guarantees. When `int32_t` exists it is exactly thirty-two bits wide — this comes from the
definition itself, not from the target. This is also why the comparison is built on bit count
rather than byte count: the number of bits in a byte (`CHAR_BIT`) is guaranteed by the
standard only to be at least eight, so `sizeof(int32_t)` can vary from target to target, while
`sizeof(int32_t) * CHAR_BIT` does not. `sizeof(char)` is always one — this too comes from the
definition; this is exactly why no size other than `sizeof(char)` has been printed anywhere
in the course. `INT_MAX` is guaranteed by the standard to be at least `32767`; its real value
may be larger depending on the target, but whether this lower bound **holds** gives the same
result on every target.

**RO12.** The fourth and fifth lines measure pointer equality without printing addresses.
`same` and `again` hold the address of the same variable; their equality comes from a
language rule — the same expression (`&first`) gives the same result every time it is
evaluated. `same` and `different`, on the other hand, hold the addresses of two separate
automatic objects alive at the same time; the standard guarantees that these two objects will
not overlap, so the inequality too is not an implementation coincidence, it is a language
guarantee. Both lines measure these two guarantees without ever showing the address
**itself**.

**RO13.** The last line takes up the shared core's third unprinted value: whether `malloc(0)`
returns null is target-dependent, and this lesson does **not answer** that question. Instead
it measures whether `counter.c`'s two lines are affected by that ambiguity. If `count_alloc(0)`
returns `NULL`, the counter never increments and `count_free(NULL)` never increments either —
the difference stays zero. If `count_alloc(0)` returns a valid pointer, the counter increments
by one and the counter increments by one again as `count_free` frees it — the difference
still stays zero. Which branch `malloc(0)` took is never visible; the only thing visible is
that the contract stays the **same on both branches**.

## The Fixed-Width Types' Own Limit

The isolation `<stdint.h>` offers is not unlimited by itself. The standard requires
full-width types like `int32_t` to be defined only **if** the target platform has such a
type; in its absence these types are not defined at all, and a source file using them does
not compile. The standard gives the types it requires to exist on every target separate
names — `int_least32_t` guarantees being at least thirty-two bits wide, `int32_t` guarantees
being exactly thirty-two bits wide, but only when it exists.

**RO14.** That this lesson uses `int32_t` directly instead of testing for its existence is an
assumption made by the lesson itself: this type **exists** in the environment where the
lesson is compiled. The isolation technique itself thus also rests on an assumption — not
about the width, but about the type's **existence**. Where the exact width is not required, a
portable source uses `int_least32_t` or `int_fast32_t`; this lesson chose `int32_t` only as an
example.

## The Bounding Measurement

Printing a guarantee instead of a value does not **eliminate** what is target-dependent — it
only moves it outside the observation. The expression `sizeof(int32_t) * CHAR_BIT == 32` gives
the same result on every target, but only when `int32_t` **exists**; the standard itself does
not require this type to exist. Likewise `INT_MAX >= 32767` holds on every target, but the
real `INT_MAX` value can vary from target to target — the lesson preserves portability by
never printing this value, but leaves the actual question it wanted to test (exactly what
`INT_MAX` is) unanswered. A comparison is more portable than a value, but it carries less
information; this trade-off between portability and information marks **where** the
language's guarantee ends. None of the measurement core's four classes eliminates this
trade-off — including the language class, because the language that gives the guarantee also
draws the guarantee's own boundary.

## Summary

- The memory class this lesson measures is the **language**: the party giving the guarantee
  is not a caller, a callee, or a block left unowned, it is the language's standard itself.
- Three values the shared core deliberately never printed — addresses, type sizes other than
  `sizeof(char)`, `malloc(0)`'s return — are all target-dependent, and none of them were
  printed in this lesson either.
- When a comparison the standard guarantees is printed instead of a value, the result stays
  the same on every target; `sizeof(int32_t) * CHAR_BIT == 32`, `sizeof(char) == 1`, and
  `INT_MAX >= 32767` all followed this pattern.
- The addresses of two automatic objects alive at the same time do not overlap; this
  inequality was measured only with the `==` comparison, never by printing the address
  itself.
- Whether `malloc(0)` returns null stays target-dependent, but `counter.c`'s
  allocate-free contract was unaffected by that ambiguity: the difference stayed zero on
  both branches.
- Printing a guarantee does not eliminate what is target-dependent, it only moves it outside
  the observation — `int32_t` itself is a type that may not exist.

## Course Wrap-Up

Throughout the course, a single question was asked: who owns this memory. Fourteen lessons
answered that question with four classes — language, caller, callee, unowned — and this
lesson showed what the same question says when asked of the language itself: where the
standard guarantees something, the language owns it; where the guarantee ends, the
observation stays target-dependent and is not printed.

| Lesson | Memory measured | Owner | Bounding measurement |
|---|---|---|---|
| Pointer Basics | Local variable, static storage, string literal, local array | language | A pointer's type does not say who will free it |
| Pointer Arithmetic | In-array advance and element difference | language | Pointing one past the end is valid, dereferencing it is undefined |
| Arrays and Pointers | Dynamically allocated array | caller | Decay works at only one level, it does not carry the element count |
| Null and Dangling Pointers | Pointer pointing at a freed block | unowned | Assigning NULL only makes that one pointer checkable, it does not affect another pointer pointing at the same block |
| `void` and Opaque Pointers | Opaque record (`Record`) | callee | Opacity hides the struct's inside, not its ownership: the obligation to free is written only in the comment |
| Function Pointers | Context carried into a callback | caller | A signature guarantees no behavior, it only checks type compatibility |
| Memory Allocation Functions | Block allocated with malloc/calloc/realloc | caller | malloc(0) and realloc(block, 0)'s return is implementation-defined, never printed |
| Deallocation Discipline | Lent name and returned copy | callee | Ownership is read not from the signature but from the comment in the header file |
| Memory Leaks | Two unowned blocks | unowned | A leak produces no crash or warning, it only shows up as a counter difference |
| Memory Debugging Tools | Balanced and unbalanced allocate-free pair | unowned | A tool only counts the unmatched call, it does not read the contract violation |
| Memory Layout | Variable in one of four memory regions | language | Region boundaries and alignment are not printed; outside the heap, ownership comes from the contract, not the region |
| Undefined Behavior | Defined counterpart of four undefined operations | unowned | Looking like it works does not show that it is defined |
| Error Handling Patterns | Block cleaned up on the error path | callee | Who cleans up on partial failure must be written separately in every function |
| Bounds Checking | Copy bounded with snprintf | caller | The function only checks the limit it is told, it cannot catch a wrongly reported limit |
| Portability | Fixed-width type, sizeof(char), INT_MAX, zero-size allocation | language | The isolation technique itself rests on a guarantee too: int32_t's existence is not required by the standard |

The lessons are distributed across classes as **4 language · 4 caller · 3 callee · 4
unowned** — this is the lessons' distribution and is not to be confused with the shared
core's thirteen observations' 4/4/3/2.

The four classes never changed across fifteen lessons, but each lesson showed the class
through a different face: sometimes the owning party was a function, sometimes a contract,
sometimes nobody at all, and sometimes, as seen in this last lesson, the language itself. A
pointer's type never once told its owner; the owner was decided every time either by the
source itself (an `&` expression, an allocation call) or by the contract written in the
header file.

The next course turns to C and systems programming, to operating-system interfaces. This
course's ownership question keeps holding there too — who owns a file descriptor, a process,
or a signal is shaped in the same form as the question asked for memory — but that course
will establish its own resources' classes with its own measurement.
