5.1.8 · D4C Programming

Exercises — Pointers — declaration, dereferencing ( - ), address-of (&)

2,288 words10 min readBack to topic

We will use a running memory diagram convention. Look at the picture below once — every later problem reuses this exact street-of-houses layout.

Figure — Pointers — declaration, dereferencing ( - ), address-of (&)

Level 1 — Recognition

Goal: can you read pointer notation and say what each symbol asks for?

Exercise 1.1

Given int x = 8; and int *p = &x;, state in plain words what each of these evaluates to: &x, p, *p, *(&x).

Recall Solution 1.1
  • &x → the address of x (a house number, e.g. 0x1000). Type: int *.
  • p → the same address — we just copied &x into p, so p == 0x1000.
  • *pgo to the address in p and read the int there → 8.
  • *(&x)& makes the address of x, then * follows it straight back → 8 (equals x).

WHY the last two are equal: p holds &x, so *p and *(&x) follow the same address.

Exercise 1.2

For each declaration, say whether the named variable is a pointer or a plain value: int a; int *b; int *c, d; char *s;

Recall Solution 1.2
  • int a; → plain int.
  • int *b;pointer to int.
  • int *c, d;c is a pointer to int, but d is a plain int. The * binds only to c.
  • char *s;pointer to char.

WHY d is not a pointer: * attaches to the individual declarator, not the whole line.


Level 2 — Application

Goal: can you predict what code prints?

Exercise 2.1

int x = 5;
int *p = &x;
*p = 20;
printf("%d\n", x);

What prints, and why?

Recall Solution 2.1

Prints 20.

  • p = &x makes p point at x.
  • *p = 20 says "go to p's target and write 20 there". p's target is x.
  • So x becomes 20. There is only one house here — p just names it.

Exercise 2.2

int m = 3, n = 9;
int *p = &m;
int *q = &n;
int t = *p;
*p = *q;
*q = t;
printf("%d %d\n", m, n);

What prints?

Recall Solution 2.2

Prints 9 3.

  • t = *p → save m's value: t = 3.
  • *p = *q → write n's value into m's house: m = 9.
  • *q = t → write saved 3 into n's house: n = 3. This is exactly the classic swap done inline through pointers. See Pass by Reference vs Pass by Value.

Exercise 2.3

int a[3] = {10, 20, 30};
int *p = &a[0];
printf("%d %d %d\n", *p, *(p + 1), *(p + 2));

What prints?

Recall Solution 2.3

Prints 10 20 30.

  • p holds the address of a[0].
  • p + 1 does not add 1 byte — it adds sizeof(int) bytes (typically 4), landing exactly on a[1]. This is Pointer Arithmetic.
  • So *(p+1) = a[1] = 20, and *(p+2) = a[2] = 30.

Level 3 — Analysis

Goal: can you trace addresses and spot why code behaves a certain way?

Exercise 3.1

Suppose int x lives at address 0x1000 and sizeof(int) == 4. We do:

int x = 42;
int *p = &x;

On a 64-bit machine, give: (a) the value of p, (b) sizeof(x), (c) sizeof(p), (d) *p.

Recall Solution 3.1
  • (a) p == 0x1000 — it stores the address of x.
  • (b) sizeof(x) == 4x is an int (4 bytes).
  • (c) sizeof(p) == 8p is a pointer; all pointers on a 64-bit machine are 8 bytes, regardless of what they point at.
  • (d) *p == 42 — follow p to 0x1000, read the int.

WHY (b) and (c) differ: sizeof(p) measures the pointer itself, not its target.

Exercise 3.2

void addFive(int n)  { n = n + 5; }
void addTen(int *n)  { *n = *n + 10; }
 
int main(void) {
    int v = 100;
    addFive(v);
    addTen(&v);
    printf("%d\n", v);
}

What prints, and explain the difference between the two functions.

Recall Solution 3.2

Prints 110.

  • addFive(v) receives a copy of 100. It adds 5 to the copy, then the copy is discarded. v in main is untouched → still 100.
  • addTen(&v) receives the address of v. *n = *n + 10 walks back to main's v and writes 110.
  • So only addTen changes the caller. 100 + 10 = 110.

This is the whole reason pointers exist as parameters — see Pass by Reference vs Pass by Value.


Level 4 — Synthesis

Goal: can you assemble multiple pointer ideas into one working piece?

Exercise 4.1

