Proven C Book한국어 GitHub

26 The families of types — how the standard divides them

What to know first

chapter 23, Declaring variables · a declaration — type, name and first value
chapter 6, Memory and addresses · size and alignment
chapter 8, Representing numbers · how integers and floating point are represented

Looking back

Chapter 23 kept using the word “type” while writing int x = 0;. So what exactly does a type settle? Name three.

A. Four can be named.

  • Size — how many bytes this value occupies (chapter 6).
  • Representation — through what eye those bytes are read (chapter 7 and 8′s two’s complement and IEEE 754).
  • Permitted operations — what may be done (% only on integers, ++ only on real types and pointers).
  • Contract — what the compiler checks and what it guarantees.

This chapter sets out what branches exist under that word “type”, exactly as the standard divides them.

The need for this chapter, and its context

The reason part 6 opens with the families of types is simple — this book has already been using words like “arithmetic type”, “scalar” and “aggregate”. This is where those borrowed words are paid for. Any earlier and the definitions would have nowhere to land; any later and they would have been borrowed twenty times over. Now is exactly the right moment to settle up.

By the end of this chapter

This book has been using words like “arithmetic type”, “scalar type”, “aggregate” and “complete type” already. This is where they get defined. The whole classification of §6.2.5 gets built — object and function, complete and incomplete, basic types, integer, real and arithmetic, derived, scalar and aggregate, qualifiers — and on top of it the types of <stdint.h> that write their width into their name. The next chapter’s integers, and the one after that’s promotion, stand entirely on this vocabulary.

The questions this chapter answers

  1. void means “nothing at all”, so why is it a type?
  2. Then is int also a separate type from signed int? By the same logic it ought to be.
  3. So are qualifiers and storage classes the only words a declaration takes?
  4. “Optional”? Is there really a machine without uint8_t?

26.1 The four things a type settles

Given the same eight bytes, reading them as a double gives 3.14 and as a long gives 4614253070214989087 (exactly as chapter 8 showed). Bytes do not know what they are; only the type knows.

What a type settlesExampleMore
sizesizeof(int) = 4chapter 6
representation-1 as an int is FF FF FF FFchapters 7, 8
permitted operations% only on integers, / on arithmetic typeschapters 28, 49
contractconst says “I will not change this”chapters 23, 51

Table 26.1

So “knowing a type” is not knowing syntax but knowing those four. And the standard settles those four branch by branch.

26.2 The first division — object types and function types

The topmost division is in two.

BranchWhat it isExamples
object typedescribes a place that holds a valueint, double, struct point, int[10], char *
function typedescribes something that works — by return type and parametersint(void), void(int, char *)

Table 26.2

This division shows itself in practice. sizeof cannot be applied to a function type, and being no object it has neither size nor alignment. That is why treating a function as a value means turning it into a pointer to a function (chapter 59).

26.2.1 Complete and incomplete types

Object types divide again — is the size known?

StateWhat it cannot doExample
complete type(no restriction)int, struct point once its definition is seen
incomplete typesizeof is unusable, and no object of it can be madean array with no size int a[], a struct node declared by tag only
voidan incomplete object type that cannot be completedits set of values is empty

Table 26.3

An incomplete type is not a defect but a tool. It can express “I know this type exists and nothing of its insides”, which is what makes an opaque type possible — hiding the insides in a header and passing only pointers (FILE * is the archetype). Hiding the insides means callers cannot depend on them, and that is the design gain (chapter 47).

Q. void means “nothing at all”, so why is it a type?

A. Because it does three different jobs in three places.

  • void f(void) — “there is no value to return” and “there are no parameters”.
  • (void)printf(…) — a cast that writes down “I am discarding this value”.
  • void * — “what kind of object it points at is not settled yet”. This one alone is a complete object type (being a pointer, it has a size).

The standard’s definition is “an incomplete object type whose set of values is empty and that cannot be completed”. So no void variable can be made and sizeof(void) is not usable in the standard — GCC gives 1 as an extension (chapter 12′s grey area).

26.3 The basic types

