C / STANDARD LIBRARY TOUR
stddef.h, NULL, size_t and ptrdiff_t done right
Name and print size_t and ptrdiff_t correctly, avoid unsigned wraparound in loops and comparisons, and use NULL and offsetof the way the standard defines them.
What you will learn
- Print sizes with %zu and pointer differences with %td, never %d or %lu
- Write countdown loops that stay correct when the size_t count is zero
- Use ptrdiff_t for distances and for an index that may be -1; size_t for counts
- Read struct layout with offsetof instead of adding member sizes by hand
Understanding stddef.h, NULL, size_t and ptrdiff_t done right
<stddef.h> declares no functions. Its job is to give names to things the core language already produces: size_t is the type of every sizeof expression, ptrdiff_t is the type of a pointer subtraction, NULL is the spelling of the null pointer constant, and offsetof reports where a struct member sits (wchar_t and max_align_t live here too). Because <stdio.h>, <string.h> and <stdlib.h> are also required to define size_t and NULL, a file that forgets the include often still compiles, and that is luck rather than portability, so include <stddef.h> whenever you write those names yourself.
size_t is unsigned and wide enough to hold the size in bytes of the largest object the implementation supports. Model it as a byte count rather than as a number: it cannot be negative, so 0 - 1 is not -1 but the maximum size_t value, and i >= 0 is a tautology for a size_t i. That one fact is behind nearly every size_t bug, including countdown loops that never end, an i <= n - 1 bound that explodes when n is 0, and comparisons where a negative int silently becomes an enormous value because the usual arithmetic conversions widen the signed operand to the unsigned type, never the other way round.
ptrdiff_t is the signed companion: subtracting two pointers into the same array gives a ptrdiff_t measured in elements, not bytes, and it is the natural type for a distance, an offset, or an "index, or -1 if absent" return value. Because the widths of both types vary by platform (size_t is unsigned long on 64-bit Linux but unsigned long long on 64-bit Windows), printf has the %zu and %td conversions, since a variadic call has no prototype that could convert the argument for you. Using %d or %lu on a size_t is undefined behaviour that merely happens to work where the widths accidentally line up.
placeholder
<stdio.h>
<stddef.h>
int main(void)
{
int a[] = { 10, 20, 30, 40, 50 };
size_t n = sizeof a / sizeof a[0];
printf("n = %zu, sizeof n = %zu\n", n, sizeof n);
int *first = a;
int *last = a + n - 1;
ptrdiff_t gap = last - first;
printf("gap = %td elements, %zu bytes\n", gap, (size_t)gap * sizeof a[0]);
for (ptrdiff_t i = (ptrdiff_t)n - 1; i >= 0; i--)
printf("a[%td] = %d\n", i, a[i]);
size_t empty = 0;
printf("0 - 1 as size_t = %zu\n", empty - 1);
int *p = NULL;
printf("p is %s\n", p == NULL ? "null" : "not null");
return 0;
}
sizeof and pointer subtraction have real types, unsigned size_t and signed ptrdiff_t, and most size bugs come from pretending they are int.
Worked examples
A negative int against a size_t bound
Shows how the usual arithmetic conversions turn -1 into a huge unsigned value in a comparison.
<stdio.h>
<stddef.h>
static size_t count_below(const int *v, size_t n, int limit)
{
size_t hits = 0;
for (size_t i = 0; i < n; i++)
if (v[i] < limit)
hits++;
return hits;
}
int main(void)
{
int v[] = { 1, 9, 2, 8 };
size_t n = sizeof v / sizeof v[0];
int i = -1;
printf("below 5: %zu of %zu\n", count_below(v, n, 5), n);
printf("i < n: %s\n", i < n ? "true" : "false");
printf("(ptrdiff_t)i < (ptrdiff_t)n: %s\n",
(ptrdiff_t)i < (ptrdiff_t)n ? "true" : "false");
return 0;
}
Example explained
Line 1sizeof v / sizeof v[0] is size_t arithmetic throughout, so n needs no cast and can never come out negative.
Line 2hits prints with %zu because printf receives raw argument bytes; %d would read four of the eight bytes an LP64 size_t occupies.
Line 3In i < n the int is converted to size_t, since size_t is at least as wide, so -1 becomes SIZE_MAX and the test is false; -Wextra reports this as a sign-compare warning.
Line 4Casting both sides to the signed ptrdiff_t restores the arithmetic you meant, which is why a bounds check should compare like types instead of mixing them.
ptrdiff_t as an index or -1
Uses a real pointer subtraction as the return value of a search function.
<stdio.h>
<stddef.h>
<string.h>
static ptrdiff_t index_of(const char *s, char c)
{
const char *hit = strchr(s, c);
return hit ? hit - s : -1;
}
int main(void)
{
const char *s = "ptrdiff_t";
printf("'_' at %td\n", index_of(s, '_'));
printf("'z' at %td\n", index_of(s, 'z'));
return 0;
}
Example explained
Line 1hit - s subtracts two pointers into the same array, so the expression already has type ptrdiff_t and no cast is needed.
Line 2Returning -1 is legal because ptrdiff_t is signed; a size_t version would need a SIZE_MAX sentinel that callers forget to test.
Line 3%td matches ptrdiff_t exactly, and for char pointers the element count and the byte count coincide.
Line 4The subtraction is only defined because both pointers point into the same string object; differencing unrelated pointers is undefined.
offsetof and hidden padding
Demonstrates why member offsets are not the running sum of member sizes.
<stdio.h>
<stddef.h>
struct packet {
unsigned char kind;
unsigned int length;
char body[8];
};
int main(void)
{
printf("kind at %zu\n", offsetof(struct packet, kind));
printf("length at %zu\n", offsetof(struct packet, length));
printf("body at %zu\n", offsetof(struct packet, body));
printf("total %zu bytes (members add up to %zu)\n",
sizeof(struct packet),
sizeof(unsigned char) + sizeof(unsigned int) + sizeof(char[8]));
return 0;
}
Example explained
Line 1offsetof expands to a size_t byte offset, so every one of these values prints with %zu.
Line 2length sits at 4 and not at 1 because the compiler inserts three padding bytes so the unsigned int starts on a 4-byte boundary.
Line 3The struct is 16 bytes while its members total 13, which is exactly why adding sizeofs by hand produces wrong offsets.
Line 4These numbers follow common x86-64 and AArch64 alignment rules; another ABI may pad differently, which is why offsetof is supplied by the implementation.
Important notes
NULL is guaranteed only to be a null pointer constant, and it may expand to plain 0, so in a variadic call such as execl write (char *)NULL; otherwise the callee may read pointer-sized bytes where an int was pushed.
%zu and %td are C99 features, so compile with -std=c99 or later, and remember the widths vary: the wraparound value printed above is 4294967295 in a 32-bit build, so take the real limit from SIZE_MAX in <stdint.h> instead of hardcoding it.
Common mistakes
Counting down with for (size_t i = n - 1; i >= 0; i--): an unsigned value is never negative, so the condition never fails, and after i == 0 it wraps to SIZE_MAX and indexes far outside the array, giving a hang or a segfault instead of a clean stop.
Printing a size with printf("%d", sizeof x): printf reads four bytes of an argument that is eight bytes wide on a 64-bit ABI, so the number is garbage and the program is undefined; only -Wformat (included in -Wall) warns you.
Comparing a signed index against a size_t bound, as in int i = -1; if (i < n): the int is converted to size_t, -1 becomes SIZE_MAX, and the bounds check you wrote to reject negative indices silently accepts them.
Try it yourself
Change, predict, then run
Write void print_reverse(const int *v, size_t n) that prints the elements from last to first using the while (n-- > 0) idiom, then call it once with a 3-element array and once with n = 0, and confirm the second call prints nothing instead of running billions of iterations.
Open the C workspaceCheck your understanding
With size_t n = 0;, why does for (size_t i = 0; i <= n - 1; i++) run roughly SIZE_MAX times instead of not at all?
- n - 1 is evaluated in unsigned arithmetic, so it wraps to the largest size_t value and the condition holds for every reachable i
- size_t is signed on 64-bit platforms, so n - 1 is -1 and any i compares greater than -1
- Both operands are promoted to int, and signed integer overflow makes the comparison true
- The compiler evaluates n - 1 once before i is initialised, so the zero case is never checked
Show answer
Unsigned arithmetic is modular, so 0 - 1 is SIZE_MAX rather than -1, making the bound enormous and pushing the loop far past the array. The promotion answer is tempting because integer promotions are real, but they only widen types narrower than int, and size_t is at least as wide as int, so nothing becomes signed here; writing i < n instead is correct and stops immediately when n is 0.