Why this matters
A pointer is a variable, so it has an address too, and that address can be stored in another pointer. This sounds like a puzzle for its own sake, but it answers a practical question: how does a function change the caller's pointer (rather than the thing it points to)? Library functions like strtol and getline need exactly that, and argv is a pointer to pointers. This lesson also sorts out const with pointers, where the position of one keyword changes the meaning completely, so you can read real-world declarations like const char *const *names.
A pointer to a pointer
#include <stdio.h>
int main(void)
{
int x = 5;
int *p = &x;
int **pp = &p;
printf("x = %d, *p = %d, **pp = %d\n", x, *p, **pp);
**pp = 6;
printf("after **pp = 6: x = %d\n", x);
int y = 50;
*pp = &y; /* changes p itself */
printf("after *pp = &y: *p = %d, x = %d\n", *p, x);
return 0;
}
x = 5, *p = 5, **pp = 5
after **pp = 6: x = 6
after *pp = &y: *p = 50, x = 6
int **pp declares "pointer to pointer to int": **pp is an int, *pp is an int *. Each * follows one arrow:
pp p x
+------+ +------+ +-----+
| *---+------->| *---+------->| 5 |
+------+ +------+ +-----+
int ** int * int
pp the address of p
*pp p itself (the address of x)
**pp x
After *pp = &y; the middle arrow moves:
pp p x
+------+ +------+ +-----+
| *---+------->| * | | 6 |
+------+ +--+---+ +-----+
| y
| +-----+
+----------->| 50 |
+-----+
Why: letting a function move the caller's pointer
Suppose you want a function that skips leading spaces in a string by advancing the caller's pointer. A first attempt:
#include <stdio.h>
void skip_spaces_wrong(const char *s)
{
while (*s == ' ') {
s++;
}
}
int main(void)
{
const char *cur = " x";
skip_spaces_wrong(cur);
printf("[%s]\n", cur);
return 0;
}
[ x]
Nothing happened, for the same reason swap_broken failed two lessons ago: s is a copy of cur, and moving the copy does not move the original. The fix is the same too: to change a variable of type T in the caller, pass a T *. Here T is const char *, so the parameter is const char **:
#include <stdio.h>
/* Moves the caller's pointer past any leading spaces. */
void skip_spaces(const char **s)
{
while (**s == ' ') {
(*s)++;
}
}
int main(void)
{
const char *text = " indented";
const char *cur = text;
skip_spaces(&cur);
printf("[%s] -> [%s], skipped %td\n", text, cur, cur - text);
return 0;
}
[ indented] -> [indented], skipped 4
Inside the function, *s is the caller's cur and **s is the character it points at. (*s)++ advances cur. The parentheses are essential: postfix ++ binds tighter than unary *, so *s++ would mean *(s++), moving the local s and reading through the old value.
| you want the function to change | the caller passes | the parameter type |
|---|---|---|
an int | &n | int * |
an int * (where it points) | &p | int ** |
a const char * | &cur | const char ** |
The standard library does this
strtol (string to long), which the I/O module covers in detail, converts the start of a string to a number and reports where it stopped through a char ** output parameter:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *input = "42 apples";
char *end;
long n = strtol(input, &end, 10);
printf("number: %ld, rest: \"%s\"\n", n, end);
return 0;
}
number: 42, rest: " apples"
You pass &end, the address of your pointer; strtol stores into it a pointer to the first character it did not use.
argv is a char **
The parameter char *argv[] is, like every array parameter, really a pointer: char **argv. Both spellings of main are equivalent. argv points at the first element of the array of string pointers; argv[1] is *(argv + 1), a char *; argv[1][0] is the first character of the first argument.
argv (char **)
+------+ +---------+
| *---+------>| argv[0] |----> "./prog\0"
+------+ +---------+
| argv[1] |----> "pear\0"
+---------+
| argv[2] |----> "apple\0"
+---------+
| NULL |
+---------+
Sorting such an array means rearranging the pointers in the middle column; the strings never move. That is the last exercise of this lesson.
const and pointers
With a pointer there are two things that could be read-only: the object pointed to, and the pointer variable itself. Where you put const decides which. The reliable trick is to read the declaration from right to left, starting at the name:
| declaration | read as | *p = ... | p = ... |
|---|---|---|---|
const int *p | p is a pointer to an int that is const | no | yes |
int const *p | (same as above; const before or after the type) | no | yes |
int *const p | p is a const pointer to an int | yes | no |
const int *const p | p is a const pointer to a const int | no | no |
int x = 1, y = 2;
const int *a = &x;
int *const b = &x;
*a = 5; /* error */
a = &y; /* fine */
*b = 5; /* fine */
b = &y; /* error */
main.c:6:8: error: assignment of read-only location '*a'
main.c:9:7: error: assignment of read-only variable 'b'
In practice, const T * ("pointer to const") is by far the most common: it is what a function parameter uses to promise "I only read through this". T *const is rarer; it is a pointer you promise not to re-aim, like any other const variable.
const int *a = &x; does not make x itself constant. It only means you will not change x through a. x can still change by other means.
Conversions follow the safety rule: you may add const implicitly (int * to const int *), but not drop it. Passing a const char * to a parameter of type char * makes gcc report passing argument 1 of 'f' discards 'const' qualifier from pointer target type, because the function could then modify read-only data.
Common mistakes
*s++when you meant(*s)++: the first moves the local pointer, the second the caller's.- Passing
curinstead of&curto aconst char **parameter: gcc reports an incompatible pointer type (an error in GCC 14). - Confusing
const int *andint *const: read right to left. - Casting away
constwith(char *)to silence a warning, then writing through the pointer. If the data really is read-only (a literal), the program crashes. - Comparing strings with
>while sorting:a[i] > a[j]compares addresses. Usestrcmp.
Recap
T **pppoints to aT *; each*follows one arrow:*ppis the pointer,**ppthe object.- To let a function change the caller's pointer, pass its address (
&p), takeT **, and assign through*pp.strtol'sendptrworks this way. char *argv[]ischar **argv: a pointer to an array of string pointers.- Read declarations right to left:
const int *pprotects the int,int *const pthe pointer. - Adding
constis implicit; removing it needs a cast and is usually a bug.
Going further: man 3 getline describes a function that takes char **lineptr and may replace your buffer with a bigger one, which needs malloc (next module). Read its description and notice why it needs a pointer to your pointer.