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&, butscanf("%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
wordalready is an address. - We also saw indexing is really arithmetic on an address:
a[2]lives atbase + 2 * (bytes per element). - "A value that is an address" is the idea we name today: the pointer. Once we
have it, the
scanfrule, 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¶
&xis the address-of operator: it yields the address wherexlives.int *pdeclarespas "pointer toint." The type matters: a pointer tointis different from a pointer tocharordouble.- A pointer is just another value, so it has its own address too. For now we only
care that
ppoints atx.
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 */
*pis the dereference (or "indirection") operator: "the thingppoints at." You can read it (int y = *p;) or write it (*p = 99;).- Writing through
pchangesxitself, becausepholdsx'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.
xnames the box.&xis the address of the box,1000.pholds1000.*pfollows the arrow back to the box, the value99.
Two details to flag now¶
NULLis 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
%pfor debugging:printf("x lives at %p\n", (void *) &x);. The exact number is not important and changes every run; what matters is that&xandpprint 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;
}
- Note that we never wrote
x = 99directly. We changedxby 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 */
scanfcannot 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&nis - a pointer ton. Insidescanf, it does the equivalent of*address = the_number_read;, writing through the pointer just like our demo.wordis already an address (segment 1), so it needs no&. Passing&wordwould 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.
scanfis 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 + 1does not add1to the address. It adds1 * sizeof(int)bytes, becausepis a pointer toint. 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 + ilands exactly on elementi, 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 + nis 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 instantpreaches 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
nalongside).
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
atoa + 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_setcannot touchx.
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 originalx. This is exactly whatscanfdoes 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;
}
swap_badshuffles its private copies and returns; nothing inmainmoves.swapdereferences 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 frommain. 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
qandr, 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."
scanfitself 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
divideas above and call it. Watch the order of arguments and that you pass&qand&r, notqandr.
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;
}
brokenhands 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 print42, 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.
&xmakes one;*pfollows it to read or write the thing it points at. int *pis "pointer to int." The type sets the element size for arithmetic:p + 1steps one element, anda[i]is exactly*(a + i).- An array name decays to the address of its first element, which is why
scanf("%s", word)needs no&, whyint 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.