What the standard calls the basic types is exactly these three groups together.

What the basic types are (§6.2.5p18)Note
charjust one. One of the three character types below
the signed and unsigned integer typesshort, int, long, long long and their unsigned partners
the floating typesfloat, double, long double, and complex

Table 26.4

Enumerated types are not basic types. They are integer types, but they do not appear in the list of basic types — a commonly confused spot.

The standard nails down one more sentence about them: “even if the implementation defines two or more basic types to have the same representation, they are nevertheless distinct types.” The character types of the next section are the example of that sentence.

26.3.1 There are three character types

TypeSignednessWhere it is used
charthe implementation settles it — the same range, representation and behaviour as either signed char or unsigned charfor holding characters
signed charsigneda small signed integer
unsigned charunsignedfor looking at bytes (chapter 48)

Table 26.5

The heart of it is the sentence a footnote nails down — whichever choice was made, char is a separate type from the other two and is compatible with neither. There are three, not two.

Q. Then is int also a separate type from signed int? By the same logic it ought to be.

A. No. And that asymmetry is exactly what makes char special. int and signed int are the same type — two spellings of one thing. Whereas char, signed char and unsigned char are three different types.

★ The standard shows this in a table of its own. §6.7.3.1 lists the permitted combinations of type specifiers, and spellings listed in the same item are the same type.

The standard’s itemSo
char★ an item all to itself
signed char★ an item all to itself
unsigned char★ an item all to itself
int, signed, signed intthe three share one item — one type, three spellings
unsigned, unsigned inttwo in one item

Table 26.6

Only the character types are scattered across three items. Every other integer type has just two sides, signed and unsigned, and writing signed changes nothing.

This book asked the compiler directly. _Generic refuses two branches of the same type, which makes it usable as a decision procedure.

_Generic((i), int: "a", signed int: "b")   /* error: two compatible types */

GCC rejects it with _Generic specifies two compatible types” — the compiler declaring that the two are one type. Write char, signed char and unsigned char as three branches, by contrast, and it says nothing; each goes to its own.

The place the difference becomes visible is pointers. Measured, it splits like this.

char c;  take_signed_char(&c);   /* warning: … differ in signedness */
int  i;  take_signed_int(&i);    /* not a word --- they are the same type */

So char * is not compatible with signed char *, even on a machine where char is signed and the ranges are identical — the earlier rule that types with the same representation are nevertheless distinct (§6.2.5) applies here in full.

A common misconception. “Then int x : 3; must be the same as signed int x : 3;

This is the one exception. In a bit-field, plain int parts company with signed int, because the standard says that if the type specifier used is int, it is implementation-defined whether the bit-field is signed or unsigned (§6.7.3.2, footnote).

So in a bit-field you always write signed int or unsigned int. Leave it as plain int and x = -1 reads back as -1 on one compiler and as 7 on another. On the machine this book used it was the signed one, but that is this machine’s business, not a rule.

The rule, then: signed is optional decoration on other integer types, a word that changes the type on char, and, on a bit-field, a word whose absence hands the meaning to the implementation.

A common misconception. char is just signed char

It depends on the platform. On x86 Linux it is signed; on ARM Linux and many embedded toolchains it is unsigned. CHAR_MIN in <limits.h> being 0 or SCHAR_MIN tells you which.

The difference bites quietly in one place. Put a byte of 128 or more into a char and compare, and on the signed side it is negative.

char c = 0xFF;
if (c == 0xFF) {}      /* does not hold with a signed char */

So the discipline is simple — char for letters, unsigned char for bytes, int8_t for small numbers. Separate the three uses by name and this trap never appears.

Platform note. C23: bool is an unsigned integer type

C23′s §6.2.5p8 says that bool together with the unsigned types corresponding to the standard signed integer types are collectively the standard unsigned integer types. So bool is an integer type, an arithmetic type, and a scalar.

The measurement confirms it — the type of bool + 0 is int (it was promoted). All that is special is the value: it is always 0 or 1, since putting any scalar into a bool gives 0 for zero and 1 for anything else. C99′s _Bool gained the keyword bool in C23, and <stdbool.h> became a header you may or may not include (chapter 82).

