Pointers

A pointer holds the address of an object. Through it a function can read and change memory that belongs to its caller, which is the mechanism behind the out-parameters of the C library. The basics are collected on Pointers in C; this lesson builds the working habits the rest of the course uses.

Address and dereference

&value produces the address of an object, and *p names the object at that address. The two operations are inverses: a pointer initialised with &value reads and writes exactly that variable. Printing the address needs %p and a cast to void *.

#include <stdio.h> int main(void) { int value = 42; int *p = &value; printf("value: %d\n", value); printf("*p: %d\n", *p); printf("address: %p\n", (void *) p); *p = 7; printf("value now: %d\n", value); return 0; }

gcc -Wall -Wextra addr.c -o addr
./addr

value: 42
*p: 42
address: 0x7ffef8b8f0cc
value now: 7

The address changes on every run because the system places the stack differently; the first, second and last lines are the stable result. Writing through *p changed value, which proves the pointer was not a copy.

NULL and the validity check

NULL is the address that points to no object, and dereferencing it crashes the process. A pointer that may be absent is tested before use. The check is cheap and makes the failure a message instead of a segmentation fault.

#include <stdio.h> int main(void) { int *p = NULL; if (p != NULL) { printf("%d\n", *p); } else { printf("null pointer, not dereferenced\n"); } return 0; }

gcc -Wall -Wextra null.c -o null
./null

null pointer, not dereferenced

The comparison is written out rather than abbreviated: if (p) means the same, but the explicit form tells a reader that p is a pointer, not a number.

Pointers as out-parameters

Lesson 17 showed that a parameter is a copy, so a function cannot change its caller's variable directly. Passing the address changes that: the function receives a copy of the pointer, but the copy points at the caller's object. The classic example swaps two integers.

#include <stdio.h> static void swap(int *a, int *b) { int temporary = *a; *a = *b; *b = temporary; } int main(void) { int x = 1; int y = 2; swap(&x, &y); printf("x: %d, y: %d\n", x, y); return 0; }

gcc -Wall -Wextra swap.c -o swap
./swap

x: 2, y: 1

The temporary exists because the first assignment overwrites *a; without it both variables would end with the same value. The parameters are pointers, so the call passes addresses, and a mistake such as swap(x, y) fails to compile.

More than one result

A function returns one value, so a second result travels through a pointer. Division is the standard case: the quotient and the remainder are both wanted, and the caller declares two variables and passes their addresses. The function writes through the pointers and returns nothing.

#include <stdio.h> static void divide(int a, int b, int *quotient, int *remainder) { *quotient = a / b; *remainder = a % b; } int main(void) { int q = 0; int r = 0; divide(17, 5, &q, &r); printf("17 / 5 = %d remainder %d\n", q, r); return 0; }

gcc -Wall -Wextra divmod.c -o divmod
./divmod

17 / 5 = 3 remainder 2

The caller initialises both outputs, which is not required by the function but makes a missed write visible. A function that can fail usually returns a status and writes its result through a pointer, the pattern of scanf from lesson 10.

An integer is not an address

An integer constant cannot initialise a pointer without a cast, and GCC 15 treats the mismatch as an error rather than a warning. The message names both types and the exact character:

#include <stdio.h> int main(void) { int *p = 42; printf("%d\n", *p); return 0; }

gcc -Wall -Wextra badint.c -o badint

badint.c: In function 'main':
badint.c:5:14: error: initialization of 'int *' from 'int' makes pointer from integer without a cast [-Wint-conversion]
5 | int *p = 42;
| ^~

The second mistake is passing the pointer to %d instead of dereferencing it. The format expects an int and receives an address, and the compiler suggests the conversion that matches a pointer:

#include <stdio.h> int main(void) { int value = 5; int *p = &value; printf("%d\n", p); return 0; }

gcc -Wall -Wextra badfmt.c -o badfmt

badfmt.c: In function 'main':
badfmt.c:8:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'int *' [-Wformat=]
8 | printf("%d\n", p);
| ~^ ~
| | |
| | int *
| int
| %ls

The suggested %ls is not the fix; it matches a wide string. The intended conversion is *p with %d, or (void *) p with %p when the address itself is the subject.

Exercises

  1. Write a function that takes two pointers and stores the larger value in the first and the smaller in the second.
  2. Change the division function to reject a zero divisor through its return value, and show both the success and the failure call.
  3. Print the address of a local variable, a static variable and a function with %p, and describe what the three addresses tell you about the layout of the program.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. How do & and * relate?

    Answer

    & takes an address, * reads through it

    The two operators are inverses: a pointer initialised with &value reads and writes that same object through *p.

  2. Why is a pointer checked against NULL before it is dereferenced?

    Answer

    Because NULL means no object and dereferencing it crashes

    NULL points to no object, so a dereference is undefined and usually ends in a segmentation fault; the check turns it into a message.

  3. How can a function change a variable that belongs to its caller?

    Answer

    By receiving the address of the variable

    The parameter is still a copy, but a copy of the address points at the caller object, so writes through it are visible outside.

  4. How does a function return two results?

    Answer

    Through an out-parameter pointer

    One value comes from return, and the second is written through a pointer the caller supplies.

  5. What does this program print? #include <stdio.h> int main(void) { int value = 42; int *p = &value; printf("%d\n", *p); return 0; }

    Answer

    42

    p holds the address of value, so *p names that object and prints its value.

  6. What does this program print? #include <stdio.h> int main(void) { int value = 42; int *p = &value; *p = 7; printf("%d\n", value); return 0; }

    Answer

    7

    Writing through the pointer changes the pointee, so the original variable now holds 7.

  7. What does this program print? #include <stdio.h> static void swap(int *a, int *b) { int temporary = *a; *a = *b; *b = temporary; } int main(void) { int x = 1, y = 2; swap(&x, &y); printf("x: %d, y: %d\n", x, y); return 0; }

    Answer

    x: 2, y: 1

    The function swaps the values through the addresses, so the caller sees 2 and 1.

  8. What does this program print? #include <stdio.h> static void divide(int a, int b, int *quotient, int *remainder) { *quotient = a / b; *remainder = a % b; } int main(void) { int q = 0, r = 0; divide(17, 5, &q, &r); printf("17 / 5 = %d remainder %d\n", q, r); return 0; }

    Answer

    17 / 5 = 3 remainder 2

    Integer division gives quotient 3 and remainder 2, and both are written through the pointers.

  9. This program is built with gcc -std=c17 -Wall -Wextra badint.c -o badint. Which error does GCC print? #include <stdio.h> int main(void) { int *p = 42; printf("%d\n", *p); return 0; }

    Answer

    makes pointer from integer

    The integer initialiser for int *p is the error described as making a pointer from an integer.

  10. This program is built with gcc -std=c17 -Wall -Wextra badfmt.c -o badfmt. Which warning flag does GCC print? #include <stdio.h> int main(void) { int value = 5; int *p = &value; printf("%d\n", p); return 0; }

    Answer

    -Wformat=

    %d against an int * is a format mismatch, reported by -Wformat=.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev