Skip to content

Lecture 5 - Pointers, Pointer Arithmetic, and Function Arguments

This lecture pays off the Lecture 4 teaser - "why does scanf("%s", word) take no &?" - by introducing the pointer: a value that is an address. We name the & and * operators, make array indexing precise, and use pointers to let a function reach back and change its caller's variables.

1. Recap - an array name is an address

  • Last lecture ended on an oddity: scanf("%d", &n) needs an &, but scanf("%s", word) does not.
  • The one-line answer we gave: an array's name, used in an expression, evaluates to the address of its first element. So word already is an address.
  • We also saw indexing is really arithmetic on an address: a[2] lives at base + 2 * (bytes per element).
  • "A value that is an address" is the idea we name today: the pointer. Once we have it, the scanf rule, array decay, and passing arrays to functions all stop being special cases and become one idea.

2. Pointers - &, *, and int *p

Every variable lives somewhere in memory, and that somewhere has a numeric address. A pointer is a variable whose value is an address.

Address-of and the pointer type

int x = 41;
int *p = &x;     /* p holds the address of x; read "int star p" */
  • &x is the address-of operator: it yields the address where x lives.
  • int *p declares p as "pointer to int." The type matters: a pointer to int is different from a pointer to char or double.
  • A pointer is just another value, so it has its own address too. For now we only care that p points at x.

Dereferencing - reaching the thing pointed at

printf("%d\n", *p);   /* prints 41: follow p to x, read it */
*p = 99;              /* write THROUGH p: this changes x   */
printf("%d\n", x);    /* prints 99 */
  • *p is the dereference (or "indirection") operator: "the thing p points at." You can read it (int y = *p;) or write it (*p = 99;).
  • Writing through p changes x itself, because p holds x's address. This is the whole power of pointers and the key to segment 5.

Box-and-arrow picture

Picture two boxes. x is a box holding 99. p is a box holding an arrow that points at the x box. &x makes the arrow; *p follows it.

      +------+            +------+
  x:  |  99  | <--------- |  p   |
      +------+    (arrow) +------+
      addr 1000           holds 1000
  • x names the box.
  • &x is the address of the box, 1000.
  • p holds 1000.
  • *p follows the arrow back to the box, the value 99.

Two details to flag now

  • NULL is the "points at nothing" pointer value. int *p = NULL; deliberately points nowhere; dereferencing it is a crash (a useful crash - it fails loudly instead of corrupting memory). Always have a pointer point somewhere real before you dereference it.
  • You can print an address with %p for debugging: printf("x lives at %p\n", (void *) &x);. The exact number is not important and changes every run; what matters is that &x and p print the same address.

Example - watch a write travel through a pointer

#include <stdio.h>

int main(void) {
    int x = 41;
    int *p = &x;

    printf("x = %d, *p = %d\n", x, *p);   /* same value, two names */
    *p = 99;                              /* write through the pointer */
    printf("x = %d, *p = %d\n", x, *p);   /* x changed too */
    return 0;
}
x = 41, *p = 41
x = 99, *p = 99
  • Note that we never wrote x = 99 directly. We changed x by writing through a pointer to it. Hold that thought for function arguments.

3. The scanf mystery, resolved

int n;
scanf("%d", &n);        /* an int needs the & */

char word[256];
scanf("%s", word);      /* a string does not  */
  • scanf cannot return your value to you the normal way; it must write into your variable, so you have to hand it the address to write to. That is exactly what &n is - a pointer to n. Inside scanf, it does the equivalent of *address = the_number_read;, writing through the pointer just like our demo.
  • word is already an address (segment 1), so it needs no &. Passing &word would hand over the wrong type.
  • This is the same machine as segment 2: to let someone else change your variable, you give them a pointer to it. scanf is just a function that takes pointers so it can fill in your variables.

4. Pointer arithmetic and arrays

a[i] is *(a + i)

When you write a[i], C literally computes *(a + i): start at the array's address, step forward i elements, and dereference.

int a[4] = {10, 20, 30, 40};
printf("%d\n", a[2]);        /* 30 */
printf("%d\n", *(a + 2));    /* 30 - exactly the same thing */
  • This is why indexing starts at 0: a[0] is *(a + 0), the first element, with no step at all.

Adding to a pointer steps by elements, not bytes

int a[4] = {10, 20, 30, 40};
int *p = a;        /* p points at a[0]; the array name decays to that address */
p = p + 1;         /* now p points at a[1] */
printf("%d\n", *p);   /* 20 */
  • p + 1 does not add 1 to the address. It adds 1 * sizeof(int) bytes, because p is a pointer to int. The compiler knows the element size from the pointer's type and scales for you. That is why the pointer's type matters.
  • So a + i lands exactly on element i, and *(a + i) reads it. Indexing and pointer arithmetic are two spellings of one operation.

Walking a pointer to the end

int a[5] = {10, 20, 30, 40, 50};
int n = 5;

int total = 0;
for (int *p = a; p < a + n; p++) {   /* p walks from a[0] up to one-past-the-end */
    total += *p;
}
printf("sum = %d\n", total);          /* 150 */
  • a + n is the address one past the last element. It is a legal place to point (the standard guarantees it) as a stop marker, but you must not dereference it. The loop stops the instant p reaches it.
  • This pointer-walk is the same loop as for (int i = 0; i < n; i++), written with the address moving instead of the index.

Why int a[] as a parameter is really int *a