26.4 Integer, real, arithmetic — the collective names

From here comes the source of the vocabulary this book has been using. These names are not branches of the tree but words that gather several branches.

NameWhat it gathers (§6.2.5)Where the name is used
integer typeschar + signed integer + unsigned integer + enumerated typesthe operands of % and <<
real typesinteger types + real floating types (complex excluded)the operands of ++ and -- (chapter 49)
arithmetic typesinteger types + floating types (complex included)the operands of +, -, *, /; what promotion acts on
scalar typesarithmetic types + pointers + nullptr_t (C23)the condition of if and while, the operand of !
aggregate typesarray + struct — ★not unionthe rules for initializer lists

Table 26.7

In chapter 49′s tables of operator contracts you will meet cells such as “operand: a real type or a pointer”, and now they can be read exactly.

A common misconception. “A union is an aggregate too”

Not in C. Section 6.2.5p26 says only “array and structure types are collectively called aggregate types”. The union is missing.

The reason is that “aggregate” means holding several things at once. A union has only one member alive at a time (chapter 48′s active member), so it does not fit that definition.

C++ differs — there a union that meets the conditions is an aggregate. The difference shows when comparing the two languages’ initialisation rules.

type-tree

Figure 26.1 — How the standard divides types. A dashed box is a name that gathers several branches.

26.5 Derived types — made out of what is there

New types can be constructed from object and function types, and what is so constructed is a derived type.

DerivationFrom what to whatMore
arrayfrom element type T to “array of T”chapter 38
structureholding several types in sequencechapter 46
unionholding several types overlappingchapter 48
functionfrom return type T to “function returning T”chapter 24
pointerfrom referenced type T to “pointer to T”chapter 35
atomic_Atomic(T) — a conditional featurechapter 80

Table 26.8

These constructions apply recursively. “An array of 10 pointers to int” and “a pointer to a function returning int” are both built that way. That recursion is exactly why chapter 60, “Reading declarations”, is hard, and the standard gathers three of them — array, function and pointer — under the name derived declarator types. Those three are precisely what must be unwrapped from the inside out when reading a declaration.

26.6 Qualifiers — a qualified edition of the same type

A type qualifier does not make a new type; it makes a qualified version. C23 fixes the list at four, and that is all of them.

QualifierWhat it promisesBreak it andMore
constI will not change it through this namecompile error (constraint violation)chapter 23
volatileit may change without my knowing, so do not remove or merge accessesvalues go quietly wrongchapters 13, 76, 77
restrictthis object is reached only through this pointer★ undefined behaviour — no diagnosticchapters 38, 40
_Atomicaccesses to this object are not torna data race = undefined behaviourchapter 80

Table 26.9

A common misconception. “Isn’t register a qualifier too?”

No. And the confusion is very common. register is a storage-class specifier, of the same family as static, extern and typedef (chapter 44). The two do different jobs and sit in different syntactic slots.

Type qualifierStorage-class specifier
What it qualifiesthe typethe object (that name)
How manyall four may be combined★ in principle one
What it fixeswhat may be donelifetime, scope, linkage
Exampleconst volatile intstatic int

Table 26.10

One question separates them: “can it be pulled out with typedef?” const int is one whole type, so typedef const int ci; works; static is not part of a type, so typedef static int si; is a syntax error (measured: multiple storage classes in declaration specifiers).

26.6.1 A qualified edition is the same size as the original

A qualified type and an unqualified one have the same size, representation and alignment — what changes is only what may be done.

examples-en/ch26/qualifiers.c

/* See all four type qualifiers in one place.
   The point: a qualified type is another edition of the same type - neither
   its size nor its alignment changes. */
#include <stdio.h>
#include <stdalign.h>

struct big { char a[9]; };

