Lesson 15 / 15
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.
Published Updated
In this lesson
- 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
Contents
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.
// 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
// 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.
// 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, andINT_MAX >= 32767all 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, butcounter.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_titself 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.