Pointers and Memory Addresses
A pointer is a typed value that stores the address of an object. It lets a function reach caller-owned data, lets array traversal describe a current position, and makes memory relationships explicit—but only while the address, lifetime, type, and valid range all agree.
Every safe pointer answers four questions
An object and its address are different values
int score = 18; creates an int object named score. The expression score reads its value, 18. The expression &score produces the address where that object lives. Because the object is an int, the address has type int *—“pointer to int.”
A real address may look like 0x7ffd..., but its numeric spelling is not the useful contract. Addresses can differ across runs, machines, optimization levels, and operating systems. Reason from identity—“the address of score”—instead of memorizing a particular number.
scoreAn int object with its own storage and lifetime.
18The whole-number value currently stored in that object.
&scoreA pointer value identifying where the object lives.
*pointerThe object reached by following a valid pointer.
Give scanf an address where it may store input
scanf needs &score because it must write into the existing score object rather than receive a copy of its current value.
Edit the program, predict its output, then run it.
Declare the pointed-to type, then initialize the pointer
int score = 18;
int *score_pointer = &score;
printf("value: %d\n", score);
printf("address: %p\n", (void *)&score);
printf("stored address: %p\n", (void *)score_pointer);The star in int *score_pointer belongs to the declaration: it says the variable stores the address of an int. Initialize a pointer at declaration whenever possible. An uninitialized local pointer has an indeterminate value; following it is not a search for a useful object—it is undefined behavior.
Dereference means “the object at this address”
If score_pointer contains &score, then *score_pointer designates the same object as score. In a value expression it reads the object. On the left side of assignment it writes the object.
int score = 18;
int *score_pointer = &score;
printf("before: %d\n", *score_pointer);
*score_pointer = 20;
printf("after: %d\n", score);The pointer is not the integer 18 and it is not another score object. It stores an address. The dereferenced expression identifies the original object, so the assignment changes what a later read through score observes.
A pointer parameter creates an explicit mutation channel
C passes every argument by value, including pointers. A function receives a copy of the address, but both the caller's pointer expression and the parameter can lead to the same object. That is how a function can update caller-owned data without C having a separate pass-by-reference mechanism.
void add_bonus(int *score, int bonus) {
if (score == NULL) {
return;
}
*score = *score + bonus;
}
int main(void) {
int result = 16;
add_bonus(&result, 2);
printf("%d\n", result);
return 0;
}- 01The caller takes an address
&resultidentifies the caller-ownedint. - 02The call copies that pointer value
The parameter
scorereceives its own pointer value, not a new integer object. - 03The function validates the address
The null check proves the pointer represents an object before dereference.
- 04Dereference reaches shared storage
*scoredesignatesresult, so assignment is visible to the caller.
NULL means “no object,” not “safe object”
NULL is a null pointer constant. Use it when a pointer deliberately refers to no object yet or when “not found” is part of an interface. It is valid to assign, compare, or return a null pointer. It is never valid to dereference one.
Points to a live object
The type agrees, the object's lifetime is active, and the intended access remains inside its bounds.
Points to no object
Test pointer != NULL before dereference when null is permitted by the function contract.
Contains an indeterminate value
Do not read or dereference it. Initialize it with a valid address or NULL.
Outlived its object
The old address remains stored, but the referred object's lifetime has ended. Do not use it.
Array expressions connect indexing to pointer arithmetic
In most expressions, an array name converts to a pointer to its first element. For int scores[4], the expression scores usually has the same value as &scores[0]. This is why a function parameter written as int scores[] behaves as int *scores.
Pointer arithmetic advances in elements, not raw bytes. If cursor points to an int, cursor + 1 points to the next int. The identity scores[index] == *(scores + index) explains array indexing; it does not remove the need for a count.
Observe the array object that a pointer would reach
This safe runner keeps pointer arithmetic visible in the explanation while you verify that indexed access reads and writes the same array elements.
Edit the program, predict its output, then run it.
Place const according to what must not change
const int *readingreading = &other; /* yes */ *reading = 4; /* no */The pointer may point elsewhere, but this access path cannot modify the int.int *const reading = &score*reading = 4; /* yes */ reading = &other; /* no */The pointer must keep its initial address, but it may modify the pointed-to int.const int *const reading = &score/* neither the address nor the int may change through reading */Both promises apply to this access path.An address is valid only during the object's lifetime
A local object normally lives from the execution of its declaration until control leaves its block. Returning &local_value from a function produces a pointer to an object whose lifetime ends as the function returns. The address may still look plausible, but dereferencing it is undefined behavior.
int *broken_result(void) {
int local_value = 42;
return &local_value; /* wrong: local_value soon stops existing */
}Later, dynamic allocation introduces another lifetime boundary: allocated storage remains alive until free, and every retained pointer to it becomes invalid after that call. For now, prefer addresses of caller-owned objects whose lifetime clearly covers the function call.
Print addresses with %p and void *
Use printf("%p\n", (void *)pointer); for diagnostic address output. Do not use %d: an address is not an int, and mismatching a variadic format with its argument type is undefined behavior. Treat the printed address as temporary diagnostic evidence, not stable application data.
Independent lab: update caller-owned readings safely
First run the browser starter to practice supplying three valid element addresses to scanf. Then copy the local-compiler extension below and replace each indexed update with a small pointer function. Keep the count beside the pointer whenever more than one element is accessible.
Store three readings through explicit addresses
Each &readings[index] identifies one live element inside the array; the bounded loop then consumes the same three objects.
Edit the program, predict its output, then run it.
void increase_all(int *values, int count, int amount) {
if (values == NULL || count < 0) {
return;
}
for (int index = 0; index < count; index++) {
*(values + index) += amount;
}
}score and score_pointer; put the value in one and an arrow in the other.void swap(int *left, int *right), validate both pointers, and exchange the two pointed-to values.sum(const int *values, int count) without modifying the array.Lesson review
&objectproduces the object's address; its pointer type follows the object type.*pointerdesignates the pointed-to object only when the pointer is valid to dereference.- C passes a pointer parameter by value, but that copied address can still reach caller-owned storage.
- A pointer does not carry an array length. Pass the valid element count separately.
- Pointer arithmetic advances by pointed-to elements and must remain within one array object or its one-past boundary.
NULLrepresents no object and must be checked before dereference when the contract permits it.- Uninitialized and dangling pointers do not identify objects you may safely access.
- Use
const int *for read-only element access and%pwithvoid *for diagnostic address output.