int main(void)
{
    puts("(1) a qualifier changes neither size nor alignment");
    printf("  int            : %zu / %zu\n", sizeof(int), alignof(int));
    printf("  const int      : %zu / %zu\n", sizeof(const int), alignof(const int));
    printf("  volatile int   : %zu / %zu\n", sizeof(volatile int), alignof(volatile int));
    printf("  struct big     : %zu / %zu\n", sizeof(struct big), alignof(struct big));
    printf("  _Atomic big    : %zu / %zu   <- unchanged on this implementation\n",
           sizeof(_Atomic struct big), alignof(_Atomic struct big));

    puts("\n(2) the order is free and they may be combined");
    const volatile int a = 1;
    volatile const int b = 2;      /* exactly the same type as above */
    printf("  const volatile int a = %d,  volatile const int b = %d\n", a, b);

    puts("\n(3) _Atomic has two faces");
    _Atomic int   c = 3;           /* written as a qualifier */
    _Atomic(int)  d = 4;           /* written as a type specifier - same type */
    printf("  _Atomic int c = %d,  _Atomic(int) d = %d\n", c, d);
    /* typedef int A[3];  _Atomic A x;   <- cannot qualify an array: compile error */

    puts("\n(4) const only says \"not through this name\"");
    int  x  = 10;
    const int *p = &x;             /* cannot change it through p */
    x = 20;                        /* but x itself still can */
    printf("  changed x directly, so *p = %d  <- const is a promise about the route, not the value\n", *p);
    /* *p = 30;  <- error: assignment of read-only location */

    puts("\n(5) a qualifier may be added silently, never removed");
    const int ci = 7;
    const int *q = &ci;            /* adding: allowed */
    printf("  int * -> const int * passes; the reverse warns (-Wdiscarded-qualifiers), *q = %d\n", *q);
    /* int *bad = &ci;  <- warning: initialization discards 'const' qualifier */

    return 0;
}

Output

(1) a qualifier changes neither size nor alignment
  int            : 4 / 4
  const int      : 4 / 4
  volatile int   : 4 / 4
  struct big     : 9 / 1
  _Atomic big    : 9 / 1   <- unchanged on this implementation

(2) the order is free and they may be combined
  const volatile int a = 1,  volatile const int b = 2

(3) _Atomic has two faces
  _Atomic int c = 3,  _Atomic(int) d = 4

(4) const only says "not through this name"
  changed x directly, so *p = 20  <- const is a promise about the route, not the value

(5) a qualifier may be added silently, never removed
  int * -> const int * passes; the reverse warns (-Wdiscarded-qualifiers), *q = 7

Output ① confirms it. const int and volatile int are both four bytes, aligned the same.

★ One caveat belongs on _Atomic. The standard does not require an atomic type to have the same size and alignment as its corresponding non-atomic type. If an implementation has to hide a lock inside, it may grow. The nine-byte struct in the example was unchanged here, but it need not be — that is the contract. Which is why atomic types come with a separate question, atomic_is_lock_free (chapter 80).

26.6.2 The order is free and they may be combined

As output ② shows, const volatile int and volatile const int are the same type. A qualifier may go before or after the type specifier, and all four may be used together.

Writing the same qualifier twice is allowed in C23 as well — it counts as appearing once. Going through a typedef makes that easy to do by accident.

typedef const int ci;
const ci x = 1;        /* const twice - legal, counts as once */

The compiler kindly says so (GCC’s -Wduplicate-decl-specifier). Nobody writes it on purpose, but the rule saves programs where macros and typedefs meet.

26.6.3 Only _Atomic has two faces

Of the four, only _Atomic serves both as a qualifier and as a type specifier.

_Atomic int  c;      /* written as a qualifier */
_Atomic(int) d;      /* written as a type specifier - the same type */

Output ③ shows the two are the same. The parenthesised form exists because it is needed to wrap a complicated type_Atomic(int *) is “an atomic pointer”, and int * _Atomic says the same thing but reads badly.

★ There is also one restriction peculiar to _Atomic: it cannot be applied to arrays or functions. There is no way to make a whole array untearable at once (measured: '_Atomic'-qualified array type). _Atomic int a[3]; is fine, but that means each element is atomic, not the array.