int sum(int a[], int n) { /* ... */ }   /* the [] here is a polite fiction */
int sum(int *a, int n) { /* ... */ }    /* this is what the compiler sees */
  • When you pass an array to a function, the array name decays to a pointer to its first element. The function receives only that address - a copy of the pointer, not a copy of the array.
  • That explains both facts from Lecture 4 at once: the function can modify the caller's array (it has the real address, segment 5), and it cannot recover the length (an address carries no count, so you pass n alongside).

Exercise - sum an array with a walking pointer

Exercises handout, Part B, Exercise B1 (on the computer).

  • Rewrite the array sum using a pointer that walks from a to a + n, with no [] anywhere. Same answer as the index loop, different spelling.

5. Pointers as function arguments

C passes arguments by value

void try_to_set(int v) {
    v = 99;            /* changes only this function's private copy */
}

int main(void) {
    int x = 1;
    try_to_set(x);
    printf("%d\n", x);  /* still 1 - the copy was thrown away */
    return 0;
}
  • A function gets its own copy of each argument. Assigning to a parameter changes the copy and nothing in the caller. This is pass by value, and it is why try_to_set cannot touch x.

To change the caller's variable, pass its address

void set_to_99(int *p) {
    *p = 99;            /* write through the pointer: the caller's variable */
}

int main(void) {
    int x = 1;
    set_to_99(&x);      /* hand over the address of x */
    printf("%d\n", x);  /* 99 */
    return 0;
}
  • The function still gets a copy - but a copy of the address. Following that address (*p = 99;) reaches the original x. This is exactly what scanf does for you, now in your own code.

Example - the broken swap, then the fix

/* BROKEN: swaps two copies, leaves the originals alone */
void swap_bad(int a, int b) {
    int t = a; a = b; b = t;
}

/* FIXED: swaps through addresses, so the caller's variables change */
void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

int main(void) {
    int x = 3, y = 7;
    swap_bad(x, y);
    printf("after swap_bad: x = %d, y = %d\n", x, y);   /* 3 7 - unchanged */
    swap(&x, &y);
    printf("after swap:     x = %d, y = %d\n", x, y);   /* 7 3 - swapped */
    return 0;
}
after swap_bad: x = 3, y = 7
after swap:     x = 7, y = 3
  • swap_bad shuffles its private copies and returns; nothing in main moves.
  • swap dereferences the addresses, so every read and write lands on the caller's variables. The & at the call site and the * inside the function are the two halves of the same handshake.

Exercise - write swap yourself

Exercises handout, Part B, Exercise B2 (on the computer).

  • Write void swap(int *a, int *b) and prove it works from main. The mistake to catch: forgetting a * and swapping the pointers' values instead of what they point at.

6. Out-parameters - returning more than one value

A function returns one value with return. To hand back two or more results, pass in pointers for the function to fill. These are called out-parameters.

void divide(int a, int b, int *quotient, int *remainder) {
    *quotient = a / b;
    *remainder = a % b;
}

int main(void) {
    int q, r;
    divide(17, 5, &q, &r);
    printf("17 / 5 = %d remainder %d\n", q, r);   /* 3 remainder 2 */
    return 0;
}
  • The caller declares q and r, passes their addresses, and reads them after the call. The function writes both results through the pointers.
  • This is the standard C answer to "a function can only return one thing." scanf itself uses the same pattern: its return value is a status, and your data comes back through the pointers you passed.

Exercise - divide with two results

Exercises handout, Part B, Exercise B3 (on the computer).

  • Write divide as above and call it. Watch the order of arguments and that you pass &q and &r, not q and r.

7. Scope and lifetime

Pointers make it tempting to pass addresses around freely. There is one rule that keeps that safe, and one classic bug when you break it.

Scope - where a name is visible

  • A variable declared inside a block ({ ... }) is visible only from its declaration to the closing brace. Function parameters and locals are visible only inside that function.
  • An inner block can introduce its own variable that shadows an outer one; when the block ends, the name is gone again.

Lifetime - when the variable exists

  • A local variable is created when its function is entered and destroyed when that function returns. Its storage is reused by the next call. This is automatic lifetime.
  • While a function runs, its locals are alive and their addresses are valid. The moment it returns, those addresses point at storage that no longer belongs to anyone.

The classic bug - returning the address of a local

int *broken(void) {
    int local = 42;
    return &local;     /* BUG: local dies the instant we return */
}

int main(void) {
    int *p = broken();
    printf("%d\n", *p);   /* undefined behavior: reading dead storage */
    return 0;
}
  • broken hands back the address of a variable that ceases to exist as the function returns. The pointer is now dangling: it names storage that is no longer valid. Reading or writing through it is undefined behavior - it may print 42, may print garbage, may crash.
  • The fix is not a pointer trick. The value has to live somewhere that outlives the call. Two ways exist: return the value by copy (int broken(void)), or get memory that you control the lifetime of - the heap.
  • That raises the real question: where do locals actually live, such that they appear and vanish with each call? The answer is the call stack, and the heap is the alternative. Both are Lecture 6.

8. Wrap-up

  • A pointer is a value that is an address. &x makes one; *p follows it to read or write the thing it points at.
  • int *p is "pointer to int." The type sets the element size for arithmetic: p + 1 steps one element, and a[i] is exactly *(a + i).
  • An array name decays to the address of its first element, which is why scanf("%s", word) needs no &, why int a[] parameters are really pointers, and why such a function never learns the array's length.
  • C passes arguments by value. To let a function change your variable - or return more than one result - pass a pointer to it (scanf, swap, out-parameters).
  • A local's lifetime ends when its function returns; returning the address of a local leaves a dangling pointer.
  • Cliffhanger: where do locals live so they can appear and vanish per call, and how do we get memory that outlives a function? The call stack and the heap, next lecture.