Proven C Book한국어 GitHub

36 Null — the three siblings, formally

What to know first

chapter 6, Special knowledge about addresses · what is special about address zero
chapter 35, Objects, addresses, pointers · a pointer is a value that points at something

Looking back

Chapter 6 showed the history that “a null pointer’s internal representation need not be 0” (the Prime 50, the CDC Cyber 180). Then does the comparison p == 0 in source code — break on such machines?

A. It does not — that is the standard’s promise. The integer constant 0 in the source is read in a pointer context as a null pointer constant, and the compiler translates it so that the comparison is against that machine’s actual null representation. The compiler upholds the separation between the symbol (the 0 in the source) and the representation (the bits in the machine). What does break is another kind of code — code that assumes at the bit level that “the representation is 0.” We see that trap shortly.

The need for this chapter, and its context

The three lookalikes named “null”, introduced by face in chapter 6, are now handled properly. That this comes immediately after pointers is the point: the moment you first hold a pointer is the moment you first meet a null dereference. The safety rule has to arrive before the accident.

By the end of this chapter

The three nulls whose faces we learned in chapter 6 — the null pointer, NULL/nullptr, and the NUL character — get formal treatment in syntax and practical rules. How to handle “empty” is the first chapter of the pointer safety code.

The questions this chapter answers

  1. When does the NUL character ('\0') get its formal treatment — one of the three siblings is still outstanding.

36.1 nullptr — the name of emptiness

A pointer variable must be made to point at something the moment it is declared, or, if there is no target yet, must state that it is empty (chapter 23′s initialisation rule is even more vital for pointers — following a rubbish address being the worst accident). Two notations for emptiness are current — the traditional NULL macro and the keyword nullptr brought in by C23. This book uses nullptr: NULL historically had definitions that differed by implementation with subtle traps (variadic arguments and so on), while nullptr is a modern notation clear down to its type.

examples-en/ch36/null.c

#include <stdio.h>

int main(void)
{
    int *p = nullptr;           /* it points nowhere yet */

    if (p == nullptr) {
        printf("p is empty for now\n");
    }

    int n = 7;
    p = &n;
    if (p != nullptr) {
        printf("now p points to %d\n", *p);
    }
    return 0;
}

Output

p is empty for now
now p points to 7

The rule is exactly this demonstration — check whether a pointer is empty before using it. Dereferencing an empty pointer (*p) is outside the contract (undefined behaviour), and as chapter 6 taught, in a protected-mode environment the program usually collapses on the spot — and that dying loudly is the kinder outcome is also exactly chapter 6′s story.

In practice. The billion-dollar mistake — an apology from null’s inventor

The very value “empty” has a history of controversy. Tony Hoare, who first introduced the null reference into an ALGOL-family language in 1965 (the same Hoare of the quicksort family of algorithms, chapter 31), called it in a 2009 lecture “my billion-dollar mistake” — a self-indictment that “I could not resist the temptation of it being easy to implement, and the cost of the collapses and vulnerabilities null dereferences have caused over the following forty years must exceed a billion dollars.” That modern languages (Rust, Swift, Kotlin) evolved to force a check on “may be empty” through the type system is a direct descendant of that reflection. C has no such enforcement — checking is the business of culture and discipline, which is why this book pins the check down as idiom.

36.2 How far may 0 be used — the exact definition of a null pointer constant

Chapter 6 said “the 0 in the source is a symbol”. The standard fixes exactly what qualifies as that symbol. A null pointer constant is one of two things:

C23 added the keyword nullptr to those. So the five spellings below all mean the same thing.

SpellingA null pointer constant?Note
0YesThe oldest spelling
0L, 0u, '\0'YesAny integer constant expression with value 0
(void *)0YesNULL is commonly defined this way
NULLYesA macro the implementation defines as one of the above
nullptrYesC23. Its type, nullptr_t, is unambiguous
zero in const int zero = 0;NoA variable, not a constant expression — a compile error
An int variable that became 0 at run timeNoPutting an integer into a pointer is another matter

Table 36.1

The last two rows are the point. What qualifies is not being zero but being a constant expression. So passing a variable whose value is zero, as in int *p = zero;, is not a null assignment but “an integer into a pointer” — something else entirely, and today’s compilers reject it.

36.2.1 The bits it becomes are another matter

The same spelling does not mean the same representation. Chapter 6′s thread is picked up here.

examples-en/ch36/nullrep.c

