A Variable Whose Value Is an Address

You have been writing & since chapter 1. printf("%p\n", (void *)&age) to see where a variable lives, scanf("%d", &age) to let a function put something in it. In every one of those lines an address was produced, used, and thrown away at the semicolon. You have never kept one. A pointer is a variable whose value is an address. That is the entire definition, and the rest of this lesson is consequences. score holds 21; a pointer aimed at score holds the number of the byte where score begins.

#include <stdio.h>

int main(void)
{
    int score = 21;
    int *p = &score;

    printf("score is %d, and lives at %p\n", score, (void *)&score);
    printf("p holds    %p\n", (void *)p);
    printf("*p reads   %d\n", *p);

    *p = 42;

    printf("score is now %d\n", score);

    return 0;
}
score is 21, and lives at 0xfbff9daf0020
p holds    0xfbff9daf0020
*p reads   21
score is now 42

Read int *p = &score; as an ordinary declaration: a type, a name, an initializer. The type is int *, spoken "pointer to int", and it is the type name gcc already used on you in chapter 1, when a scanf call was missing its & and the compiler said it expected an int *. The initializer is &score, an address, which is precisely what a variable of that type may hold. Two of those output lines carry the same number, and that is the whole point: p does not contain 21, it contains the location of the 21. The number itself belongs to one run and will differ on the next, for the reason chapter 1 gave, but within a run the two agree by construction.

    p                      score
 +---------------+       +------+
 | 0xfbff...020  | ----> |  21  |
 +---------------+       +------+

Reading and Writing Through the Pointer

*p is the other half of the idea. Written in an expression, * is the dereference operator, meaning "the object at this address". So *p is not a copy of score, it is score, reached by a different route: printf("%d", *p) reads it and *p = 42; writes it. That assignment is the line to stare at, because score is never mentioned in it and yet the final printf, which reads score by its own name, prints 42. The write went through the pointer and landed in the variable.

That capability is the one the previous two lessons said you did not have, and it arrives with an obligation, because a dereference is a raw memory access with nothing checking it. A pointer carries a type, and the type is what makes the dereference mean anything. An address alone says only where something starts, exactly as chapter 1 put it; int * adds that four bytes live there and are to be read as an int. A double * holding the identical number would read eight bytes and interpret them completely differently. That distinction has a spelling: sizeof p is the size of the pointer itself, 8 bytes here, while sizeof *p is the size of the thing it points at, 4.

Two Meanings of a Star

One character, two unrelated jobs, and telling them apart is most of learning to read pointer code. In a declaration, * shapes the type: int *p; says "p is something which, when dereferenced, is an int". In an expression, * performs the dereference. So int *p = &score; declares, *p = 42; dereferences, and the two lines share a symbol and no meaning whatsoever.

That reading also settles where to put the star. Write int *p;, not int* p;. The * belongs to the declarator, the name being declared, rather than to the type, and C proves it the moment you declare two things on one line. int* a, b; declares a as an int * and b as a plain int, because the * attached to a alone, and the spacing that suggested otherwise is invisible to the compiler. Nothing warns you, because nothing is wrong yet: under -Wall -Wextra that line compiled here in total silence. You find out later, when you try to use b as the pointer you thought you had:

declare.c: In function 'main':
declare.c:9:7: error: assignment to 'int' from 'int *' makes integer from pointer without a cast [-Wint-conversion]
    9 |     b = &value;
      |       ^

Write int *a, b; and the line says what it does at a glance.

The Pointer That Points at Nothing

Sometimes you need a pointer before you have anything to aim it at. C reserves a value for that: NULL, a pointer value guaranteed not to be the address of any object. Its canonical home is <stddef.h>, but <stdio.h> defines it too, so it is already available in every program in this course. Initialize every pointer. A pointer declared with no initializer holds an indeterminate value exactly as an int does, and an indeterminate pointer is not NULL. It is an arbitrary number that will be treated as an address the instant you dereference it, which is undefined behaviour with no diagnostic and no reliable symptom. If you cannot yet say where a pointer points, write NULL and you have at least written something you can test for.

Testing is the other half of the bargain, because dereferencing a null pointer is undefined behaviour too, and the standard promises nothing about a program past that point. What this platform does, with AddressSanitizer built into every program, is report it. The program below is wrong on purpose:

#include <stdio.h>

int main(void)
{
    int *p = NULL;

    *p = 5;

    return 0;
}
==12==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
==12==The signal is caused by a WRITE memory access.
==12==Hint: address points to the zero page.
    #0 0x0000004006dc in main /tmp/nothing.c:7
SUMMARY: AddressSanitizer: SEGV /tmp/nothing.c:7 in main