26.6.4 A qualifier is a property of the route, not of the value

Output ④ is this section’s point. Even with const int *p = &x;, x itself can still be changed. const does not say “this value is constant”; it says “I will not change it through this name.”

26.6.5 It may be added silently, never removed

Output ⑤ shows the direction. Passing an int * where a const int * is wanted is legal; the reverse warns (-Wdiscarded-qualifiers). Only the direction that increases qualification is allowed quietly.

★ There is a famous trap in this rule, though. Passing a char ** where a const char ** is wanted is not allowed: pointer compatibility does not carry one level down. Chapter 60 follows the standard’s clauses through that argument.

26.6.6 Where it attaches flips the meaning

The place a qualifier most often causes accidents is the pointer.

DeclarationWhat is read-only
const int *pthe pointed-to value
int const *pthe same — only the order differs
int *const pthe pointer itself
const int *const pboth

Table 26.11

One line separates them: a qualifier qualifies the thing immediately to its left; if there is nothing to its left, the thing to its right. That is clause C of chapter 60′s precedence rule, and it is split apart there by running it.

Q. So are qualifiers and storage classes the only words a declaration takes?

A. No. The standard divides what may appear in the declaration specifier slot into five families. This book treats each family in the chapter where it actually causes trouble.

FamilyThe wordsGathered in
type specifiersvoid, char, int, struct, enum, typeof, …this chapter
type qualifiersconst, volatile, restrict, _Atomicthis section
storage-class specifiersauto, constexpr, extern, register, static, thread_local, typedefchapter 44
function specifiersinline, _Noreturnchapter 24
alignment specifieralignaschapter 47

Table 26.12

★ Those five are all of them. So stacking one from each family, as in static const volatile unsigned long x;, is fine; taking two from one family is not, where that family is limited to one (as storage classes are).

26.7 Types with their width in the name — <stdint.h>

That was the types the language gives; now the ones the standard library names for us. They are the direct answer to the problem that int’s size differs by implementation (the next chapter).

examples-en/ch26/stdint_kinds.c

/* The three families of <stdint.h>, and the traps of uint8_t. */
#include <inttypes.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    puts("[three families — they demand different things]");
    printf("  %-16s %-6s %s\n", "type", "size", "what it guarantees");
    printf("  %-16s %-6zu %s\n", "uint8_t",       sizeof(uint8_t),
           "exactly 8 bits, no padding (optional)");
    printf("  %-16s %-6zu %s\n", "uint_least8_t", sizeof(uint_least8_t),
           "smallest with at least 8 bits (required)");
    printf("  %-16s %-6zu %s\n", "uint_fast8_t",  sizeof(uint_fast8_t),
           "usually fastest with at least 8 bits (required)");
    printf("  %-16s %-6zu %s\n", "uint_least32_t", sizeof(uint_least32_t), "at least 32 bits");
    printf("  %-16s %-6zu %s\n", "uint_fast32_t",  sizeof(uint_fast32_t),  "at least 32 bits, speed first");
    printf("  %-16s %-6zu %s\n", "uintmax_t",      sizeof(uintmax_t),      "the widest integer (required)");
    printf("  %-16s %-6zu %s\n", "uintptr_t",      sizeof(uintptr_t),      "void* round trip (optional)");

    puts("\n[trap 1: uint8_t is usually unsigned char, so it leaks as a character]");
    uint8_t age = 65;
    printf("  with %%u: %u\n", age);
    printf("  with %%c: %c   <- 'A' comes out, not 65\n", age);
    printf("  putchar(age) is the same trap\n");

    puts("\n[trap 2: arithmetic promotes to int — this is not 8-bit arithmetic]");
    uint8_t a = 200, b = 100;
    printf("  a + b        = %d   <- computed as int: 300, it did not wrap\n", a + b);
    printf("  (uint8_t)(a+b) = %u   <- put it back to make it wrap\n", (uint8_t)(a + b));
    printf("  sizeof(a + b) = %zu  <- the result is 4 bytes\n", sizeof(a + b));

    puts("\n[trap 3: being an alias, the format must follow the alias]");
    printf("  with PRIu8: %" PRIu8 "\n", age);
    printf("  UINT8_C(200) is %u, UINT8_MAX is %u\n", UINT8_C(200), UINT8_MAX);

    puts("\n[which to use when]");
    puts("  protocols, file formats, registers -> exact width (uint32_t)");
    puts("  portability first, width at least -> minimum width (uint_least16_t)");
    puts("  loop counters, local sums          -> fastest (uint_fast32_t)");
    puts("  sizes, indices, byte counts        -> size_t (<stddef.h>)");
    return 0;
}