/* The 0 in the source and the bits in memory — spelling and representation. */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* Look straight at the bytes of one pointer */
static void dump(const char *how, const void *pobj, size_t n)
{
    const unsigned char *b = pobj;
    int all_zero = 1;

    printf("  %-22s", how);
    for (size_t i = 0; i < n; i++) {
        printf(" %02X", b[i]);
        if (b[i] != 0) all_zero = 0;
    }
    printf("   all bits zero? %s\n", all_zero ? "yes" : "no");
}

struct node {
    int          id;
    struct node *next;      /* a pointer member */
    double       weight;    /* a floating member */
};

int main(void)
{
    printf("pointer size on this implementation: %zu bytes\n\n", sizeof(int *));

    /* (1) all three spellings are null pointer constants — the syntax layer */
    puts("[the same null, written three ways]");
    int *a = 0;          dump("int *a = 0;",       &a, sizeof a);
    int *b = NULL;       dump("int *b = NULL;",    &b, sizeof b);
    int *c = nullptr;    dump("int *c = nullptr;", &c, sizeof c);
    printf("  are the three equal? %s\n",
           (a == b && b == c) ? "yes — the standard promises it" : "no");

    /* 0L and (void *)0 are null pointer constants too. A variable holding 0
       is not:
         const int zero = 0;
         int *p = zero;        <- a compile error: a variable, not a constant. */
    int *d = 0L;         dump("int *d = 0L;",        &d, sizeof d);
    int *e = (void *)0;  dump("int *e = (void *)0;", &e, sizeof e);

    /* (2) comparison and assignment always hold, whatever the bits are */
    puts("\n[comparison holds regardless of representation]");
    printf("  a == 0      : %s\n", a == 0       ? "true" : "false");
    printf("  a == NULL   : %s\n", a == NULL    ? "true" : "false");
    printf("  a == nullptr: %s\n", a == nullptr ? "true" : "false");
    printf("  !a          : %s\n", !a           ? "true" : "false");

    /* (3) two ways of filling a struct with zero — they mean different things */
    puts("\n[two ways of emptying a struct]");
    struct node x = { 0 };              /* the value layer: null and 0.0 promised */
    struct node y;
    memset(&y, 0, sizeof y);            /* the representation layer: all bits zero */

    dump("next after { 0 }", &x.next, sizeof x.next);
    dump("next after memset", &y.next, sizeof y.next);
    printf("  x.next == nullptr : %s   <- the standard promises it\n",
           x.next == nullptr ? "true" : "false");
    printf("  x.weight == 0.0   : %s   <- the standard promises it\n",
           x.weight == 0.0 ? "true" : "false");
    printf("  y.next == nullptr : %s   <- true only on this implementation\n",
           y.next == nullptr ? "true" : "false");

    /* (4) calloc belongs to the all-bits-zero side too */
    puts("\n[what calloc gives is zeroed bits]");
    int **arr = calloc(4, sizeof *arr);
    if (!arr) { perror("calloc"); return 1; }
    dump("arr[0] from calloc", &arr[0], sizeof arr[0]);
    printf("  arr[0] == nullptr : %s   <- true only on this implementation\n",
           arr[0] == nullptr ? "true" : "false");
    free(arr);

    puts("\nIn short: the 0 in the source is a *spelling*; the bits are a");
    puts("      *representation*. Comparison and assignment live in the first");
    puts("      layer and always hold; memset and calloc live in the second");
    puts("      and promise nothing about null.");
    return 0;
}

Output

pointer size on this implementation: 8 bytes

[the same null, written three ways]
  int *a = 0;            00 00 00 00 00 00 00 00   all bits zero? yes
  int *b = NULL;         00 00 00 00 00 00 00 00   all bits zero? yes
  int *c = nullptr;      00 00 00 00 00 00 00 00   all bits zero? yes
  are the three equal? yes — the standard promises it
  int *d = 0L;           00 00 00 00 00 00 00 00   all bits zero? yes
  int *e = (void *)0;    00 00 00 00 00 00 00 00   all bits zero? yes

[comparison holds regardless of representation]
  a == 0      : true
  a == NULL   : true
  a == nullptr: true
  !a          : true

[two ways of emptying a struct]
  next after { 0 }       00 00 00 00 00 00 00 00   all bits zero? yes
  next after memset      00 00 00 00 00 00 00 00   all bits zero? yes
  x.next == nullptr : true   <- the standard promises it
  x.weight == 0.0   : true   <- the standard promises it
  y.next == nullptr : true   <- true only on this implementation