Write a function swap3 that rotates three int variables so a→b→c→a. Then trace:

int a = 1, b = 2, c = 3;
swap3(&a, &b, &c);   // want a=3, b=1, c=2
printf("%d %d %d\n", a, b, c);
Recall Solution 4.1
void swap3(int *a, int *b, int *c) {
    int t = *c;   // save c's value (3)
    *c = *b;      // c gets b's value (2)
    *b = *a;      // b gets a's value (1)
    *a = t;       // a gets saved c (3)
}

Trace with a=1, b=2, c=3:

  • t = *ct = 3.
  • *c = *bc = 2.
  • *b = *ab = 1.
  • *a = ta = 3. Result: a=3 b=1 c=2. Prints 3 1 2. Because we pass addresses, the changes land on main's variables.

Exercise 4.2

int a[4] = {5, 10, 15, 20};
int *p = a;          // array name decays to &a[0]
p = p + 2;           // advance two elements
*p = *p + 100;
printf("%d %d %d %d\n", a[0], a[1], a[2], a[3]);

What prints?

Recall Solution 4.2

Prints 5 10 115 20.

  • int *p = a; → the array name a decays to the address of a[0] (Arrays in C).
  • p = p + 2; → advance two elements (2 × sizeof(int) bytes) → p now points at a[2].
  • *p = *p + 100;a[2] was 15, becomes 115.
  • Only a[2] changes; the rest are untouched → 5 10 115 20.

Level 5 — Mastery

Goal: reason about pointers to pointers, NULL safety, and degenerate cases.

Exercise 5.1 — Pointer to a pointer

int x = 7;
int *p = &x;
int **pp = &p;
printf("%d %d %d\n", x, *p, **pp);
**pp = 50;
printf("%d\n", x);

What do the two printfs print? Explain **pp.

Recall Solution 5.1

First line prints 7 7 7; second prints 50.

  • p = &xp points at x.
  • pp = &ppp points at p. So pp is a pointer to a pointer (int **).
  • *pp → follow pp once → you reach p (which is an address).
  • **pp → follow twice → reach x's value → 7.
  • **pp = 50 → two hops land on x; write 50 there. So x becomes 50. Look at the two-hop chain in the figure below.
Figure — Pointers — declaration, dereferencing ( - ), address-of (&)

Exercise 5.2 — NULL and the degenerate case

int *p = NULL;
if (p != NULL)
    printf("%d\n", *p);
else
    printf("empty\n");

What prints, and what would happen if we removed the if guard?

Recall Solution 5.2

Prints empty.

  • p = NULLp points at "nowhere" (address 0), a safe sentinel.
  • The guard p != NULL is false, so the else runs → prints empty.
  • If we removed the guard and wrote *p directly: dereferencing address 0 is illegal → undefined behaviour, almost always a segmentation fault / crash. See NULL and Segmentation Faults.

WHY the guard: always confirm a pointer targets valid memory before *p.

Exercise 5.3 — The inverse identity under pressure

Without running code, argue whether *(&(*p)) equals *p for a valid pointer p. What if p is NULL?

Recall Solution 5.3

For a valid p: *(&(*p)) equals *p.

  • *p names the target object (an int you can read or write).
  • &(*p) asks "what is the address of that object?" → that is just p again.
  • * of p → back to the value → *p. The & and inner * cancel. For NULL p: it breaks. *p first tries to access address 0 → crash before any cancellation can "logically" happen. The identity is only safe when p points at real memory.

Lesson: algebraic identities on pointers assume a valid pointer; a NULL/dangling pointer voids the guarantee.


Recall wrap-up

Recall Quick self-check — predict before revealing

Level of each hop before you open.

  • *(p + 1) for int *p reads which element? ::: the element one int further on, i.e. p[1].
  • Why does addFive(v) leave v unchanged? ::: C copies the argument; the function edits a copy.
  • How many hops does **pp take? ::: two (pp → p → value).
  • What happens on *p when p == NULL? ::: illegal access to address 0 → crash / undefined behaviour.

Connections

  • Pointer Arithmetic — the scaling behind p + n.
  • Arrays in C — array-to-pointer decay used in 4.2.
  • Pass by Reference vs Pass by Value — the L3 copy-vs-address contrast.
  • NULL and Segmentation Faults — the L5 degenerate case.
  • Dynamic Memory malloc free — pointers to heap memory (next step).
  • Strings as char pointerschar * follow-on drills.