Output

[three families — they demand different things]
  type             size   what it guarantees
  uint8_t          1      exactly 8 bits, no padding (optional)
  uint_least8_t    1      smallest with at least 8 bits (required)
  uint_fast8_t     1      usually fastest with at least 8 bits (required)
  uint_least32_t   4      at least 32 bits
  uint_fast32_t    8      at least 32 bits, speed first
  uintmax_t        8      the widest integer (required)
  uintptr_t        8      void* round trip (optional)

[trap 1: uint8_t is usually unsigned char, so it leaks as a character]
  with %u: 65
  with %c: A   <- 'A' comes out, not 65
  putchar(age) is the same trap

[trap 2: arithmetic promotes to int — this is not 8-bit arithmetic]
  a + b        = 300   <- computed as int: 300, it did not wrap
  (uint8_t)(a+b) = 44   <- put it back to make it wrap
  sizeof(a + b) = 4  <- the result is 4 bytes

[trap 3: being an alias, the format must follow the alias]
  with PRIu8: 65
  UINT8_C(200) is 200, UINT8_MAX is 255

[which to use when]
  protocols, file formats, registers -> exact width (uint32_t)
  portability first, width at least -> minimum width (uint_least16_t)
  loop counters, local sums          -> fastest (uint_fast32_t)
  sizes, indices, byte counts        -> size_t (<stddef.h>)

26.7.1 What separates the three families is “what they demand”

FamilyNamesWhat it guaranteesIs it there?
exact widthint8_t, uint32_t, …exactly N bits, no padding bits, two’s complementoptional — defined only by implementations that have such a type
minimum widthint_least8_t, uint_least32_t, …the smallest with at least N bits8, 16, 32, 64 are required
fastestint_fast8_t, uint_fast32_t, …usually the fastest with at least N bits8, 16, 32, 64 are required
holding a pointerintptr_t, uintptr_ta void * put in and taken back out compares equaloptional (chapter 35)
widestintmax_t, uintmax_tholds the value of any integer typerequired

Table 26.13

The measurement shows the difference — on this machine uint_fast32_t was eight bytes. Thirty-two bits would have sufficed and it took sixty-four; that is what “fast” means (matching the register width is faster). It is not a type for saving space.

Q. “Optional”? Is there really a machine without uint8_t?

A. Rare, but yes. And “optional” is only half right — the standard’s condition is more precise.

“ If an implementation provides standard or extended integer types with a particular width and no padding bits, it shall define the corresponding typedef names. (§7.22.1.1) ”

So an implementation does not leave them out as it pleases: it cannot define them when the machine has no such type. uint8_t is the case in point — some DSPs have a 16-bit char, so there is no type that is “exactly 8 bits” at all, and uint8_t goes undefined. On a machine where CHAR_BIT is not 8, every width that is not a multiple of it drops out together.

★ The wider the type, the better the odds. A machine without uint32_t is far rarer — it would have to be one where no integer type is exactly 32 bits, such as an old mainframe with a 36-bit word. So if you are worried about absence, the worry is almost always about uint8_t and uint16_t.

Practice judges it this way. If you face only desktops, servers and mainstream embedded targets, use the exact-width types freely (rungs 1–2 of chapter 12′s ladder). If you write a library facing every C implementation, use the minimum-width types and, where exact width is genuinely required, let the build say so with something like static_assert(sizeof(uint8_t) == 1) (rung 3).

26.7.2 The three traps of uint8_t