[what calloc gives is zeroed bits]
  arr[0] from calloc     00 00 00 00 00 00 00 00   all bits zero? yes
  arr[0] == nullptr : true   <- true only on this implementation

In short: the 0 in the source is a *spelling*; the bits are a
      *representation*. Comparison and assignment live in the first
      layer and always hold; memset and calloc live in the second
      and promise nothing about null.

The demonstration puts the two layers side by side. All five spellings become the same value (a == b && b == c), and every comparison is true — the spelling layer is what the standard promises. The printed bytes, on the other hand, are merely what this implementation chose. Run the same program on chapter 6′s Prime 50 or CDC Cyber 180 and the comparisons would still be true while the bytes would not be zero.

What you doWhich layerDoes it promise null?
p = nullptr; / p = 0;Spelling★ Yes
p == nullptr / !pSpelling★ Yes
struct s x = { 0 };Value★ Yes — pointer members null, floating members 0.0
memset(&x, 0, sizeof x)RepresentationNo
calloc(n, size)RepresentationNo
memcmp(&p, zeros, sizeof p)RepresentationNot a way to ask whether it is null

Table 36.2

The third and fourth parts of the demonstration show those rows in the flesh. A struct made with { 0 } has the standard’s promise that its pointer members are null and its floating members 0.0, while memset and calloc merely happen to give the same result on this machine.

Platform note. How C++ differs — the same 0, other rules

This is the famous place where C and C++ part. The root is one thing: C++ has no implicit conversion from void * to another pointer type.

So C++‘s NULL cannot be defined as ((void *)0); that would make int *p = NULL; an error outright. Checked on this machine, C’s NULL is ((void *)0) while g++‘s is the compiler extension __null. Traditional C++ implementations simply defined it as 0 or 0L.

Using 0 as null creates an accident peculiar to C++: overloading.

void f(int);
void f(char *);
f(NULL);      // if NULL is 0 -> f(int) is quietly chosen

Run it and, with NULL defined as 0, f(int) wins; with g++‘s __null the call is ambiguous and fails to compile. Either way it is not the f(char *) that was meant.

WhatCC++
(void *)0 into another pointerFineError — a cast is required
The definition of NULLUsually ((void *)0)0, 0L or __null
A const int variable holding 0Not null (not a constant expression)Null in C++03, an error from C++11
A dedicated keywordC23′s nullptrC++11′s nullptr (type std::nullptr_t)

Table 36.3

The third row’s history is interesting. C++03 said “an integral constant expression rvalue of integer type that evaluates to zero”, so the zero of const int zero = 0; was a null pointer constant; C++11 narrowed it to “an integer literal with value zero, or a prvalue of type std::nullptr_t”. This book checked that the same code compiles under -std=c++03 and fails under -std=c++11.

The conclusion is the same in both languages — use nullptr. C++11 brought it in first and C23 followed, and one word removes both the overloading accident and the ambiguity of “is this a zero or a pointer?”.

36.3 The trap — code that assumes the representation is 0

Let us collect the foreshadowing of the opening exchange. These two pieces of code do not mean the same thing:

int *arr[8];
for (int i = 0; i < 8; i += 1) { arr[i] = nullptr; }  /* correct */
/* memset(arr, 0, sizeof arr);     — fill the bits with 0: a different meaning! */

The first line says “hold a null pointer” (the world of symbols — whatever the representation, the compiler handles it). The commented memset says “fill every bit with 0” (the world of representation). On today’s mainstream machines, where the null representation is all zeros, the results coincide by accident, but as chapter 6′s history shows, that accident is not a contract. You will often see the memset style in practice (and on mainstream platforms it does work), but with the eye of portability it is a grey zone, and using it knowingly differs from using it unknowingly — this book’s choice is the explicit nullptr assignment.

Q. When does the NUL character ('\0') get its formal treatment — one of the three siblings is still outstanding.

A. Three chapters later. The NUL character works not in the world of pointers but in the world of strings (as the end marker), so chapter 42, which treats strings properly, is its stage. While waiting, the distinction once more — nullptr is “nowhere to point” (a pointer value), '\0' is “the text ends here” (a character value, one byte in size). Strangers living in different worlds.

We can handle emptiness. The next chapter takes the remaining rules that bind pointers — the constraint chapter 6′s alignment places on pointer casts, and a practical feel for the provenance (the tag of origin) foreshadowed in chapter 14.