You have met that report before. Chapter 1 produced it line for line from scanf("%d", age) with the & left off: the same SEGV on unknown address 0x000000000000, the same WRITE memory access. It is one mistake seen from two ends. Something was handed a number to write through, and the number was zero. So when a pointer can be null, check it before dereferencing, and write the comparison out as if (p != NULL). Bare if (p) does the same work, and the spelled-out form says which of the many things that could be false you actually meant. Checking is not a plan by itself, though. What matters is what the null path does, and the next section is where that gets an answer.

Handing a Function an Address

Here is the payoff the chapter has been building toward. Lesson 1 proved a function cannot change your variable, because every argument arrives as a copy. Lesson 2 showed where the copy lives: a separate variable in a separate frame at a different address. Neither fact changes now. What changes is what you copy. Give a function an address as its value, and it can reach the variable that address names. A function taking a pointer so that it can write a result through it is using an out-parameter.

#include <stdio.h>

void split_seconds(int total, int *minutes, int *seconds)
{
    if (minutes == NULL || seconds == NULL)
    {
        return;
    }

    *minutes = total / 60;
    *seconds = total % 60;
}

int main(void)
{
    int minutes = 0;
    int seconds = 0;

    split_seconds(200, &minutes, &seconds);

    printf("200 seconds is %d minutes and %d seconds\n", minutes, seconds);

    return 0;
}
200 seconds is 3 minutes and 20 seconds

split_seconds returns void, and main nevertheless ends up with two answers it did not have before, printed from its own variables under their own names. Pass by value is still in full force: minutes inside the function is a copy, a distinct variable in the callee's frame, holding a copy of the address of main's minutes. Copying an address hands over a second route to the same object, and that is the mechanism in one sentence. It is also how a function produces more than one result, which return on its own cannot do. The guard at the top is the null path, and it is a decision rather than a formality. split_seconds has nowhere to put an answer if a caller passed NULL, so it does nothing at all, and returning early is how it says so. A function with a status to report would return an int failure code instead, and its caller would check that the way chapter 2 checked the count from scanf.

Now read that call once more, and then read this: scanf("%d", &score). They are the same pattern. scanf is an ordinary function with an out-parameter, and the & you have typed since chapter 1 was building a pointer argument every single time. You were never following a special rule about scanf; you were handing an address to a function that had to write. This lesson was promised as the one that makes that mechanism yours, and that is it.

Look But Do Not Touch

Not every pointer parameter exists to be written through. A function may take one only to read what it points at, and it can say so in the type. const int * is a pointer to an int that the function promises not to modify, and the compiler holds it to the promise. This program is wrong on purpose and does not build:

#include <stdio.h>

void report(const int *value)
{
    printf("value is %d\n", *value);
    *value = 0;
}

int main(void)
{
    int n = 5;

    report(&n);

    return 0;
}
report.c: In function 'report':
report.c:6:12: error: assignment of read-only location '*value'
    6 |     *value = 0;
      |            ^

Delete the assignment and it compiles clean and prints value is 5. The const costs nothing at run time and buys two things: whoever reads the signature alone knows the argument is safe from this function, and a slip of the finger becomes a build error instead of a surprise in some caller. Make const T * your default for any pointer parameter you only read. One thing this lesson has quietly relied on throughout is this: whatever a pointer points at is still there. Every pointer above aimed at a live local variable in a frame further up the stack, and every one of them was finished with before that frame was popped. A pointer can outlive the thing it points at, and what happens then is the next lesson.

Key Takeaways

  • A pointer is a variable whose value is an address. int *p = &score; declares p with type int * and aims it at score.
  • * means two different things. In a declaration it shapes the type; in an expression it is the dereference operator. *p is the object itself, so *p reads it and *p = 42; writes it, changing the variable without naming it.
  • Write the star with the name, int *p;, because int* a, b; declares one pointer and one int and no warning tells you so.
  • A pointer's type decides how many bytes a dereference touches. sizeof *p is the size of the pointed-at object, sizeof p the size of the pointer.
  • Initialize every pointer. An uninitialized pointer is not NULL, it is arbitrary. Dereferencing a null or indeterminate pointer is undefined behaviour; here AddressSanitizer reports SEGV on unknown address 0x000000000000 caused by a WRITE memory access, the identical report chapter 1 got from a scanf missing its &. Check with if (p != NULL) whenever a pointer can be null, and decide what the null path actually does rather than only that it is checked.
  • Give a function an address and it can modify the caller's variable, which is an out-parameter. It is how one call yields several results, and scanf("%d", &x) has been exactly this pattern since chapter 1.
  • const int * is a pointer a function may read but not write through, enforced as error: assignment of read-only location. Use it for every pointer parameter you do not modify.