The most used, and the most injuring, so it gets its own treatment.

Counter-example.

  1. A number goes in and a letter comes out

uint8_t age = 65;
printf("%c\n", age);      /* 'A' comes out */
putchar(age);             /* the same trap */

uint8_t is usually an alias for unsigned char, so it slots straight into a place expecting a character. The types match, so the compiler says nothing.

uint8_t is safer used as “a byte” than as “a small number”. For a small number a person will read, use int or int16_t; if it must be printed, use %u (it is promoted, so it arrives as unsigned) or PRIu8.

Counter-example.

  1. Thinking it is 8-bit arithmetic

uint8_t a = 200, b = 100;
a + b     /* 300, not 44 */

The measurement shows it. uint8_t is narrower than int, so before arithmetic it is promoted to int (the chapter after next). The computation happens in 32 bits, and to make it wrap you must put it back(uint8_t)(a + b) is 44.

This is a safeguard rather than a defect: multiply two narrow types and the intermediate result is not cut off. Only the expectation that “I used an 8-bit variable so it will compute in 8 bits” must be abandoned.

Counter-example.

  1. Thinking it is a new type

A typedef makes an alias, not a new type (chapter 60). So uint8_t and unsigned char are the same type, and neither _Generic nor overloading can tell them apart. The measurement’s first output is the evidence.

For the same reason the compiler will not catch this.

void send(uint8_t port, uint8_t value);
send(value, port);        /* swapped, and it stays silent */

To distinguish them by type, wrapping in a struct is C’s way — struct port { uint8_t v; };. The price is clumsier syntax; what you buy is the compiler’s checking.

26.7.3 So which do you use

In this placeuse thiswhy
protocols, file formats, hardware registersexact width uint32_tthe byte count is the contract (chapter 48)
portable code facing every implementationminimum width uint_least16_texact width is optional
loop counters, local computationfastest uint_fast32_t, or simply intwhere speed is worth more than width
sizes, indices, byte countssize_t (<stddef.h>)it is sizeof’s type and covers any array
the difference of two pointersptrdiff_t (<stddef.h>)a sign is needed (chapter 38)
a small number a person will readintit gets promoted to int anyway

Table 26.14

Emphasise that last row as practice’s default. With no particular reason, use int. The types with a width in their name are a tool for places where the width is the contract, not something good to sprinkle about for thrift.

In practice. Where does size_t live?

A frequently confused spot, so it is written down. size_t and ptrdiff_t belong not to <stdint.h> but to <stddef.h>. <stdint.h> is the home of the types with a width in their name; <stddef.h> is the home of the types the language itself uses (size_t, ptrdiff_t, nullptr_t, max_align_t).

In practice another header such as <stdio.h> often drags size_t in for you, which blurs the distinction — and the day that header changes, it breaks. The discipline is to include the header that defines the type you use (the same grain as chapter 55′s story about names).

26.8 Summary — the standard’s words and this book’s chapters

The standard’s wordWhat it isThe chapter that faces it
object type / function typewhat holds a value / what workshere, chapters 24, 59
complete / incomplete typeis the size knownhere, chapter 47
basic typeschar + integer + floatinghere, chapters 27, 49
character typesthe three char, signed char, unsigned charhere, chapters 9, 42
integer typeschar + integer + enumeratedchapter 27
real typesinteger + real floatingchapter 49
arithmetic typesinteger + floatingchapter 29 (promotion)
derived typesarray, struct, union, function, pointer, atomicchapters 35–38, 46–48
scalar typesarithmetic + pointer + nullptr_tchapters 30, 36
aggregate typesarray + structchapters 38, 46
qualified typesconst, volatile, restrict, _Atomic — fourthis ch., 23, 38, 79
storage-class specifiersstatic, extern, register, … sevenchapter 44
function specifiersinline, _Noreturnchapter 24

Table 26.15

The families are laid out. From the next chapter we dig into its cells one at a time — first integers. The family passed over here in the single phrase “the signed and unsigned integer types” gets its insides, its ranges, its representation and its accidents.